"Getting out" using exceptions

Prolog has a couple of ways to stop the current computation. halt/1 is the most drastic. Next to that we have thread_exit/1 to terminate a thread immediately providing some value and abort/0 to stop the current execution without leaving Prolog.

Long ago, abort/0 simply discarded the current Prolog stacks and started new ones. With the introduction of threads one needs proper cleanup to ensure no locks are left behind. As we can abandon a computation safely using an exception, abort/0 got implemented as throw(‘$aborted’). But, for several reasons outside the scope of this, many people write code like

 catch(MyGoal, _, fail)

If the abort is raised in MyGoal, the abort is aborted :frowning: This was solved in catch/3 by dynamically wrapping the recovery call in call_cleanup/2 if the ball is '$aborted'. That has done its job for decades.

thread_exit/1 used to be implemented based on pthread_exit(), but this too is problematic as it bypasses Prolog cleanup. The predicate was deprecated and the use of exceptions was suggested. But again, we may have to deal with the exception being caught for the wrong reason.

This post introduced another issue

Python deals with interrupts and sys:exit() using exceptions. But, as Python calls Prolog, signals are not processed and exceptions may not be propagated.

To deal with this in a general way there is now a prototype doing

  • Handle any exception of the shape unwind(Term) as '$aborted' now: force the catch/3 to re-throw it.
  • Use unwind(abort) for the current '$aborted'
  • Introduce unwind(halt(Status)) and `unwind(thread_exit(Term))`` for the obvious purposes.
  • Map halt/1 to throw unwind(halt(Status)) and start the old cleanup after the exception has bubbled to the top.
  • If other threads need to be killed, raise unwind(halt(Status)) in them. The current system uses '$aborted' for the same purpose.

For Janus (Python), we improve on the situation by

  • Mapping SystemExit(code) to/from unwind(halt(Code)) while unwinding the nested Python → Prolog → Python → … stack
  • Do the same for KeyboardInterrupt to/from unwind(keyboard_interrupt))
  • Added janus.heartbeat(count), which calls a dummy Python function every count inferences. This allows Python to handle signals while Prolog is working.

With the above. the interrupt problem raised by @pampelmuse76 can be solved simply by calling this somewhere during the initialization (e.g., right after importing janus).

  janus.heartbeat()

Possibly we should do so automatically? It causes a slowdown of about 1% using the default setting to call Python every 10,000 inferences.

I post this as an RFC to see whether someone can suggest further improvements or changes that make it all nicer. The code is in the current master. Except for some issues such as reported by @josderoo, this should have little consequences.

Interesting thoughts. I forgot about the system_error. I’d be interested to know what the ISO people in those days were thinking about when they wrote this paragraph. As is, it is rather unclear to me. Are they referring to unknown internal errors? what did they have in mind with a “system dependent action”? Maybe someone who was involved is listening in?

The second issue is how to handle such exceptions? You imply make them truly uncatchable. I wrap the recover goal in call_cleanup/2. Both probably have something to say for. That also relates to the status of call_cleanup/2 or setup_call_cleanup/3. These are related to exceptions in the sense that they deal with cleanup under all conditions. They are not ISO though and I do not know how widespread they are at the moment.

A related topic is that we have unification to select exceptions we want to catch, but no hierarchy. This works poorly. For example, when opening a file we may want to act on failures. But, open/3 may raise a permission error or an existence error (and probably some more; various OS errors are hard to map). I want to catch these, but I do not want to catch a type or instantiation error, or a resource error or a timeout, etc. That is really clumsy to specify and as a result people tend to use either catch(open(...), _, Recover) or catch(open(...), error(_,_), Recover). That is why I started using patterns other than error(,) for things that are more alternative control than error, such as timeout or $aborted. unwind(Term) is a generalization of the $aborted (not timeout; we want to be able to catch that, but we want most catch calls to propagate it).

@jfmc proposed yesterday in a discussion to have an alternative for catch/3 that determines whether or not to catch by calling some predicate on the ball. That would allow for a concise description such as

   pcatch(open(...), file_error(Ball), Recover)

But, thinking about this,we can already do

   mycatch(Goal, Ball, Cond, Recover) :-
       catch(Goal, Ball, (Cond->Recover;throw(Ball)).

This particular branch in the hierarchy, yes. With the file_error example I was referring to the idea that instead of using unification, we use classifier predicates, so we can define

file_error(error(existence_error(source_sink, _),_)).
file_error(permission_error(open, source_sink, _), _)).

etc. Now I can easily trap all file related errors and have other errors in the code, such as it calling some undefined predicate (existence_error(procedure, _)) bubble up normally. By using predicates to classify errors we are much more flexible than inheritance based selection of exceptions.

Your sys_trap/3 is adding something to the system that must be used by normal programmers and that is not in ISO. It is merely another way to work around something missing in ISO. To continue with this, we’ll need file_trap/3, arithmetic_trap/3, etc. I don’t think I like that.

At some point I’ve also been thinking about using constraints. So, you can do

 file_error(E),
 catch(Goal, E, Recover).

This doesn’t work for SWI-Prolog at the moment :frowning: