Using exceptions for "getting out"

ISO defines exceptions to have the shape error(Formal, ImplDefined). Users can throw any term using throw/1, except for a variable.

In the good old days, before threads, SWI-Prolog implemented e.g., abort/0 by simply discarding the stacks and creating new ones. That is now dangerous as the running code may hold locks and discarding may require other cleanup. Cleanup is (I think) under discussion in ISO as SICStus call_cleanup/2 or SWI-Prolog’s derived setup_call_cleanup/3. That allows “getting out” while cleaning up using exceptions.

So, SWI-Prolog’s abort/0 is for a long time throw('$aborted'). Of course, there is a problem as code like catch(goal, _, fail) will now break our abort :frowning: So, SWI-Prolog’s catch/3 recognises certain exceptions and when found, wraps the cleanup handler of catch/3 into call_cleanup(OrigHandler, throw(Ex)), which will re-throw the exception. Of course, we can still block aborting for indefinite time by ensuring the OrigHandler never completes. I think that is bearable, at least it never caused big troubles.

I plan to extend this mechanism by re-implementing thread_exit(+Term) using throw(exit(Term)) and allow halt/1 to be implemented as throw(halt(Status)). The latter is also used by Python, which implements sys.exit(code) as raise SystemExit(code). This is also the reason for considering implementing halt/1 this way as it allows clean unwinding of a mixed Python and Prolog stack for Janus.

So, I’m wondering whether other people are interested in this. It would be a very short pip, defining these reserved exception terms and agreeing on the call_cleanup/2 (or comparable mechanism) to make catch/3 bubble up these exceptions, even when caught. Roughly I see three options

  • Give these exceptions nice standard names, e.g., abort, halt(Code), exit(Term)
  • Wrap them as e.g. unwind(Term), for any exception of this shape bubbles up to the very top.
  • Give them $-reserved names. This is how it works for abort/0 now, but I’m a bit unhappy about that.
1 Like

Hi Jan,

If I understand it correctly, your proposal is making some reserved terms uncatchable by catch/3 so that the semantics of throw/1 can be safely be reused for aborting, etc.?

That looks interesting as an implementation mechanism. My only concern is it needs to be exposed to users. I’d go for wrapping the terms, use reserved names, or maybe implement a (internal?) sys_throw/1 predicate that allows throwing reserved terms (either wrapping, mapping to internal names, etc.). You could document and use nice names for these system exceptions without unexpected interactions with programs using arbitrary exception names.

Your first option (give standard names to these exceptions) is tempting but maybe too risky and hard to extend?

Life would be easier in a functor-based module system if we could qualify terms as in throw(sys:abort) (if anyone is interested, it can be another PIP).

Yes. “uncatchable” might not be he right term as you can catch them ok, but catch/3 will re-throw after the recovery completes.

I’m not sure this is a concern. Yes, it introduces an incompatibility (but I think without significant impact if we choose the right term(s)). The reason to want standard known terms is that it does allow the user to act and, for example, to do some specific cleanup if an abort happens or before halting (other than global on_halt handlers, although even these are not in the standard). So, I think there is a value in using public terms and only a very minor compatibility implication.

The alternative would be a completely separate unwinding mechanism. Besides a weird duplication, this would deny users the ability to interact with it, which is not what I want. If anything, I’d be more interested in a catch/3 version that would allow catching these the normal way :slight_smile:

The proposal would be defining special exceptions and the ability for catch/3 to automatically rethrow these.