The Ciao program development environment includes a number of advanced debugging tools, such as the
ciaopp
preprocessor and some execution visualizers. Herein we discuss the interactive debugger available in the standard top-level, which allows to trace the control flow of programs, similar to other popular Prolog systems. This is a classical Byrd 'box-type'
debugger [Byr80,BBP81], with some enhancements.
Byrd's Procedure Box model of debugging execution provides a simple way of visualising control flow, especially during backtracking. Control flow is viewed at the predicate level, rather than at the level of individual clauses. The Ciao debugger has the ability to mark selected modules and/or files for debugging, rather than having to exhaustively trace your program. It also allows to selectively set spy-points. Spy-points allow the programmer to nominate interesting predicates at which program execution is to pause so that the programmer can interact with the debugger. There is a wide choice of control and information options available during debugging interaction.
Usually, when a program is not working properly, the programmer has a feeling of which are the modules where the fault may be. Since full-fledged debugging is only available on consulted (interpreted) modules, which are executed much slower than compiled modules, there is the posibility of telling the top level which particular modules are wanted to be loaded in
consult mode, with the aim of debugging them. The most simple way of achieving this (appart from using the Ciao
emacs
mode, which includes special support for debugging) is executing in the Ciao shell prompt, for each suspicious module Module
in the program, a command like this:
?- debug_module(Module).
Note that files without module declaration belong to the module user, so the command to be issued for debugging a user file, say 'foo.pl', would be debug_module(user)
, and not debug_module(foo)
.
The above command performs in fact two related things: on the one hand, it instructs the compiler to load any file containing a module of this name in consult mode; on the other, it instructs the debugger to actually prepare for debugging the code belonging to that module. After that, the modules which are to be debugged have to be (re)loaded in any appropriate form. The nice thing is that, if the modules are part of a bigger application, a command to load the main application file will automatically force loading the dependent modules which have changed, including those whose loading mode has changed, thus allowing the debugging of required files.
Later, as the bug location is isolated, one would want to restrict more and more the modules where debugging takes place. To this end, and without the need of reloading, one can tell the debugger to not consider a module for debugging issuing a
nodebug_module/1
command, which counteracts a
debug_module/1
command with the same module name.
There exist also the top-level commands
set_debug_mode/1
and
set_nodebug_mode/1
, which accept as argument a file spec (i.e., library(foo)
or foo
, even if it is a user file) to be able to load a file in consult mode without changing the set of modules the debugger will try to spy.
Once modules or user files are marked for debugging and reloaded, the traditional debugging shell commands can be used (the
debugger
library following this section contains all the commands and their description), with the same meaning as in other Prolog systems such as
SICStus. The difference in their behavior is that in Ciao debugging takes place only in the modules in which it was activated, and that
nospy/1
and
spy/1
accept sequences of predicate specs, and they will search for those predicates only in the debugging-marked modules.
During debugging the interpreter prints out a sequence of goals in various states of instantiation in order to show the state the program has reached in its execution. However, in order to understand what is occurring it is necessary to understand when and why the interpreter prints out goals. As in other programming languages, key points of interest are procedure entry and return, but in Prolog there is the additional complexity of backtracking. One of the major confusions that novice Prolog programmers have to face is the question of what actually happens when a goal fails and the system suddenly starts backtracking. The Procedure Box model of Prolog execution views program control flow in terms of movement about the program text. This model provides a basis for the debugging mechanism in the interpreter, and enables the user to view the behaviour of the program in a consistent way.
Let us look at an example Prolog procedure:
The first clause states that Y
is a descendant of X
if Y
is an offspring of X
, and the second clause states that Z
is a descendant of X
if Y
is an offspring of X
and if Z
is a descendant of Y
. In the diagram a box has been drawn around the whole procedure and labelled arrows indicate the control flow in and out of this box. There are four such arrows which we shall look at in turn.
descendant(X,Y)
is required to be satisfied, control passes through the Call port of the descendant box with the intention of matching a component clause and then satisfying any subgoals in the body of that clause. Note that this is independent of whether such a match is possible; i.e. first the box is called, and then the attempt to match takes place. Textually we can imagine moving to the code for descendant when meeting a call to descendant in some other part of the code.
In terms of this model, the information we get about the procedure box is only the control flow through these four ports. This means that at this level we are not concerned with which clause matches, and how any subgoals are satisfied, but rather we only wish to know the initial goal and the final outcome. However, it can be seen that whenever we are trying to satisfy subgoals, what we are actually doing is passing through the ports of their respective boxes. If we were to follow this, then we would have complete information about the control flow inside the procedure box.
Note that the box we have drawn around the procedure should really be seen as an invocation box. That is, there will be a different box for each different invocation of the procedure. Obviously, with something like a recursive procedure, there will be many different Calls and Exits in the control flow, but these will be for different invocations. Since this might get confusing each invocation box is given a unique integer identifier.
This section explains the exact format of the message output by the debugger at a port. All trace messages are output to the terminal regardless of where the current output stream is directed (which allows tracing programs while they are performing file IO). The basic format is as follows:
S 13 7 Call: T user:descendant(dani,_123) ?
S
is a spy-point indicator. It is printed as '+
', indicating that there is a spy-point on descendant/2
in module user
, or as ' ', denoting no spy-point.
T
is a subterm trace. This is used in conjunction with the ^
command (set subterm), described below. If a subterm has been selected, T
is printed as the sequence of commands used to select the subterm. Normally, however, T
is printed as ' ', indicating that no subterm has been selected.
The first number is the unique invocation identifier. It is always nondecreasing (provided that the debugger is switched on) regardless of whether or not the invocations are being actually seen. This number can be used to cross correlate the trace messages for the various ports, since it is unique for every invocation. It will also give an indication of the number of procedure calls made since the start of the execution. The invocation counter starts again for every fresh execution of a command, and it is also reset when retries (see later) are performed.
The number following this is the current depth; i.e., the number of direct ancestors this goal has. The next word specifies the particular port (Call
, Exit
, Redo
or Fail
). The goal is then printed so that its current instantiation state can be inspected. The final ?
is the prompt indicating that the debugger is waiting for user interaction. One of the option codes allowed (see below) can be input at this point.
Ports can be unleashed. This means that the debugger will not stop for interaction at the messages for an unleashed port. Obviously, the ?
prompt will not be shown in such messages, since the user has specified to not wish to interact at this point.
Note that not all procedure calls are traced; there are a few basic predicates which have been made invisible since it is more convenient not to trace them. These include debugging directives, basic control structures, and some builtins. This means that messages will never be printed concerning these predicates during debugging.
This section describes the particular options that are available when the debugger prompts after printing out a debugging message. All the options are one letter mnemonics, some of which can be optionally followed by a decimal integer. They are read from the terminal with any blanks being completely ignored up to the next terminator (carriage-return, line-feed, or escape). Some options only actually require the terminator; e.g., the creep option, only requires RET.
The only option which do really needs to be remembered is 'h
' (followed by RET). This provides help in the form of the following list of available options.
<cr> creep c creep l leap s skip r retry r <i> retry i f fail f <i> fail i d display p print w write g ancestors g <n> ancestors n n nodebug = debugging + spy this - nospy this a abort b break @ command u unify < reset printdepth < <n> set printdepth ^ reset subterm ^ <n> set subterm ? help h help
c
creep causes the debugger to single-step to the very next port and print a message. Then if the port is leashed the user is prompted for further interaction. Otherwise it continues creeping. If leashing is off, creep is the same as leap (see below) except that a complete trace is printed on the terminal.
l
leap causes the interpreter to resume running the program, only stopping when a spy-point is reached (or when the program terminates). Leaping can thus be used to follow the execution at a higher level than exhaustive tracing. All that is needed to do is to set spy-points on an evenly spread set of pertinent predicates, and then follow the control flow through these by leaping from one to the other.
s
skip is only valid for Call and Redo ports. It skips over the entire execution of the predicate. That is, no message will be seen until control comes back to this predicate (at either the Exit port or the Fail port). Skip is particularly useful while creeping since it guarantees that control will be returned after the (possibly complex) execution within the box. With skip then no message at all will appear until control returns. This includes calls to predicates with spy-points set; they will be masked out during the skip. There is a way of overriding this: the t
option after a ^C interrupt will disable the masking. Normally, however, this masking is just what is required!
r
retry can be used at any of the four ports (although at the Call port it has no effect). It transfers control back to the Call port of the box. This allows restarting an invocation when, for example, it has left the programmer with some weird result. The state of execution is exactly the same as in the original call, (unless the invocation has performed side effects, which will not be undone). When a retry is performed the invocation counter is reset so that counting will continue from the current invocation number regardless of what happened before the retry. This is in accord with the fact that execution has, in operational terms, returned to the state before anything else was called.
If an integer is supplied after the retry command, then this is taken as specifying an invocation number and the system tries to get to the Call port, not of the current box, but of the invocation box specified. It does this by continuously failing until it reaches the right place. Unfortunately this process cannot be guaranteed: it may be the case that the invocation then programmer is looking for has been cut out of the search space by cuts in the program. In this case the system fails to the latest surviving Call port before the correct one.
f
fail can be used at any of the four ports (although at the Fail port it has no effect). It transfers control to the Fail port of the box, forcing the invocation to fail prematurely. If an integer is supplied after the command, then this is taken as specifying an invocation number and the system tries to get to the Fail port of the invocation box specified. It does this by continuously failing until it reaches the right place. Unfortunately, as before, this process cannot be guaranteed.
d
displays the current goal using display/1
. See write (below).
p
re-prints the current goal using print/1
. Nested structures will be printed to the specified printdepth (below).
w
writes the current goal on the terminal using write/1
.
g
provides a list of ancestors to the current goal, i.e., all goals that are hierarchically above the current goal in the calling sequence. It is always possible to jump to any goal in the ancestor list (by using retry, etc.). If an integer n
is supplied, then only n
ancestors will be printed. That is to say, the last n
ancestors will be printed counting back from the current goal. Each entry in the list is preceded by the invocation number followed by the depth number (as would be given in a trace message).
n
switches the debugger off. Note that this is the correct way to switch debugging off at a trace point. The @
or b
options cannot be used because they always return to the debugger.
=
outputs information concerning the status of the current debugging session.
+
spy sets a spy-point on the current goal.
-
nospy removes the spy-point from the current goal.
a
abort causes an abort of the current execution. All the execution states built so far are destroyed and the system is put right back at the top-level of the interpreter. (This is the same as the built-in predicate abort/0
.)
b
break introduces an interpreter break-point: current execution flow is suspended, and a new interpreter top-level started 'on top' of the previous execution, which is 'sitting underneath' it. When the break-point is ended (with ^D) previous execution is resumed, and the user will be reprompted at the port at which execution broke. The new execution is completely separate from the suspended one; the invocation numbers will start again from 1 during the break. The debugger is temporarily switched off as the break-point is called and will be re-switched on when the break is finished. However, any changes to the leashing or to spy-points will remain in effect. (This option might not be temporarily available.)
@
command allows calling arbitrary goals. It is effectively a one-off break (see above). The initial message | :-
will be output on the terminal, and a command is then read from the terminal and executed as if it was at top-level.
u
unify is available at the Call port and gives the option of providing a solution to the goal from the terminal rather than executing the goal. This is convenient, e.g., for providing a "stub" for a predicate that has not yet been written. A prompt |:
will be output on the terminal, and the solution is then read from the terminal and unified with the goal. (This option might not be temporarily available.)
<
printdepth sets a limit for the subterm nesting level that is printed in messages. While in the debugger, a printdepth is in effect for limiting the subterm nesting level when printing the current goal. When displaying or writing the current goal, all nesting levels are shown. The limit is initially 10. This command, without arguments, resets the limit to 10. With an argument of n
the limit is set to n
.
^
subterm sets the subterm to be printed in messages. While at a particular port, a current subterm of the current goal is maintained. It is the current subterm which is displayed, printed, or written when prompting for a debugger command. Used in combination with the printdepth, this provides a means for navigating in the current goal for focusing on the part which is of interest. The current subterm is set to the current goal when arriving at a new port. This command, without arguments, resets the current subterm to the current goal. With an argument of n
(greater than 0 and less or equal to the number of subterms of the current subterm), the current subterm is replaced by its n
'th subterm. With an argument of 0
, the current subterm is replaced by its parent term.
?
or h
displays the table of options given above.
Go to the first, previous, next, last section, table of contents.