7.6.8 Accessing Past Debugger States

In this section we introduce the built-in predicates for accessing past debugger states, and the breakpoint conditions related to these.

The debugger collects control flow information about the goals being executed, more precisely about those goals, for which a procedure box is built. This collection of information, the backtrace, includes the invocations that were called but not exited yet, as well as those that exited nondeterminately. For each invocation, the main data items present in the backtrace are the following: the goal, the module, the invocation number, the depth and the source information, if any.

Furthermore, as you can enter a new break level from within the debugger, there can be multiple backtraces, one for each active break level.

You can access all the information collected by the debugger using the built-in predicate execution_state(Focus, Tests). Here Focus is a ground term specifying which break level and which invocation to access. It can be one of the following:

Note that the top-level counts as break level 0, while the invocations are numbered from 1 upwards.

The second argument of execution_state/2, Tests, is a simple or composite breakpoint condition. Most simple tests can appear inside Tests, with the exception of the port, bid, advice, debugger, and get tests. These tests will be interpreted in the context of the specified past debugger state. Specifically, if a true/1 condition is used, then any execution_state/1 queries appearing in it will be evaluated in the past context.

To illustrate the use of execution_state/2, we now define a predicate last_call_arg(ArgNo, Arg), which is to be called from within a break, and which will look at the last debugged goal of the previous break level, and return in Arg the ArgNoth argument of this goal.

     last_call_arg(ArgNo, Arg) :-
             execution_state(break_level(BL1)),
             BL is BL1-1,
             execution_state(break_level(BL), goal(Goal)),
             arg(ArgNo, Goal, Arg).

We see two occurrences of the term break_level(...) in the above example. Although these look very similar, they have different roles. The first one, in execution_state/1, is a breakpoint test, which unifies the current break level with its argument. Here it is used to obtain the current break level and store it in BL1. The second use of break_level(...), in the first argument of execution_state/2, is a focus condition, whose argument has to be instantiated, and which prescribes the break level to focus on. Here we use it to obtain the goal of the current invocation of the previous break level.

Note that the goal retrieved from the backtrace is always in its latest instantiation state. For example, it not possible to get hold of the goal instantiation at the Call port, if the invocation in question is at the Exit port.

Here is an example run, showing how last_call_arg/2 can be used:

             5      2 Call: _937 is 13+8 ? b
     % Break level 1
     % 1
     | ?- last_call_arg(2, A).
     A = 13+8

There are some further breakpoint tests that are primarily used in looking at past execution states.

The test max_inv(MaxInv) returns the maximal invocation number within the current (or selected) break level. The test exited(Boolean) unifies Boolean with true if the invocation has exited, and with false otherwise.

The following example predicate lists those goals in the backtrace, together with their invocation numbers, that have exited. These are the invocations that are listed by the t interactive debugger command (print backtrace), but not by the g command (print ancestor goals). Note that the predicate between(N,M,I) enumerates all integers such that N \leq I \leq M.

     exited_goals :-
          execution_state(max_inv(Max)),
          between(1, Max, Inv),
          execution_state(inv(Inv), [exited(true),goal(G)]),
          format('~t~d~6| ~p\n', [Inv,G]),
          fail.
     exited_goals.
     (...)
     
     ?*     41     11 Exit: foo(2,1) ? @
     | :- exited_goals.
         26 foo(3,2)
         28 bar(3,1,1)
         31 foo(2,1)
         33 bar(2,1,0)
         36 foo(1,1)
         37 foo(0,0)
         39 foo(1,1)
         41 foo(2,1)
         43 bar(2,1,0)
         46 foo(1,1)
         47 foo(0,0)
     ?*     41     11 Exit: foo(2,1) ?

Note that similar output can be obtained by entering a new break level and calling exited_goals from within an execution_state/2:

     % 1
     | ?- execution_state(break_level(0), true(exited_goals)).

The remaining two breakpoint tests allow you to find parent and ancestor invocations in the backtrace. The parent_inv(Inv) test unifies Inv with the invocation number of the youngest ancestor present in the backtrace, called debugger-parent for short. The test ancestor(AncGoal,Inv) looks for the youngest ancestor in the backtrace that is an instance of AncGoal. It then unifies the ancestor goal with AncGoal and its invocation number with Inv.

Assume you would like to stop at all invocations of foo/2 that are somewhere within bar/1, possibly deeply nested. The following two breakpoints achieve this effect:

     | ?- spy(bar/1, advice), spy(foo/2, ancestor(bar(_),_)).
     % Plain advice point for user:bar/1 added, BID=3
     % Conditional spypoint for user:foo/2 added, BID=4

We added an advice-point for bar/1 to ensure that all calls to it will have procedure boxes built, and so become part of the backtrace. Advice-points are a better choice than spypoints for this purpose, as with ?- spy(bar/1, -proceed) the debugger will not stop at the call port of bar/1 in trace mode. Note that it is perfectly all right to create an advice-point using spy/2, although this is a bit of terminological inconsistency.

Further examples of accessing past debugger states can be found in library(debugger_examples).