"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.

Since this is a “Request For Comments”, here my comment.
This is a difficult puzzle to solve, since '$aborted' doesn’t follow
the form of an error term, that is found in ISO core standard.

ISO core standard 1st level terms have the form:

error(<2nd level term>, <extra info>)
warning(<2nd level term>, <extra info>)
Etc...

Lets say we don’t want to invent something new, like unwind/1,
what options do we have to distinguish system and user errors,
such that we have:

  • system errors, cannot be catched be catch/3
  • user errors, can be catched by catch/3
  • What else?

Well there is a hint in the ISO core standard:

7.12.2 Error classification
image

So lets use exactly that, and make an implementation dependent
behaviour. It is a litte bit more complex in my current systems, here is
a sketch in that the user catch/3 is a sugared form of the system

sys_trap/3, letting pass through error(system_error(_),_) terms:

catch(A, Pattern, B) :-
   sys_trap(A, Error, sys_error_handler(Error, Pattern, B)).

sys_error_handler(Error, _, _) :- Error = error(system_error(_),_), 
   sys_raise(Error).
sys_error_handler(Error, Error, B) :- !, B.
sys_error_handler(Error, _, _) :-
   sys_raise(Error).

System code can use sys_trap/3 if it wants to catch system
errors. User code using catch/3 will not catch system errors.
BTW: sys_raise/1 is a fast path error thrower, that doesn’t

fill some backtrace. throw/1 is bootstrapped from sys_raise/1.
We have stretched the ISO suggestion a little bit, in that we don’t
use a zero arity system_error/0 rather an unary system_error/1.

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)).

In my take, for the above scenario you wouldn’t need to do
anything. Since file_error/1 would let through system_error/1.
Since the two don’t unify:

?- file_error(_) = system_error(_).
false.

The crucial use case is really only a user application, that does
undiscriminately catch exceptions. In standard catch/3 semantic
the wild card would literally catch any 2nd level term:

pcatch(MyGoal, _, fail)

It would catch any 2nd level term including system erros, because
the wild card unfies with system errors:

?- _ = system_error(_).
true.

Ediit 04.10.2024
In languages like Java and Python the problem is not that aggravated.
Since there catch has a subclass selector. And for wild card caching
you usually pick a class a little bit down in class hierarchy, and this

why system errors that are elsewhere in the class hierarchy
usually are not catched. For example Python defines this class:

exception Exception
All built-in, non-system-exiting exceptions are derived
from this class. All user-defined exceptions should also
be derived from this class.
https://docs.python.org/3/library/exceptions.html#base-classes

On the other hand a system exception is derived from BaseException:

exception KeyboardInterrupt
Raised when the user hits the interrupt key (normally
Control-C or Delete). During execution, a check for
interrupts is made regularly. The exception inherits
from BaseException so as to not be accidentally
caught by code that catches Exception and thus
prevent the interpreter from exiting.
https://docs.python.org/3/library/exceptions.html#KeyError

You cannot do the same in Prolog since Prologs exception mechanism
is based on term unification, and not on subclass membership. On
the other hand sys_trap/3 and catch/3 would basically model the

two cases of catching BaseException respectively Exception.

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: