We will show two examples using the advanced features of the debugger.
The first example defines a hide_exit(Pred)
predicate,
which will hide the Exit port for Pred
(i.e. it will
silently proceed), provided the current goal was already
ground at the Call port, and nothing was traced inside the
given invocation. The hide_exit(Pred)
goal creates two
spypoints for predicate Pred
:
:- meta_predicate hide_exit(:). hide_exit(Pred) :- add_breakpoint([pred(Pred),call]- true(save_groundness), _), add_breakpoint([pred(Pred),exit,true(hide_exit)]-hide, _).
The first spypoint is applicable at the Call port, and it
calls save_groundness
to check if the given invocation was
ground, and if so, it stores a term hide_exit(ground)
in the goal_private
attribute of the invocation.
save_groundness :- execution_state([goal(_:G),goal_private(Priv)]), ground(G), !, memberchk(hide_exit(ground), Priv). save_groundness.
The second spypoint created by hide_exit/1
is applicable at
the Exit port and it checks whether the hide_exit/0
condition is true. If so, it issues a hide
action, which is a
breakpoint macro expanding to [silent,proceed]
.
hide_exit :- execution_state([inv(I),max_inv(I),goal_private(Priv)]), memberchk(hide_exit(Ground), Priv), Ground == ground.
Here, hide_exit
encapsulates the tests that the invocation number
be the same as the last invocation number used (max_inv
), and
that the goal_private
attribute of the invocation be identical to
ground
. The first test ensures that nothing was traced inside the
current invocation.
If we load the above code, as well as the small example below,
the following interaction, discussed below, can take place. Note that the
hide_exit
predicate is called with the _:_
argument, resulting in generic spypoints being created.
| ?- [user]. | cnt(0) :- !. | cnt(N) :- N > 0, N1 is N-1, cnt(N1). | ^D % consulted user in module user, 0 msec 424 bytes | ?- hide_exit(_:_), trace, cnt(1). % The debugger will first zip -- showing spypoints (zip) % Generic spypoint added, BID=1 % Generic spypoint added, BID=2 % The debugger will first creep -- showing everything (trace) # 1 1 Call: cnt(1) ? c # 2 2 Call: 1>0 ? c # 3 2 Call: _2019 is 1-1 ? c 3 2 Exit: 0 is 1-1 ? c # 4 2 Call: cnt(0) ? c 1 1 Exit: cnt(1) ? c % trace | ?-
Invocation 1 is ground, its Exit port is not hidden, because further goals were traced inside it. On the other hand, Exit ports of ground invocations 2 and 4 are hidden.
Our second example defines a predicate call_backtrace(Goal,
BTrace)
, which will execute Goal
and build a backtrace
showing the successful invocations executed during the solution of
Goal
.
The advantages of such a special backtrace over the one incorporated in the debugger are the following:
The call_backtrace/2
predicate is based on the advice
facility. It uses the variable accessible via the
private(_)
condition to store a mutable (see ref-lte-mut) holding the
backtrace. Outside the call_backtrace
predicate the mutable will have the value off
.
The example is a module-file, so that internal invocations can be
identified by the module name. We load the lists
library, because memberchk/2
will be used in the handling of the
private field.
:- module(backtrace, [call_backtrace/2]). :- use_module(library(lists)). :- meta_predicate call_backtrace(:, ?). call_backtrace(Goal, BTrace) :- Spec = [advice,call] -[true((goal(M:G),store_goal(M,G))),flit], ( current_breakpoint(Spec, _, on, _, _) -> B = [] ; add_breakpoint(Spec, B) ), call_cleanup(call_backtrace1(Goal, BTrace), remove_breakpoints(B)).
call_backtrace(Goal, BTrace)
is a meta-predicate, which
first sets up an appropriate advice-point for building the
backtrace. The advice-point will be activated at each Call
port and will call the store_goal/2
predicate with
arguments containing the module and the goal in
question. Note that the advice-point will not build a
procedure box (cf. the flit
command in the action part).
The advice-point will be added just once: any further (recursive)
calls to call_backtrace/2
will notice the existence of the
breakpoint and will skip the add_breakpoint/2
call.
Having ensured the appropriate advice-point exists,
call_backtrace/2
calls call_backtrace1/2
with a cleanup
operation that removes the breakpoint added, if any.
:- meta_predicate call_backtrace1(:, ?). call_backtrace1(Goal, BTrace) :- execution_state(private(Priv)), memberchk(backtrace_mutable(Mut), Priv), ( is_mutable(Mut) -> get_mutable(Old, Mut), update_mutable([], Mut) ; create_mutable([], Mut), Old = off ), call(Goal), get_mutable(BTrace, Mut), update_mutable(Old, Mut).
The predicate call_backtrace1/2
retrieves the private field
of the execution state and uses it to store a mutable, wrapped in
backtrace_mutable
. When first called within a top-level the
mutable is created with the value []
. In later calls the
mutable is re-initialized to []
. Having set up the
mutable, Goal
is called. In the course of the execution of
the Goal
the debugger will accumulate the backtrace in the
mutable. Finally, the mutable is read, its value is returned
in BTrace
, and it is restored to its old value (or off
).
store_goal(M, G) :- M \== backtrace, G \= call(_), execution_state(private(Priv)), memberchk(backtrace_mutable(Mut), Priv), is_mutable(Mut), get_mutable(BTrace, Mut), BTrace \== off, !, update_mutable([M:G|BTrace], Mut). store_goal(_, _).
store_goal/2
is the predicate called by the
advice-point, with the module and the goal as
arguments. We first ensure that calls from within the
backtrace
module and those of call/1
get
ignored. Next, the module qualified goal term is
prepended to the mutable value retrieved from the private field,
provided the mutable exists and its value is not off
.
Below is an example run, using a small program:
| ?- [user]. | cnt(N):- N =< 0, !. | cnt(N) :- N > 0, N1 is N-1, cnt(N1). | ^D % consulted user in module user, 0 msec 424 bytes | ?- call_backtrace(cnt(1), B). % Generic advice point added, BID=1 % Generic advice point, BID=1, removed (last) B = [user:(0=<0),user:cnt(0),user:(0 is 1-1),user:(1>0),user:cnt(1)] | ?-
Note that the backtrace produced by call_backtrace/2
can
not contain any information regarding failed branches. For example, the
very first invocation within the above execution, 1 =< 0
, is
first put on the backtrace at its Call port, but this is
immediately undone because the goal fails. If you would like to
build a backtrace that preserves failed branches, you have to use
side-effects, e.g. dynamic predicates.
Further examples of complex breakpoint handling are contained in
library(debugger_examples)
.
This concludes the tutorial introduction of the advanced debugger features.