Did you know ... | Search Documentation: |
The exception term |
The predicate throw/1
takes a single argument, the exception term, and the ISO
standard stipulates that the exception term be of the form error(Formal,
Context)
with:
Thus, constructing an error term and throwing it might take this form (although you would not use the illustrative explicit naming given here; instead composing the exception term directly in a one-liner):
Exception = error(Formal, Context), Context = ... some local convention ..., Formal = type_error(ValidType, Culprit), % for "type error" for example ValidType = integer, % valid atoms are listed in the ISO standard Culprit = ... some value ..., throw(Exception)
Note that the ISO standard formal term expresses what should be the case or what is the expected correct state, and not what is the problem. For example:
instantiation_error
: The
problem is not that there is an unwanted instantiation, but that the
correct state is the one with an instantiated variable.
uninstantiation_error(Culprit)
: The problem is not that
there is lack of instantiation, but that the correct state is the one
which
Culprit (or one of its subterms) is more uninstantiated than
is the case.
type_error(compound,[])
. The problem is
not that []
is (erroneously) a compound term,
but that a compound term is expected and []
does not belong to that class.
User predicates are free to choose the structure of their exception terms (i.e., they can define their own conventions) but should adhere to the ISO standard if possible, in particular for libraries.
Notably, exceptions of the shape error(Formal,Context)
are recognised by the development tools and therefore expressing
unexpected situations using these exceptions improves the debugging
experience.
In SWI-Prolog, the second argument of the exception term, i.e., the
Context argument, is generally of the form
context(Location, Message)
, where:
library(prolog_stack)
library. This library recognises
uncaught errors or errors caught by catch_with_backtrace/3
and fills the
Location argument with a backtrace.
ISO standard exceptions can be thrown via the predicates exported
from library(error)
. Termwise, these predicates look
exactly like the
Formal of the ISO standard error term they throw:
The ISO standard ISO/IEC 13211-1 is VERY paywalled (I won't say anything more about the reasonableness/ethics/sustainability of this), so:
I'm trying to keep this opinionated page updated with info on Prolog exceptions.
How do we get a backtrace and how does the exception term have to look to get one?
The backtrace is filled in according to SWI-Prolog conventions because the ISO Standard has nothing to say about this.
SWI-Prolog wants the second argument of the error/2 term (given in the ISO standard as Imp_def) to look like
context(Location,Message)
. If Location is fresh and the catch is performed by catch_with_backtrace/3
(which happens either explicity in code or at the latest possible time at the Prolog Toplevel),
then Location is filled with a backtrace (as implemented by library(prolog_stack)
in
file swipl/lib/swipl/library/prolog_stack.pl
). The Message is generally a cleartext message (string or atom).
Take this program:
call0(ExceptionTerm) :- call1(ExceptionTerm). call1(ExceptionTerm) :- call2(ExceptionTerm). call2(ExceptionTerm) :- throw(ExceptionTerm).
Enable debugging to keep the compiler from optimizing-away stack frames.
?- debug.
Let's study the behaviour of "backtrace generation" by catch_with_backtrace/3 with various forms of ExceptionTerm. We will let the exception be caught at the Prolog Toplevel, which uses that predicate.
Non-ISO-Standard exception term without placeholder
No backtrace is generated, there is minimal printing at toplevel:
?- call0("deep in a search tree"). ERROR: Unhandled exception: "deep in a search tree"
Quasi-ISO-standard exception term error(_,_)
An exception term that looks like error(_,_)
matches the ISO Standard basic format, although the requirements
regarding the formal term on the first position have to be followed too for full compliance.
The second argument is set to context(B,_)
where B contains a backtrace.
The toplevel tries validly to print something in the first line, but has to admit that it found an `Unknown error term`:
[debug] ?- call0(error("deep in a search tree",Context)). ERROR: Unknown error term: "deep in a search tree" ERROR: In: ERROR: [13] throw(error("deep in a search tree",_4126)) ERROR: [12] call2(error("deep in a search tree",_4156)) at user://1:14 ERROR: [11] call1(error("deep in a search tree",_4186)) at user://1:11 ERROR: [10] call0(error("deep in a search tree",_4216)) at user://1:8 ERROR: [9] <user> Exception: (13) throw(error("deep in a search tree", _3266)) ? Exception details
Asking for "exception details" using m
reveals that Context has been filled in with a
term context(prolog_stack(Frames),_)
as the exception term looks as follows:
error("deep in a search tree", context( prolog_stack([ frame(13,call(system:throw/1),throw(error("deep in a search tree",_4126))), frame(12,clause(<clause>(0x1a7ef30),4),call2(error("deep in a search tree",_4156))), frame(11,clause(<clause>(0x1a9ccd0),4),call1(error("deep in a search tree",_4186))), frame(10,clause(<clause>(0x1a368c0),4),call0(error("deep in a search tree",_4216))), frame(9,clause(<clause>(0x18f7450),3),'$toplevel':toplevel_call(user:user: ...))]),_4082))
Quasi-ISO-standard exception term with SWI-Prolog context term error(_,context(_,_))
The same as above, we just have SWI-Prolog specific context/2 subterm already in the ISO-standard specific error/2 term.
We can put a generic message in the second argument of context/2. In this example, a String:
[debug] ?- call0(error("deep in a search tree",context(_,"get me outta here"))). ERROR: Unknown error term: "deep in a search tree" (get me outta here)
ISO-standard exception term with SWI-Prolog context term error(IsoFormal,context(_,_))
As above, backtrace and all, except that now error message generation is correct as it is based on the list of valid ISO formal terms:
[debug] ?- call0(error(instantiation_error,context(_,"get me outta here"))). ERROR: Arguments are not sufficiently instantiated (get me outta here) ERROR: In: ERROR: [13] throw(error(instantiation_error,context(_3028,"get me outta here"))) ERROR: [12] call2(error(instantiation_error,context(_3064,"get me outta here"))) at user://1:14 ERROR: [11] call1(error(instantiation_error,context(_3100,"get me outta here"))) at user://1:11 ERROR: [10] call0(error(instantiation_error,context(_3136,"get me outta here"))) at user://1:8 ERROR: [9] <user>
Suppose you want to throw your own non-ISO exception terms (but still enclosed in error(Formal,Context)
) for example:
error(check(domain,Expected,Msg,Culprit),_)
By default, the toplevel won't know how to print this Formal
.
You have to inject your own clauses for prolog:error_message(ErrorTerm)
. For example:
:- multifile prolog:error_message//1. % 1-st argument of error term extended_msg(domain,"the culprit is outside the required domain"). prolog:error_message(check(Type,Expected,Msg,Culprit)) --> { build_main_text_pair(Type,MainTextPair) }, [ MainTextPair, nl ], lineify_expected(Expected), lineify_msg(Msg), lineify_culprit(Culprit). build_main_text_pair(Type,MainTextPair) :- extended_msg(Type,ExMsg), !, atomics_to_string([ExMsg],"",ExMsgStr), % make sure it's string MainTextPair = 'check failed : ~q error (~s)'-[Type,ExMsgStr]. build_main_text_pair(Type,MainTextPair) :- MainTextPair = 'check failed : ~q error'-[Type]. lineify_expected(Expected) --> { nonvar(Expected), atomics_to_string([Expected],"",ExpectedStr) }, !, [ ' expected : ~s'-[ExpectedStr], nl ]. lineify_expected(_) --> []. lineify_msg(Msg) --> { nonvar(Msg), atomics_to_string([Msg],"",MsgStr) }, !, [ ' message : ~s'-[MsgStr], nl ]. lineify_msg(_) --> []. lineify_culprit(Culprit) --> { nonvar(Culprit), atomics_to_string([Culprit],"",CulpritStr) }, !, [ ' culprit : ~s'-[CulpritStr], nl ]. lineify_culprit(_) --> [].