Manual: Debugging Basics


Quintus Prolog Manual


(PREV) (NEXT)

E-1: Debugging Basics

E-1-0: Introduction

This chapter explains the basic concepts and terminology used by the Quintus Prolog debugger. Following chapters describe the two debuggers available in the Quintus Prolog system: the X Windows-based source linked debugger and the standard debugger.

The main features of the debugging package are:

Debugging foreign code is discussed in section I-3-11.

E-1-1: The Procedure Box Control Flow Model

During debugging, the debugger stops at various points in the execution of a goal, allowing you see what arguments the goal is called with, what arguments it returns with, and whether it succeeds or fails. 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 begins backtracking. The procedure box model of Prolog execution views program control flow in terms of movement from clause to clause in the procedures of the program. This model provides a basis for the debugging mechanism and enables the user to view program behavior in a consistent way.

Let us look at a representative Prolog procedure:


   Call                                              Exit
            descendant(X, Y) :- offspring(X, Y).

            descendant(X, Z) :-
               offspring(X, Y), descendant(Y, Z).
   Fail                                              Redo

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; labeled arrows indicate the control flow into and out of this box. There are four such arrows which we shall describe in turn.

Call
This arrow represents an initial invocation of the procedure. When a goal of the form descendant(X,Y) must be satisfied, control passes through the Call port of the descendant/2 box with the intention of matching the head of a component clause and then satisfying any subgoals in the body of that clause. Notice that this is independent of whether such a match is possible; first the box is called, and then the attempt to match takes place. Textually we can imagine moving to the code for descendant/2 when meeting a call to descendant/2 in some other part of the code.
Exit
This arrow represents a successful return from the procedure. This occurs when the initial goal has been unified with the head of one of the component clauses and all subgoals have been satisfied. Control now passes out of the Exit port of the descendant/2 box. Textually we stop following the code for descendant/2 and go back to the code we came from.
Redo
This arrow indicates that a subsequent goal has failed and that the system is backtracking in an attempt to find alternatives to previous solutions. Control passes into the descendant/2 box through the Redo port. An attempt will now be made to resatisfy one of the component subgoals in the body of the clause that last succeeded; or, if that fails, to completely rematch the original goal with an alternative clause and then try to satisfy any subgoals in the body of this new clause. Textually we follow the code backwards up the way we came looking for new ways of succeeding, possibly dropping down onto another clause and following that if necessary.
Fail
This arrow represents a failure of the initial goal, which might occur if no clause is matched, or if subgoals are never satisfied, or if all solutions produced are always rejected by later processing. Control now passes out of the Fail port of the descendant/2 box and the system continues to backtrack. Textually we move back to the code which called this procedure and keep moving backwards up the code looking for new ways of succeeding.

Note that the box we have drawn around the procedure should really be seen as an invocation box, rather than a procedure box. Often, there are many different Calls and Exits to a particular procedure in the control flow, but these are for different invocations. Each invocation box is given a unique integer identifier to prevent confusion.

E-1-2: Understanding Prolog Execution Using The Debugger

The Quintus Prolog debugger extends the procedure box control flow model to add extra information about the details of the execution of a goal, allowing you to better understand how your code behaves, and its efficiency.

Prolog incorporates a backtracking mechanism as a basic feature, which allows Prolog programs to efficiently search for multiple solutions to a problem. A goal is determinate if it has only one solution (or none). Often, this is what is desired. When a goal is not determinate, Prolog must keep around information to allow it to backtrack to look for alternate solutions. This extra information is called a choice point.

When a goal is intended to be nondeterminate, it may be expected to leave a choice point. However, when a goal that is expected to be determinate leaves a choice point, this may indicate an error in the program. In this case, the goal might succeed with the correct answer, but on backtracking produce wrong answers or not terminate. At the very least, an unnecessary choice point means that memory is being wasted. Quintus Prolog detects many kinds of determinate goals, and either does not allocate a choice point at all, or deallocates it as soon as possible, saving time and memory (see section B-5-1). Sometimes, however, you must help Prolog out by putting a cut (see section B-5-1-1) in your program, or by using an if-then-else (see section G-2-7) construct. The Quintus Prolog debugger helps you find such cases.

Quintus Prolog indexes on the first argument of a predicate. This means that if the first arguments in the clauses of a predicate are not variables, and the first argument in a call to that predicate is non-variable, then Prolog will go directly to the clause that matches, without even considering those that don't. Note that in order for this indexing to be very efficient, it only looks at the principal functor of a compound term. This means that if the first argument of one clause is a(a) and the first argument of the next clause is a(b), indexing will not be able to distinguish these clauses, so both will need to be tried.

