5.6.11 Programming Breakpoints

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.

     | ?- consult(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(0, ?).
     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(0, ?).
     call_backtrace1(Goal, BTrace) :-
             execution_state(private(Priv)),
             memberchk(backtrace_mutable(Mut), Priv),
             (   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),
             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:

     | ?- consult(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.


Send feedback on this subject.