I’m working on high-level rust bindings to SWI-Prolog’s foreign language interface. The documentation for PL_unify warns that in some cases, PL_unify may actually fail with an exception, ‘typically’ due to a resource overflow. This exception is then retrievable with PL_exception(0).
How likely is such an exception? Is there some way for me to trigger it manually in a controlled test situation? And what other conditions are there that may cause unification to fail with an exception in foreign code?
Initially I thought that one way to trigger an exception was to use freeze/2 in the following manner:
freeze(X, throw(error(moo,_))
then unifying X with PL_unify_uint64(term_x, 42). However, this unification succeeds, and the freeze goal is delayed until the next prolog query runs, resulting in this exception becoming an ordinary query exception queryable with PL_exception(qid). This behavior seems quite confusing though, as the exception is not a result of the query, but an earlier unification. So my second question is, is there some way to know the true source of an exception like this?
As is, PL_unify() indeed does not evaluate constraints. Not sure whether or not it should. The main reason not to is that this would kill non-determinism of constraints. That leaves resource exceptions as the only reason for PL_unify() and many of the other foreign functions to fail. Why do you ask though? Is there a reason why handling these is (too) hard? A more or less `fair’ solution would be to halt the process, as is currently done by Prolog itself on most failing malloc() calls. As long as you do not expect that handling such exceptions should be part of your program, that is fine.
I’m not a rust person. The C++ interface maps such errors to exceptions and the C++ foreign language wrappers deal with these, passing the exception on to Prolog. Does that work with Rust?
If some low level operation returns FALSE you can use PL_exception(0) to test whether there is a pending exception or we are dealing -for example- with an ordinary failure of the unification. This is a poorly designed interface. We should have used 0 for success, 1 for logical failure and -N for exceptions. Wisdom comes at age
Thanks for your answer. The reason I was asking is to figure out if these sorts of error are something a reasonable user would want to handle or not. From your description, I gather that it really isn’t. So the strategy to follow when this happens seems to be to either die right there, or return to prolog as soon as possible in the case where we’re a foreign predicate. That works for me.
The reason why handling these errors is a little hard for me is that I’m trying to leverage rust’s lifetime feature to tag every term reference with a lifetime during which it is valid. This means that, for example, when you create a foreign frame, the rust library will ensure that any terms created within that frame are no longer referenced as soon as that frame is closed or discarded. With unification though, it seems tricky to know what the proper lifetime of a term returned by PL_exception(0) is. Will this term be useable if I subsequently pop a bunch of foreign frames for example? It’s much easier to just be able to ignore the whole ordeal.
I’m still interested in the answer though. How long can I expect the error term returned by PL_exception(0) to live? From a quick glance at the code, it seems to behave differently from errors coming out of queries. Where query errors have a new term created upon calling PL_exception(qid), PL_exception(0) just returns some pre-existing term ref. So where on the stack is this? And how will it behave when I discard frames?
Regarding the second part of my question, I wasn’t actually asking about exceptions generated by resource exceptions, but those by coroutining. if I get an exception on PL_next_solution(qid), this could apparently have two reasons:
The predicate I’m calling has thrown an error
A unification I did earlier, before even opening this query, is now causing some unification hook to run, and now that hook has thrown an error.
Is it possible to differentiate between these two cases?
I tend to agree. When writing a foreign predicate and you get one of these exceptions you two options. In 99.9% of the cases the correct option is to cleanup what needs to be cleaned and return FALSE to Prolog asap. In some cases you may call PL_clear_exception() and continue. If the main control is outside Prolog (you embedded Prolog) you should typically just die. In multi-threaded applications where the threads provide some service but are otherwise independent you can just make this thread die.
The exception term is in a term reference that is created as soon as the thread is created. This means this reference is not invalidated by popping frames. Moreover, the term is subject to duplicate_term/3 and stack freezing and this not subject to backtracking. This is necessary to keep the exception alive while we unwind the stack as part of the exception handling. For short, they live until PL_clear_exception() is called or the exception is handled (or the thread dies).
I don’t see how. Wakeup calls are recorded in a queue and from there injected into the normal execution. This happens after the head unification and after a foreign predicate returns (both only on success). Note that exceptions may also be the result of thread_signal/2 that can be executed more or less any time.
Very interesting! So you can actually never really be sure what to expect when opening a prolog query from foreign. Even calling true/0 may suddenly result in a nondeterministic exception-throwing beast. That’s certainly something to keep in mind while developing this high-level binding.
I think I know enough for now. Many thanks for your explanation!
Its not that bad Thread signal handlers are called as ignore/1. As ignore/1, exceptions are propagated though, so thread_signal(Thread, throw(stop)) creates an exception stop in the target thread that can more or less happen anytime.