Actually, indexing is more complicated than this. Any clause whose head has a variable as first argument will match any call, so indexing cannot be applied to this clause. Therefore, a predicate can be divided into alternating groups of adjacent indexable and nonindexable clauses. When the first argument of a call is non-variable, Quintus Prolog will skip over any clauses that don't match that argument, within a group of adjacent indexable clauses. Quintus Prolog will then try every clause in a group of adjacent nonindexable clauses, and then again skip nonmatching clause in an indexable group, and so on.

Even more important than time saved by indexing is its effect on determinacy. In effect, indexing makes it possible to ignore clauses following the clause being tried, as well as clauses preceding it. If it is possible to ignore all the clauses following the clause being tried, then Prolog will not create a choice point, or if a choice point has already been allocated for the call, Prolog will de-allocate it. Careful use of indexing can save a great deal time and memory running your program.

The Quintus Prolog debugger helps you understand these efficiency concerns, and also Quintus Prolog's exception handling, by extending the box model with three extra ports. These ports are described below.

Done
This is just like the Exit port, but signifies a determinate exit. This will help you to find goals that are nondeterminate and shouldn't be.
Head
This port shows the clauses' heads that will be tried for unification. At the Head port, you will see which clause is about to be tried, and which clause will be tried next if this clause fails. Note that the Head port is shown after indexing is done, so it helps you understand how indexing is working for you. And since it shows what clause will be tried next, it also helps you understand how unexpected nondeterminism may be creeping into your program.
Exception
The Exception port signifies an exception being raised while executing a goal.

Here's a more complete picture of the invocation box, including the extended ports.




   Call                                              Exit
                     descendant(X, Y) :-
             Head             offspring(X, Y).
                                                     Done

   Fail              descendant(X, Z) :-
             Head            offspring(X, Y),
                              descendant(Y, Z).
Exception                                            Redo

E-1-3: Traveling Between Ports

The Quintus Prolog debugger provides a rich set of commands to move between ports (that is, execute your program in a controlled way).

E-1-3-1: Basic Traveling Commands

creep
Causes the debugger to single-step to the very next port.
skip
Causes the debugger to skip, or ignore, the internal details of a procedure's execution. The debugger starts showing goals again when execution returns to the invocation's Done, Exit, Fail, or Exception port. If the debugger is already at the invocation's Done, Exit, Fail, or Exception port, then skipping is meaningless, and the debugger will just creep. Note that skipping is very fast, running at nearly full compiled speed. One important point about skipping: if the goal you skip over is not determinate, and you later redo (backtrack) into the goal, you will not be able to see the redos into goals that were skipped over. This is because the debugger does not keep any information about the goals that have been skipped over, in order to achieve much greater speed. You will, however, be able to see any new calls that are executed in the process of trying to redo the goal.
nonstop
Turns off the debugger for the rest of the execution of the top-level goal. When the execution of this goal is completed, the debugger returns to its current mode. This option does not turn the debugger off; to turn the debugger off, you must type "nodebug." at the main Prolog prompt. Like skip, nonstop causes the debugger to run at nearly full compiled speed.

E-1-3-2: Spypoints

Spypoints allow you to indicate where to stop on a per-predicate or even per-goal basis. For example, you might find that you want to run until some particular predicate is called. In this case, you would set a spypoint on that predicate, using spy/1. Such spypoints are turned off with nospy/1.

It may be desirable to stop when you get to a particular call from one predicate to another. This can be done with the built-in predicate add_spypoint/1. These spypoints can be removed with remove_spypoint/1.

To examine spypoints, use current_spypoint/1. Spypoints are also included in the output of debugging/0. Finally, spypoints can be removed all at once with nospyall/0.

The debuggers also have commands for setting spypoints, which are easier to use than these predicates.

E-1-3-3: Traveling Commands Sensitive to Spypoints

The basic traveling commands listed above all ignore spypoints; the following commands take advantage of spypoints.

