I have an early prototype for something I wanted to achieve for a long time: be able to add wrappers around arbitrary predicates. The driving use-case is tabling. As is, tabled predicates can only be defined by loading a source file with a :- table ...
directive. The directive creates a wrapper predicate and the real predicate is renamed. This has some unpleasant consequences:
- As said, we cannot (in a nice and supported way) create tabled predicates on the fly.
- All the usual predicate inspection and modification affects the wrapper rather than the thing itself.
- We cannot switch between tabled and non-tabled execution for a predicate.
Some way to execute Prolog code before/after any predicate was already part of Quintus AFAIK. It provides a huge opportunity for debugging tools, ranging from logging, conditional spy points, checking the execution by verifying pre- and post conditions, determinism, etc.
What I now have is a predicate like this:
%! wrap_predicate(:Head, +Name, -Closure, +Body) is det.
%
% Wrap the predicate referenced by Head using Body. Subsequent calls
% to Head call the given Body term. Body may call the original
% definition using a term
%
% call(Closure, A1, ...)
%
% Name names the wrapper for inspection using predicate_property/2 or
% deletion using unwrap_predicate/2.
This allows us to write e.g.,
det(M:Name/Arity) :-
functor(Head, Name, Arity),
Head =.. [_|Args],
Wrapped =.. [call, Closure|Args],
wrap_predicate(M:Head, det, Closure,
( prolog_current_choice(Ch0),
Wrapped,
prolog_current_choice(Ch1)
-> ( Ch0 == Ch1
-> true
; throw(error(det_error(nondet, M:Head), _))
)
; throw(throw(error(det_error(failed, M:Head), _)))
)).
Which validates that a predicate is indeed deterministic (succeeds and leaves no choice point behind). We can use this at any time to start checking a predicate, even before it is defined.
Current properties:
- Wrappers may be added before or after defining the predicate
- Wrappers are named.
- A predicate can have multiple wrappers. The execution is simply nested, starting with the last added wrapper.
- Wrappers may be deleted by name or variable to delete the outermost (last added) wrapper
- Adding/deleting wrappers is thread-safe, which allows for e.g., temporary wrapping a predicate to track activity of another thread.
- The wrapper may, but need not to call the real implementation. That is hard to avoid with this design and can be considered a feature or a bug.
- All predicates can be wrapped, even built-ins. Only built-ins that are implemented as virtual machine instructions (
true
,fail
, unification, comparison, (optimized) arithmetic), etc.) cannot be wrapped.
Questions
- One of the things I’m struggling with is whether we should allow for multiple wrappers with the same name on a predicate. One of the disadvantages is that when using the above det/1 as a directive in a source file, every re-load will add another layer of wrappers If we deny that, we have some options, such as replacing the existing wrapper while maintaining order, rejecting or delete the old, adding the new as outermost.
- Should wrapping be the leading term to refer to this functionality?
- Any prior art from which to borrow terminology and functionality?
- Any use cases that need a different API?
Implementation
For those who want to know how it works, Creating a wrapper creates a predicate __wrap$<name>
with the same arity as the predicate being wrapped. The Body is used to create a clause for this predicate. Any SWI-Prolog predicate is executed by starting its supervisor: a (short) sequence of VM instructions that depends on the implementation (C, dynamic, thread-local, etc) and indexing of the predicate. Wrapping creates a closure blob that allows us executing a predicate with a specific supervisor instead of the default one. That is passed as Closure above. Wrapping replaces the supervisor of the wrapped predicate to call the wrapper clause in the __wrap$<name>
predicate. The overhead is pretty low