leap
Causes the debugger to resume running your program, stopping only when the next spypoint is reached, or when the program terminates. Leaping can be used to follow the program's execution at a higher level than exhaustive tracing through creeping. This is done by setting spypoints on a set of pertinent procedures or calls, then following the control flow through these by leaping from one to the next.
quasi-skip
Causes the debugger to resume running your program, stopping when the next spypoint is reached. It also ensures that the debugger will stop at the current invocation's Done, Exit, Fail, or Exception port, when one is reached. Thus quasi-skip is a combination of leaping and skipping. You may use it to travel to the next spypoint while also marking a place to stop when execution returns there.
zip
Is just like leap, except that the debugger does not keep any debugging information while looking for a spypoint, so it runs at nearly full compiled speed. It does mean that the debugger will be unable to show you the ancestors between the invocation you zipped from and the invocation you stopped at. Zipping gives up some information in exchange for greatly increased speed. This is not always desirable, but sometimes is very helpful. A good use for zipping might be to run through a time-consuming initial part of a computation that is known to work properly, and stop at the beginning of a part that has a bug. From that point, you might use leaping, creeping, and skipping to locate the bug.

E-1-3-4: Commands That Change The Flow Of Control

The debugger also has these commands that alter the flow of control of your program.

retry
This can be used at any of the seven ports (although at the Call port it has no effect). Control is transferred back to the Call port of the box. It allows you to restart an invocation when, for example, you find yourself leaving with some incorrect result. The state of execution is exactly the same as when you originally called the procedure, except that clauses that have been changed by the database modification predicates will not be changed back to their original state.
fail
This is similar to Retry except that it transfers control to the Fail port of the current box. It places your execution in a situation in which it is about to backtrack out of the current invocation, having failed the goal.

E-1-4: Debugger Concepts

E-1-4-1: Trace Mode, Debug Mode, And Zip Mode

The debugger has three modes of operation: trace mode, debug mode, and zip mode. While trace mode is in force, every time you type a goal at the top level, the debugger starts single-stepping (creeping) immediately. In contrast, while debug mode is in force, the debugger does nothing until a call is made to a procedure or goal on which you have placed a spypoint. That is, it starts by leaping. Similarly, when in zip mode, the debugger begins by zipping.

The debugger mode (trace, debug, or zip) determines the first procedure call which will be traced after a goal is typed at top level. There is no other difference among the three modes. Whenever the debugger prompts you for input, you have a number of options, including those of creeping (single-stepping) leaping (jumping to the next spypoint), and zipping (jumping to the next spypoint without keeping debugging information).

The debugger mode can be set by using

E-1-4-2: Leashing

The purpose of leashing is to allow you to speed up single-stepping (creeping) through a program by telling the debugger that it does not always need to wait for user interaction at every port.

The leashing mode only applies to procedures that do not have spypoints on them, and it determines which ports of such procedures are leashed. By default, all ports are leashed. On arrival at a leashed port, the debugger will stop to allow you to look at the execution state and decide what to do next. At unleashed ports, the goal is displayed but program execution does not stop to allow user interaction.

At any time, there is a leashing mode in force which determines at which of the seven ports of a procedure box (Call, Exit, Redo, Fail, Done, Head, and Exception) the debugger will stop and wait for a command. By default, the debugger will stop at every port, but sometimes you may wish to reduce the number of times you have to issue commands. For example, it is often convenient only to have to interact at the Call, Redo, and Exception ports.

To set the leashing mode, that is, to specify ports for leashing, call leash/1. The leashing mode can also be set from the options menu of the source linked debugger (section E-2).

NOTE: Spypoints are not affected by leashing; the debugger will always stop at every port for a procedure or call on which there is a spypoint.

E-1-4-3: Locked Predicates

To allow users to produce code that cannot be debugged by others (for security reasons), Quintus Prolog 3.0 supports the concept of a locked predicate. A locked predicate will be treated by the debugger as opaque: users will not be able to creep into it. The debugger will behave in much the same way for locked predicates as it does for built-ins.

Predicates may be locked by specifying the -h switch to qpc when compiling a file. See the documentation for qpc, section H-1-2.

E-1-4-4: Unknown Procedures

The built-in predicate prolog_flag/3 or unknown/2 can be used to determine or set Prolog's behavior when it comes across an undefined predicate. By default, unknown procedures raise an exception.

Procedures which are known to be dynamic fail when there are no clauses for them.

When an undefined predicate is called and the undefined predicate behavior is set to 'error' rather than 'fail', unknown_predicate_handler/3 is called in module 'user'. By defining this predicate, you can (selectively) trap calls to undefined predicates in a program.

E-1-4-5: Current Debugging State

Information about the current debugging state includes the following:

This information can be displayed by calling debugging/0.

E-1-5: Summary of Predicates

add_spypoint/1 notrace/0 current_spypoint/1 remove_spypoint/1 debug/0 spy/1 debugging/0 trace/0 leash/1 unknown/2 nodebug/0 unknown_predicate_handler/3 nospy/1 nospyall/0


Copyright (C) 1998 SICS
contact: product support sales information