ToC DocOverview CGDoc RelNotes FAQ Index PermutedIndex
Allegro CL version 11.0

Debugging


1.0 Introduction

Allegro CL provides a number of facilities for debugging and correcting Lisp code. Among these facilities are a set of commands to view and manipulate the runtime stack, a tracer, two steppers and an inspector. All are documented in this document except the inspector, which is documented in inspector.html.

Debugger functionality is included by default in development images (those built with either the include-debugger or include-devel-env arguments to build-lisp-image specified true). If you are building a runtime image and want debugging capability, be sure to specify include-debugger true (you cannot specify include-devel-env true when building a runtime image). See build-lisp-image and building-images.html for information on arguments to build-lisp-image. Note that the stepper is not permitted in runtime images.

A feature in Allegro CL is the ability to debug background processes in separate Lisp listeners. See use-background-streams and Debugging background processes for more information. If you are unable to use the facility described there, note the following. When multiprocessing is enabled, the user must be careful to ensure that he or she is debugging the correct process. The user types to the listener process, and debugs the focused process. Initially, they are the same process. The focus is changed only by user action. But if the focus is changed, the processes will not be the same, and it is important that the user keep track of which process is being debugged. Where relevant, the description of a debugging command specifies how it is used with multiprocessing. Users who are not using the multiprocessing facility need not concern themselves with these distinctions.

Note too that debugging compiled code is affected by what optimizations were in force when the code was compiled. These optimizations are discussed in compiling.html. Suffice it to say here that functions compiled for speed over safety may be harder to debug for the following reasons.

  1. Functions may be compiled inline, so it may be hard to figure out where you are, comparing what you expect from looking at the source to what you see on the stack.

  2. The real error may have gone unnoticed and the reported error may occur much later, perhaps in a different function. Thus you may notice unexpected arguments to a function (resulting in an error) but it may be difficult to discover where these arguments came from.

  3. The error messages themselves may be uninformative. This is because compilation by its nature removes information as it distills the code down to the machine level. Such distillation may not leave enough context to know what functionality was being attempted or where it was being attempted in the original source code.

One more point: difficulties in debugging code that was compiled for speed higher than safety can often be mitigated by recompiling the code with some combination of higher safety, higher debug, and lower speed. Allegro CL responds to the (debug 3) optimization quality by retaining as much information as possible. So rather than information essential to the debug process being thrown away, it is instead gathered as extra info that can annotate the now bare-bones machine code, and that info can be added back into the lisp as needed for particular debugging needs.

The debugger in an application

You can include much of the debugger (except the stepper) in a runtime application (see runtime.html for details). Further, the document debugger-api.html provides information about the internals of the debugger and using it, it is possible to provide a customized debugging interface. Note that debugger-api.html does not describe things needed in ordinary use of Allegro CL.


2.0 Getting out of the debugger

Debugger commands are available all the time, so you are never really in or out of the debugger. When an error occurs, you are put into a break loop (as indicated by a number in brackets in the prompt). See top-level.html for complete details, but briefly, the top-level command :pop will pop up one break level and the top-level command :reset will return you to the top-level, out of all break levels, as the following example shows:

USER(1): (car 1)
Error: Attempt to take the car of 1 which is not listp.
   [condition type: SIMPLE-ERROR]
[1] USER(2): :reset
USER(3):

3.0 Internal functions

Users trying to debug code will often have occasion to look at the stack for a list of recent function calls. Included in the list will be functions with names like: operator and operator_3op where operator is either a numeric function such as *, +, <, etc, or another function with optional arguments like read-char, etc. These functions are used in place of the expected functions (*, +, <, read-char, etc.) for compiler efficiency. They should be interpreted as the functions named by operator. Thus, for example, <_2op should be interpreted < (i.e. the less-than predicate).


4.0 Debugging background processes

The background-streams facilities described in this section only work when Allegro CL has been started with the fi:common-lisp command to Emacs. The Emacs-Lisp interface is fully described in eli.html. If you are not running under Emacs (or if the backdoor connection between Lisp and Emacs has not been established), you will not get the behavior described here. Note that it is not an error to use background streams when not running Lisp under Emacs. The described effect (creating a new listener in another Emacs buffer) will not happen. But if you later establish the connection, background streams will be used. The function use-background-streams is called automatically when Lisp starts up so it is not necessary to call it explicitly. However, you should call it if you do not get the expected behavior.

If an error occurs in a background process while Allegro Composer is not running, another listener buffer is created at once and output from the error is printed in the new listener buffer. To test the effect of background streams, evaluate the following forms.

(fmakunbound 'foo)
  (mp:process-run-function "bad process" 'foo)

These forms start a background process that will break and cause a new listener to be created. A message will be printed to the new listener saying that a background process has broken due to foo being undefined.

Background streams work by setting the global values of the following streams to the background stream:

*terminal-io*
*debug-io*
*standard-input*
*query-io*

Note that setting the value of those streams has the following consequences:

The second effect is less drastic than it sounds. The existing Lisp listener (what you see when Lisp starts up) has already bound all the listed streams so it does not see the global value at all. There may be a problem if you start your own processes (with, e.g., process-run-function), however. You should (and all applications should) set up your own streams. Consider the following two examples. In the first, the function run by process-run-function takes a stream argument and is passed *terminal-io*. This will work without error. In the second, the function itself contains a reference to *terminal-io*. It will signal an error if background streams are used.

;;; This will work:
(mp:process-run-function
      "foo"
      #'(lambda (stream)
          (format stream "This is from process ~a~%"
                  mp:*current-process*))
      *terminal-io*)
;;; This will fail:
(mp:process-run-function
      "foo"
      #'(lambda ()
          (format *terminal-io* "This is from process ~a~%"
          mp:*current-process*)))

The variable *initial-terminal-io* holds the original *terminal-io* stream when Lisp starts up. It may be useful for processes that aren't connected to a usable *terminal-io* but wish to produce some output, for example for debugging.


5.0 Stack commands

The runtime stack is the entity where arguments and variables local to Lisp functions are stored. When a function is called, the calling function evaluates and pushes the arguments to the called function onto the stack. The called function then references the stack when accessing its arguments. It also allocates space for its local variables. A stack frame is the area on the stack where the arguments to one function call and also its local variables reside. If foo calls bar, which in turns calls yaf, then (at least, and typically) three stack frames are active when yaf is entered. The frame for the most recently called function is at the top of the stack and the frame for the earliest called function is at the bottom of the stack. The commands described in the following subsections access and display the stack in a top-to-bottom order, although not all frames are necessarily displayed. After a frame is examined, it normally becomes the current stack frame. Further reference to the stack will, by default, operate on the current stack frame. When a break level is entered, the current frame pointer is typically the first interesting frame.

A frame object is a type of Lisp object. A frame expression is the printed representation of that object. We are somewhat imprecise in this document in distinguishing between frame objects (the internal Lisp object) and frame expressions (what you see printed) because in most cases, the distinction is not important. Where the distinction is important, we say frame object or frame expression.


5.1 :zoom

The :zoom top-level command prints the evaluation stack. It uses the current stack frame as the center of attention, and prints some number of frames on either side of the current frame. The value of the variable *zoom-display* is the total number of frames to display, and an equal number of frames are printed above and below the current stack frame, if possible. The arguments to the :zoom command control the type and quantity of the displayed stack.

After a :zoom or any of its analogs (such as :top or :bottom) the special variable cl:* contains the lisp expression representing the current frame. That expression is approximately what would be shown as the current frame by the :zoom with :moderate t and :function nil as arguments, regardless of the mode in which :zoom itself displays.

Here are some examples of :zoom command calls. We cause an error by trying to evaluate foo, which has no value.

USER(1): foo
Error: Attempt to take the value of the unbound variable `FOO'.
  [condition type: UNBOUND-VARIABLE]
[1] USER(2):

5.2 :brief, :moderate, :intermediate, and :verbose modes of :zoom

These arguments to :zoom (only one can be specified true and that value controls further :zoom commands until a new value is specified) control the amount of information printed.

Our examples show backtraces from the undefined error (call to undefined function foo) from the end of the previous section.

In :brief mode, only the function name is displayed for each frame and more than one frame is displayed on a single line. The current frame (EVAL) is displayed on its own line.

[1] user(2): :zoom :brief t
Evaluation stack:

  error <-
eval <-
  tpl:top-level-read-eval-print-loop <- tpl:start-interactive-top-level

In :moderate mode, each frame is on its own line and the function name and arguments appear. This is the initial setting for :zoom.

[1] user(3): :zoom :moderate t
Evaluation stack:

   (error #<unbound-variable @ #x10007cfb212>)
 ->(eval foo)
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level 
          #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000283812>
          #<Function top-level-read-eval-print-loop>
                                    ...)

:intermediate mode is the same as :moderate with pc (program counter) suspensions, which also appear in :verbose mode and are described in more detail there.

[1] cl-user(23): :zoom :intermediate t
Evaluation stack:

   (error #<unbound-variable @ #x10007cfb212>) suspended at relative address 472
 ->(eval foo) suspended at relative address 107
   (tpl:top-level-read-eval-print-loop) suspended at relative address 397
   (tpl:start-interactive-top-level 
        #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000283812> 
        #<Function top-level-read-eval-print-loop>
                                    ...) suspended at relative address 683

In :verbose mode, several lines are used per frame and much more information about arguments is provided.

[1] user(4): :zoom :verbose t
Evaluation stack:

   call to error
required arg: excl::datum = #<unbound-variable @ #x10007cfb212>
&rest excl::arguments = nil
function suspended at relative address 472
----------------------------
 ->call to eval
required arg: exp = foo
function suspended at relative address 107
----------------------------
   call to tpl:top-level-read-eval-print-loop
function suspended at relative address 397
----------------------------
   call to tpl:start-interactive-top-level
required arg: *terminal-io* = #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000283812>
required arg: function = #<Function top-level-read-eval-print-loop>
required arg: tpl::args = nil
&key tpl::initial-bindings = :unsupplied
function suspended at relative address 683
----------------------------

Note that in verbose mode, like in intermediate mode, each frame specifies the location at which the function was suspended:

   call to error
required arg: excl::datum = #<unbound-variable @ #x10007cfb212>
&rest excl::arguments = nil
function suspended at relative address 472

The suspension location may be relative or absolute (it is relative in the example just shown). It represents the value of the program counter at the time the function was suspended (usually because it called another function). The suspension location may be the instruction that caused the suspension (a call instruction or an instruction that caused trapping) or it may be the next instruction to be executed when the function is reactivated.

Suspension locations are interpreted as follows:

When a runtime operation is encountered in :verbose mode, a symbol-table is built (if an up-to-date one does not already exist) if possible. This build may take some time and trigger several garbage collections. After the symbol table is built, the address is associated with a name in the table and printed as its offset.

If the symbol table cannot be built, then no interpretation is given to the suspension location, only the absolute address is shown. Here is an example from the verbose backtrace following the attempt to evaluate an unbound variable:

----------------------------
 ->call to SYS::..CONTEXT-SAVING-RUNTIME-OPERATION with 0 arguments.
function suspended at address #x6fa1b1d0 (unbound+404)

Note that any non-lisp function can be disassembled (with disassemble) if it is represented in the symbol table by the string associated with its name. Thus

(cl:disassemble "qcons")

will print the disassembly of the runtime operation "qcons". If the name is not in the symbol table, cl:disassemble will complain that the argument is invalid.


5.3 :all t and :all nil modes of :zoom

Using the same error as above (trying to evaluate foo, which is unbound), here is the difference between specifying :all t and :all nil as arguments to :zoom. The frames hidden by default are those specified by default by :hide.

[1] USER(5): :zoom :all nil :moderate t
Evaluation stack:
 
  (ERROR UNBOUND-VARIABLE :NAME ...)
->(EVAL FOO)
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
  (TPL:START-INTERACTIVE-TOP-LEVEL
     #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @ #x18ad06>
     #<Function TOP-LEVEL-READ-EVAL-PRINT-LOOP @ #x2df9de> ...)
[1] USER(6): :zoom :all t
Evaluation stack:
 
... 1 more newer frame ...
 
  (ERROR UNBOUND-VARIABLE :NAME ...)
  (EXCL::UNBOUND-VARIABLE-HANDLER FOO)
  (EXCL::ER-GENERAL-ERROR-HANDLER-ONE 5 FOO)
  (EXCL::%EVAL FOO)
->(EVAL FOO)
  (TPL::READ-EVAL-PRINT-ONE-COMMAND NIL NIL)
  (EXCL::READ-EVAL-PRINT-LOOP :LEVEL 0)
  (TPL::TOP-LEVEL-READ-EVAL-PRINT-LOOP1)
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
 
... more older frames ...
[1] USER(7):

5.4 :function t and :function nil modes of :zoom

The difference between specifying the :function argument to :zoom t or nil can be shown by one frame, so we have removed all but one displayed frame. With :function t, the exact function object is identified.

[1] USER(7): :zoom :function t
Evaluation stack: [note: some lines removed]
 
->(FUNCALL #<Function EVAL @ #x1216d6> FOO)
 
[1] USER(8): :zoom :function nil
Evaluation stack: [note: some lines removed]
 
->(EVAL FOO)

5.5 :catches and :specials modes of :zoom

The :catches and :specials modes print information about catches and special-bindings respectively.

Both special bindings and catches are described using an internal Lisp index called a bindstack name pointer, or bnp. A bnp is an index into a binding stack for each thread, and each thread's current bnp is the index into the bindstack of the first empty binding. The bindstack holds both the symbol and its binding value, so it is always incremented and decremented by two and is thus always an even value. Both catches and bindings track bnp values; a binding's bnp value is the index into the bindstack where the global-variable's previous binding was stored, and a catch's bnp value is the index to restore to when a throw finds its way to that catch. The printouts for each are as follows:

Examples of :specials and :catches usage

Here we provide a simple function which has some catches in it and some special-bindings. The function does nothing, but is meant only for illustration. Starting by compiling and loading the file, and calling the function:

cl-user(1): (shell "cat catch.cl")
(defvar *foo* nil)
(defvar *bar* nil)
(defvar *bas* nil)

(defun foo (a b)
  (let ((*foo* nil))
    (catch 'bar
      (let ((*bar* t))
    (unwind-protect
        (catch 'bas
          (let ((*bas* 'hi))
        (break "baz: ~s ~s" a b)))
      (bar))))))

(defun bar ()
  nil)
0
cl-user(2): :cf catch
;;; Compiling file catch.cl
;;; Writing fasl file /tmp/cfta291721019271
;;; Moving fasl file /tmp/cfta291721019271 to catch.fasl
;;; Fasl write complete
cl-user(3): :ld catch
; Fast loading /net/gemini/home/duane/catch.fasl
cl-user(4): (foo 10 20)
Break: baz: 10 20

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(5):

Next we get a basic :zoom output. A count of 3 is chosen to limit some frames with a huge number of bindings. The result is a simple, moderate :zoom output:

[1c] cl-user(5): :zo :all t :count 3
Evaluation stack:

... 2 more newer frames ...

   (break "baz: ~s ~s" 10 ...)
 ->(foo 10 20)
   [... excl::%eval ]
   (eval (foo 10 20))
   (tpl::read-eval-print-one-command nil nil)

... more older frames ...
[1c] cl-user(6):

Next, we want to see both catches and special-bindings in the moderate :zoom output. Note that the stack works its way away from the stack top or frontier as it goes lexically downward, or in other words, scanning upward visually takes you toward the direction of the calls: starting from the bottom, read-eval-print-one-command binds some variables, then calls eval, which binds a couple more variables, and eval then (indirectly) calls foo, which binds *foo*, then catches bar, after which an unwind-protect is entered within which bas is caught, *bas* is bound, and finally break is called. Note that bar is not on the stack - it has not yet been called:

[1c] cl-user(6): :zo :catches t :specials t
Evaluation stack:

... 2 more newer frames ...

          Special excl::*restart-clusters* bound at bnp 292, old value: ((#) (#))
          Catching <block @ #x7ffffffc1970> last-bnp = 290 pc = 938
          Catching <block @ #x7ffffffc19b8> last-bnp = 290 pc = 966
   (break "baz: ~s ~s" 10 ...)
          Special *bas* bound at bnp 290, old value: nil
          Catching bas last-bnp = 288 pc = 407
          Catching <unwind-protect> last-bnp = 288 pc = 523
          Special *bar* bound at bnp 288, old value: nil
          Catching bar last-bnp = 286 pc = 555
          Special *foo* bound at bnp 286, old value: nil
 ->(foo 10 20)
   [... excl::%eval ]
          Special excl::%function-spec% bound at bnp 284, old value: nil
          Special sys:*interpreter-environment* bound at bnp 282, old value: nil
   (eval (foo 10 20))
          Special excl::*restart-clusters* bound at bnp 280, old value: ((#))
          Special tpl::*tpl-time* bound at bnp 278, old value: nil
   (tpl::read-eval-print-one-command nil nil)

... more older frames ...
[1c] cl-user(7):

Next we get a verbose output. Note that both :catches and :specials set the zoom-print-catches and zoom-print-special-bindings variables, respectively, so they are sticky across :zoom invocations. Notes are provided below the output:

[1c] cl-user(7): :zo :verbose t
Evaluation stack:

... 2 more newer frames ...

   call to break
optional arg: excl::datum = "baz: ~s ~s"
&rest excl::arguments = (10 20)
function suspended at relative address 895
          Catching <block @ #x7ffffffc19b8> last-bnp = 290 pc = 966
          Catching <block @ #x7ffffffc1970> last-bnp = 290 pc = 938
          Special excl::*restart-clusters* bound at bnp 292, old value: ((#) (#))
----------------------------
 ->call to foo
required arg: a = 10
required arg: b = 20
function suspended at relative address 394
          Special *foo* bound at bnp 286, old value: nil
          Catching bar last-bnp = 286 pc = 555
          Special *bar* bound at bnp 288, old value: nil
          Catching <unwind-protect> last-bnp = 288 pc = 523
          Catching bas last-bnp = 288 pc = 407
          Special *bas* bound at bnp 290, old value: nil
----------------------------
   (ghost call to excl::%eval)

----------------------------
   call to eval
required arg: exp = (foo 10 20)
function suspended at relative address 107
          Special sys:*interpreter-environment* bound at bnp 282, old value: nil
          Special excl::%function-spec% bound at bnp 284, old value: nil
----------------------------
   call to tpl::read-eval-print-one-command
required arg: tpl::continuable = nil
required arg: tpl::inspecting = nil
&key tpl::skip-prompt = :unsupplied
&key tpl::input-stream = :unsupplied
&key tpl::output-stream = :unsupplied
&key tpl::terminal-stream = :unsupplied
function suspended at relative address 8172
          Special tpl::*tpl-time* bound at bnp 278, old value: nil
          Special excl::*restart-clusters* bound at bnp 280, old value: ((#))
----------------------------

... more older frames ...
[1c] cl-user(8):

Note in the above display that the ordering of special bindings and catches is reversed from the moderate style. In a :verbose style each frame tends to represent an individual function's structure, including arguments, and is separated by dashed lines from other functions' frames. Because it is less compact than the :moderate style, functions tend to be seen individually and their contents are seen in the same ordering as they appear in actual source code (it is even more accurate to say that the output appears in the same nesting scope as the source code).

Finally, the list form of the :specials mode is demonstrated, by enlarging the count and removing :verbose and :catches mode:

[1c] cl-user(8): :zo :count t :specials (*standard-input* *standard-output*) :catches nil :verbose nil
Evaluation stack:(looking for limited specials (*standard-input* *standard-output*))

          Special *standard-input* current value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
          Special *standard-output* current value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
          Special *standard-output* bound at bnp 338, old value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
          Special *standard-input* bound at bnp 336, old value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
   (excl::read-eval-print-loop :continue-error-string "return from break." ...)
          Special *standard-input* bound at bnp 308, old value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
          Special *standard-output* bound at bnp 306, old value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
   (excl::internal-invoke-debugger "Break" #<simple-break @ #x100088d9c82> ...)
   (break "baz: ~s ~s" 10 ...)
 ->(foo 10 20)
   [... excl::%eval ]
   (eval (foo 10 20))
   (tpl::read-eval-print-one-command nil nil)
          Special *standard-output* bound at bnp 226, old value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
          Special *standard-input* bound at bnp 224, old value: #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862>
   (excl::read-eval-print-loop :level 0)
   (tpl::top-level-read-eval-print-loop1)
   (tpl:top-level-read-eval-print-loop)
   ((:runsys sys::lisp_apply))
   [... sys::funcall-tramp ]
   (tpl:start-interactive-top-level #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x10000250862> #<Function top-level-read-eval-print-loop>
                                    ...)
   (excl::start-lisp-execution t)
   ((:runsys sys::lisp_apply))
   [... excl::thread-bind-and-call* ]
   (excl::thread-bind-and-call nil #<Function start-lisp-execution-0> ...)
[1c] cl-user(9): 

Note that the bindings of these two variables are the same in all cases, which means that if either variable is modified with setq at the top level prompt and either a :pop or a :reset is performed, the original values will be restored. Note also that when specific variables are requested in the :specials mode, the current value is first displayed, and if then there are any further bindings between the very top of the stack and the first displayed frame, the closest binding to the top displayed frame will be shown as well.


5.6 :relative t and :relative nil modes of :zoom

The :relative argument causes :zoom to identify a frame with respect to the current frame. The identification is done with a number and either u (meaning up or newer than the current frame) or d (meaning down or older than the current frame).

[1] USER(18): :zoom :relative t
Evaluation stack:
 
1u: (ERROR UNBOUND-VARIABLE :NAME ...)
   ->(EVAL FOO)
1d: (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
2d: (TPL:START-INTERACTIVE-TOP-LEVEL
        #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @
#x18ad06>
               
...)

The stack with :relative nil does not display numbers next to frames.

[1] USER(18): :zoom :relative nil
Evaluation stack:
 
  (ERROR UNBOUND-VARIABLE :NAME ...)
->(EVAL FOO)
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
  (TPL:START-INTERACTIVE-TOP-LEVEL
     #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @ #x18ad06> ...)

5.7 :zoom suspension point information

When sources are in a file and compilation is done so source-level debugging is enabled (usually a debug value of 3) and the current frames function has debugging information, the most recent source will be printed after the suspension location is printed. If the frame is the current frame (if :current would have printed this frame only) then the slide point is also set, and until any breakpoint actions are taken, the :slide command can be used to look at, move around, or set breakpoints in this function.

Consider the following function defined in a file ltest.cl:

(eval-when (compile) (declaim (optimize debug)))
(defun ltest (x)
  (with-open-file (s x)
    (let ((y (read s)))
      (format t "~s" y))))

We compile and load the file and make an erroneous call to the function ltest:

cl-user(3): :cl ltest
;;; Compiling file ltest.cl
;;; Writing fasl file /tmp/cfta106831758131
;;; Moving fasl file /tmp/cfta106831758131 to ltest.fasl
;;; Fasl write complete
; Fast loading /net/gemini/home/duane/ltest.fasl
cl-user(4): (ltest 10)
Error: 10 cannot be converted to a pathname.
  [condition type: type-error]

Restart actions (select using :continue):
 0: Return to Top Level (an "abort" restart).
 1: Abort entirely from this (lisp) process.
[1] cl-user(5): :zo :all t :count 2
[ ... ]
[1] cl-user(6): :dn 2
Evaluation stack:

... 5 more newer frames ...

   (open 10)
 ->(ltest 10)
slide point set to: (open x)

   [... excl::%eval ]
   (eval (ltest 10))

... more older frames ...
[1] cl-user(7): :slide point
Function: #<Function ltest>:
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
     4/34    1-2   5/34     nil/nil    :early  nil    nil   nil   (open x)
     5/34    0-2   6/39     nil/nil    :ref   nil    87    88    x
->   6/39    1-2   7/44     nil/nil    nil    :call  nil   nil   (open x)
     7/44    1-2   8/44     nil/nil    :const  nil    nil   nil   (quote t)
     8/44    1-2   9/50     nil/nil    :init  nil    nil   nil   ((s (open x)) s nil)
  level=0 pc=39 rec=6 
Possible slide directions: (out across back)
[1] cl-user(8): :slide enclosed
Sliding to #<Function ltest>: 34
Function: #<Function ltest>:
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
     0/0     0   2/34     nil/nil    nil    nil    49    140   (defun ltest (x) (with-open-file (s x) (let ((y #)) (format t "~s" y))))
     1/34    1   2/34     nil/nil    nil    nil    (49)  (140) (block ltest (with-open-file (s x) (let ((y #)) (format t "~s" y))))
->   2/34    0-1   4/34     nil/nil    nil    nil    68    139   (with-open-file (s x) (let ((y (read s))) (format t "~s" y)))
     3/34    1-2   4/34     nil/nil    nil    nil    (68)  (139) (let ((s (open x)) (#:with-open-file-abort-159 t)) ...)
     4/34    1-2   5/34     nil/nil    :early  nil    nil   nil   (open x)
  level=0 pc=34 rec=2  starting at 68 ending at 139.
Possible slide directions: (into out over across back)
[1] cl-user(9):

Note that in the example above, the slide point was set to the open call (which was the call in which the error occurred) but that record has no position info (because it is part of the macro-expansion of the with-open-file form, and it doesn't replace the entire form). Using the :slide command with the enclosed option slides us outward conceptually to the point where the error is occurring in the text, somewhere between character positions 68 (inclusive) and 139 (exclusive) in the original source.


5.8 The :bt command for a quick look at the stack

The :bt command provides a quick way to scan the stack.

Here is the effect of the :bt command on the example above (attempt to evaluate foo which has no value). In this case, :zoom is printing all frames (:all t) so :bt does as well.

[1] USER(24): :bt
Evaluation stack:
 
EVAL <-
  TPL::READ-EVAL-PRINT-ONE-COMMAND <- 
  EXCL::READ-EVAL-PRINT-LOOP <-
  TPL::TOP-LEVEL-READ-EVAL-PRINT-LOOP1 <-
  TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP <- APPLY <-
  TPL:START-INTERACTIVE-TOP-LEVEL <- TPL::START-TOP-LEVEL <-
  EXCL::START-REBORN-LISP

5.9 Variables that affect the behavior of :zoom

The following variables affect the behavior of :zoom, usually by providing a default for an argument to :zoom. Note that the values of some of these variables are changed when :zoom is called with the argument associated with the variable specified. All symbols naming the variables are in the top-level package (nicknamed tpl).

*zoom-display* Controls the maximum number of stack frames displayed by a call to :zoom. Default for the :count argument for :zoom.
*zoom-print-circle* cl:*print-circle* is bound to this during :zoom output.
*zoom-print-level* cl:*print-level* is bound to this during :zoom output.
*zoom-print-length* cl:*print-length* is bound to this during :zoom output.
*zoom-print-long-string-length* excl:*print-long-string-length* is bound to this during :zoom output.
*zoom-print-special-bindings* Default for the :specials argument to :zoom. Value reset if :zoom is called with :specials specified.
*zoom-show-newer-frames* If true, :zoom output shows frames newer than the current frame.
*auto-zoom* Controls whether frames are printed after moving up or down a frame or displaying the current frame (see :up, :dn, and :current).

5.10 Special handling of certain errors by :zoom

Here is how calls to undefined functions and calls with the wrong number of arguments, both in compiled code, are handled.

If, in compiled code, a call is made to an undefined function foo, a frame on the stack will be created to represent that call and it will show all of the arguments passed to foo.

For example:

user(4): (defun bar (x) (foo 1 2 3 4 x))
bar
user(5): (compile 'bar)
compiling bar
user(6): (bar 222)
Error: the function foo is undefined.
  [condition type: undefined-function]
 
Restart actions (select using :continue):
  0: Try calling foo again
  1: Return a value instead of calling foo
  2: Try calling a function other than foo
  3: Setf the symbol-function of foo and call it again
[1] user(7): :zoom
Evaluation stack:
 
  (error #<undefined-function @ #x10b9a86>)
->(foo 1 2 ...)
  (bar 222)
  (eval (bar 222))
  (tpl:top-level-read-eval-print-loop)
  (tpl:start-interactive-top-level
      #<excl::bidirectional-terminal-stream @ #x2aa24e>
      #<Function top-level-read-eval-print-loop @ #x2d96de>
...)
[1] user(8): :cur
(foo 1 2 3 4 222)
[1] user(9):

This frame can be restarted after defining foo:

[1] user(10): (defun foo (&rest x) x)
foo
[1] user(11): :cur
(foo 1 2 3 4 222)
[1] user(12): :restart 
(1 2 3 4 222)
user(13):

In the wrong number of arguments situation (in our case, too few arguments) we again get an ordinary frame with the unsupplied arguments identified as :unknown:

USER(14): (defun baz (a b c)
            (+ a b c))
BAZ
USER(15): (compile 'baz)
BAZ
NIL
NIL
USER(16): (baz 1 2)
Error: BAZ got 2 args, wanted 3 args.
  [condition type: PROGRAM-ERROR]
[1] USER(17): :zo
Evaluation stack:
 
  (ERROR PROGRAM-ERROR :FORMAT-CONTROL ...)
->(BAZ 1 2 :UNKNOWN)
  (EVAL (BAZ 1 2))
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
  (TPL:START-INTERACTIVE-TOP-LEVEL
       #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @
#x487586>
       #<Function TOP-LEVEL-READ-EVAL-PRINT-LOOP @
#x49114e> ...)
[1] USER(18):

5.11 :zoom analogs and stack movement commands

The following commands are used to move the current frame pointer around the stack.

:dn Move down the stack
:up Move up the stack.
:bottom Move to the bottom (oldest frame) of the stack.
:top Move to the top (newest frame) of the stack.
:find Move to the frame associated with the function-name argument to this command.

5.12 Commands that hide frames

These commands control which frames are displayed. Note that their effect can be overridden by calling :zoom with the :all argument specified t.

:hide Hide (i.e. :zoom should not display unless :all is specified t) things specified by the arguments.
:unhide Unhide (i.e. :zoom with :all nil should display) things specified by the arguments. :unhide called with no arguments causes the list of hidden objects to revert to the default set of hidden objects. Note that if you want to see all frames in the stack, you should call :zoom with a true value for the all argument. :unhide itself is not designed to unhide all frames.
;; In this example we hide all frames in the tpl package.
USER(66): :unhide
Reverting hidden objects to initial state.
USER(67): :hide
hidden packages: LEP EXCL SYSTEM CLOS DEBUGGER
hidden packages internals: TOP-LEVEL
hidden functions: BLOCK APPLY
hidden frames: :INTERPRETER :EVAL :INTERNAL
;;
;; We cause a BREAK and do a :zoom.
USER(68): (break)
Break: call to the `break' function.
 
Restart actions (select using :continue):
  0: return from break.
[1c] USER(69): :zoom :moderate t
Evaluation stack:
 
  (BREAK "call to the `break' function.")
->(EVAL (BREAK))
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
  (TPL:START-INTERACTIVE-TOP-LEVEL
      #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @ #x19e67e>
      #<Function TOP-LEVEL-READ-EVAL-PRINT-LOOP @ #x1c7ad6>
...)
;;
;; We hide all symbols in the TOP-LEVEL package.
[1c] USER(70): :hide :package tpl
[1c] USER(71): :zoom
Evaluation stack:
 
  (BREAK "call to the `break' function.")
->(EVAL (BREAK))
[1c] USER(72):

5.13 Frame information commands

The following commands identify the current frame and the function called by the current frame or provide other information.

:current Print the current stack frame and set the value of cl:* to be the frame expression.
:function Print the function object called in the current stack frame and set the value of cl:* to that object.
:frame Print out information about the current frame.
:register Print out the names and values of registers (or a specific register when passed its name), when the frame has saved context. Registers can be set with :set-register.
;; In the example below, we use :current, :function and :frame. 
;; Note that we use :hide :binding to limit the amount of output 
;; printed in this example.
user(47): (defun foo (x)
            (let ((a (1+ x)))
              (let ((b
(1+ a)))
               
(progn (break "foo: ~a" b)
                      
(foo 1 2 3 4 5 x a b)))))
foo
 
;; :hide :binding causes binding frames to be hidden.
user(48): :hide :binding
user(49): (foo 10)
Break: foo: 12
 
Restart actions (select using :continue):
  0: return from break.
[1c] user(50): :zoom
Evaluation stack:
 
  (break "foo: ~a" 12)
->(foo 10)
  (eval (foo 10))
  (tpl:top-level-read-eval-print-loop)
  (tpl:start-interactive-top-level
      #<excl::bidirectional-terminal-stream @ #x280886>
      #<Function top-level-read-eval-print-loop @ #x2ce19e>
...)
;;
;; Print out the current frame.
[1c] user(51): :current
(foo 10)
 
;; Print out the function object associated with the 
;; current frame.
[1c] user(52): :function
#<Interpreted Function foo @ #xaf983e>
 
;; Print out interesting information about the frame. Note 
;; that the information printed included frames hidden by the
;; :hide :binding command
[1c] user(53): :frame
Expression: (foo 10)
  Source code: (let ((a (1+ x))) ..)
  Local x: 10
  Source code: (let ((b (1+ a))) ..)
  Local a: 11
  Source code: (progn (break "foo: ~a" b) (foo 1 2 3 4 5 x a b))
  Local b: 12
[1c] user(54): :res
user(55): 

5.14 Local variables and evaluation

The commands described in this section operate in some fashion on frames. Note that local name information (which is very useful for debugging) is only loaded from a compiled file if the variable *load-local-names-info* is non-nil when the compiled (fasl) file is loaded.

:local This command causes the values of local variables for the current stack frame to be printed. The handling of local variables by the debugger is discussed under the heading Local variables and the debugger below for complete information and examples.
:set-local This command sets the value of the specified (as an argument) local variable to value, which is evaluated. The local variable is identified by name if interpreted, or name or index if compiled.
:evalmode This command allows evaluation in context, that is local variables can be used by name in forms typed to the top level. Setting such a local variable (with setq or setf) at the top level will change the value in the frame. Evaluation in context does not always work as desired (unavoidably so because of the lexically-scoped design of Lisp). It should only be used temporarily while debugging.

5.15 Getting a backtrace programmatically

Sometimes, particularly when a program is being run in batch mode (rather than interactively) or when the user of the program is not familiar with Lisp internals, you may want to generate a backtrace (as produced by :zoom) programmatically, perhaps writing it to a file as part of a problem report.

Functionality for doing this is in the autozoom module, with symbols in the top-level.debug package. Load this module with the command

(require :autozoom)

If you want this functionality in an application, be sure to include :autozoom in the list of required files (the third required argument to generate-application).

There are two operators provided: the macro with-auto-zoom-and-exit and the function zoom. Both are defined in this section. The source for with-auto-zoom-and-exit is available in the file [Allegro directory]/src/autozoom.cl.



zoom

function, package: top-level.debug

Arguments: stream &rest zoom-command-args &key (count t) (all t) &allow-other-keys

Generate an execution stack trace to stream. This function is a wrapper for the top-level :zoom command and is intended to be used in applications that want to report unexpected errors. See with-auto-zoom-and-exit.

This function is only useful when used in conjunction with handler-bind. For example:

 (handler-bind
    ((error (lambda (condition)
          ;; write info about CONDITION to a log file...
              (format *log-stream* \"Error in app: ~a~%\" condition)

          ;; send a zoom to the log file, too
          (top-level.debug:zoom *log-stream*))))
  (application-init-function))

The count and all keywords are passed to the :zoom command and are documented with that command.


with-auto-zoom-and-exit

macro, package: top-level.debug

Arguments: (place &key (count t) (all t) (exit t) no-unwind &body body

This macro generates an execution stack trace to place, which can be a stream, t or a pathname. If a pathname, that file is opened, created or superseded as necessary, and used. This macro is a wrapper for top-level :zoom command and is intended to be used in applications that want to report unexpected errors.

The count and all keywords are passed to the :zoom command and are documented with that command.

A non-nil value for exit causes the running application to terminate. The default value of exit is t. (The normal situations for using this macro are (1) the program is running in batch mode and so there is no operator available to do anything; and (2) the program is being used by a user unfamiliar with the internals and so unable to do anything. As there is no one to take any action, this macro typically gets a backtrace, which is written to place, and then causes the program to exit.)

A non-nil value for no-unwind causes unwind-protects in stack frames above the error to be ignored. (This argument is simply passed to exit which also accepts a no-unwind keyword argument.) no-unwind is ignored if exit is nil. The default value of no-unwind is nil (as it is for exit).

The source for this macro is provided with the Allegro CL distribution, in [Allegro directory]/src/autozoom.cl.


6.0 Local variables and the debugger


6.1 What are local variables?

The local variables in a function include the arguments of the function and variables bound in a let or similar form within the function. Variables that are declared globally-special are not considered local variables, even if they are dynamically bound as an argument or in a let-binding.


6.2 Are locals stored in registers or on the stack?

The answer is both, either just in registers or on the stack and occasionally also in registers. Many architectures require at least one value to be in a register for their instructions to operate. So movement into and out of registers is inevitable. If a variable is stored onto the stack, it will be only in one non-volatile place for that variable's lifetime, regardless of where it may be temporarily located when it is needed in a register.

On certain platforms it is possible to store values of local variables in non-volatile registers rather than on the stack. This resulted in faster code execution but made debugging more difficult because (1) is was sometimes difficult to associate the local variable name with its value, and (2) local variables with disjoint life times (that is, one is not used at all until the other's use has ended) might share a non-volatile register so only the value of one is known when requested. When locals are on the stack, being sure of the variable name can still be a problem (but a more solvable one) while two locals never share a stack location.

As it happens, only Sparcs and RS/6000 among Allegro CL platforms allow local variables to be stored in non-volatile registers, and neither platform is supported in Allegro CL version 11.0 or later. Register allocation is relevant for 10.0 and 10.1 but those are less common platforms. So we are removing all discussion of storing locals in registers. The rest of this section is only about storing locals on the stack.


6.3 Local variable introduction

Allegro CL provides for examining and modifying the values of local variables in compiled code but this facility comes at a space cost (since extra information is needed in compiled code to provide the debugger with this information). Therefore, really useful local variable information in the debugger is only available in code compiled with the debug optimization quality set to 3 (assuming compiler switches have their default values). Further, local name information (which is very useful for debugging) is only loaded from a compiled file if the variable *load-local-names-info* is non-nil when the compiled (fasl) file is loaded.


6.4 An initial example with lots of local variables

Consider this function in the file loc-fun.cl:

(defun loc-fun (arg)
  (let ((alfa (+ arg 1)) ;; alfa alive but see text
    (bravo (+ arg 2)) ;; bravo alive but see text
    (charlie 3)) ;; charlie alive
    (break "break1")
    (setq charlie (+ alfa bravo charlie arg))

    ;; alfa and bravo are now dead
    (let ((delta 4) ;; delta alive
      (echo 5)) ;; echo alive
      (break "break2")
      (print (* delta echo charlie))))

  ;; charlie, delta, and echo are now dead
  (let ((alfa 10)) ;; (new) alpha alive
    (let ((alfa (1+ alfa)) ;; (newest) alfa alive; one alfa shadowe.
      (bravo 11) ;; (new) bravo alive
      (delta 12)) ;; (new) delta alive
      (break "break3")
      (print (+ alfa bravo delta)))
    (break "break4")
    (print alfa)))

We compile and load the file. Note the debug optimization quality is 3 and *load-local-names-info* is true:

cl-user(21): :opt

A response of ? gets help on possible answers.
   [...]
Compiler optimize setting is
     (declaim (optimize (safety 1) (space 1) (speed 1) (debug 3)
               (compilation-speed 1)))
cl-user(22): *load-local-names-info*
t
cl-user(23): :cf loc-fun
; Fast loading local-fun.fasl
cl-user(25): 

All local variables have ranges when they are alive (that is in use). Before they are used and after their last use, they are dead, which means that their value is not important because it is never looked at. We have marked our function above up to show the live ranges of our local variables. Note that alfa and bravo are technically not alive until charlie gets its value since if the charlie binding was (* *alfa* *bravo* 3), the values of alfa and bravo in that form would be the lexical values prior to the start of the let form but the compiler can see that the symbols alfa and bravo are not used in that way so they come to life sooner. (If we used let* rather than let, alfa and bravo would always come to life when bound as appearances in the charlie binding form would refer to the bound values of alfa and bravo.)


6.5 Discarding local variable information before dumplisp

Before we discuss local variables in detail, we should remark that the information necessary to provide information on local variables for debugging takes up a fairly significant amount of space in the image. If you have completed debugging and you want to dump an image (with dumplisp, you may want to discard this information before dumping (to save space in the dumped image). The function discard-local-name-info will do that.


6.6 Summary of the discussion of locals


6.7 How does the compiler treat local variables?

The names of local variables are not necessary in compiled code (the locations are sufficient). Saving names takes space. The compiler will only save names when the speed, safety, and debug compiler optimization qualities are such that the compiler switch save-local-names-switch is either a function returning true or is itself true. It is usual when developing code (so debugging is likely) to compile with that switch true, so names are available. In production code, it is usual to compile so names are not saved, to save space. When debugging code where names are not saved, locals are identified by a numerical index.

Further, local name information, if stored in a compiled file, is only loaded if the value of the variable *load-local-names-info* is non-nil when compiled (fasl) files created with local variable information are loaded.


6.8 What is the difference between using registers and using the stack?

The advantage of register access is that it is much faster than stack access. But there is also a downside: there may not be a variable name associated with the register, because it is transient (because of efficient compilation) and never stored on the stack. This means that debugging will be harder.

We will get back to the advantages of placing values only in registers below, but we want to emphasize the debugging costs so that they are clear. Consider this example:

(defun blah (&rest format-args)
  (let ((result (apply #'format nil format-args)))
    (read-from-string result)))

(compile 'blah)

(blah "~a/0" 42)

Evaluating the last form will cause a divide by zero (result is the string "42/0", which is then read). But the variable named result never shows up on the stack because it is transiently used as the return value from an apply and as the first argument to read-from-string, and thus does not even move from its register location - in all Allegro CL implementations, the first return-value register is also the first argument register; this provides for extremely efficient code. Therefore, when debugging (after the divide by zero error is signaled), you want to look at the value of result but will not be able to see it under that name.

If you want to force storing values on the stack, you can trace read-from-string with a form like

(trace (read-from-string :inside blah))

Or you can use the :break top-level command to set an instruction-level breakpoint and setting :ldb mode on will allow single-stepping to track the variables (including the arg0/result register) if the user knows how to look at the disassembler output and interpret assembler code.

Or you can redefine blah to force result onto the stack so that it will show up as a local variable:

(defun blarg () nil)

(defun blah (&rest format-args)
  (let ((result (apply #'format nil format-args)))
    (blarg)
    (read-from-string result)))

Inserting the call to blarg forces result onto the stack (because the register holding its value cannot be protected during a function call). result will thus show up in debugger when the read-from-string errors. Note, however, that if local-scopes are compiled in, then the debugger will not show the variable named result, because it is a dead local (its last usage is as an argument to read-from-string). To see the name in that situation, the :d option to the :local debugging command can be used.

Now back to the advantages of using registers to hold values. Because of the very significant performance boost, registers are used whenever possible. Further, when registers are used, the compiler will analyze the code in order to determine the live ranges of locals and have locals with disjoint live ranges share the same register (live range is defined just below). This allows maximum use of registers.

Locals stored on the stack do not share locations -- that is, each local stored on the stack is assigned its own location. This is true on machines where all locals are stored on the stack and on machines where some locals are stored in registers and others are stored on the stack. It may be that sharing locations on the stack would be a desirable optimization (because of reduced stack growth). However, it has not been implemented.

The examples in the rest of this section are taken from a Sparc machine and therefore registers are used for locals. All the behavior described applies to any machine (whether or not registers are used to store locals) except maybe the concept of register sharing.


6.9 Live ranges of local variables

When compiling a function, the compiler tries to determine when a local variable is first defined (typically, when it is bound) and when a local variable is last used. The time that the local is used is called the live range of the variable.

When locals can be stored in registers, if two locals have disjoint live ranges, the compiler may use the same register to store the values of both variables.


6.10 Locals and functions in the tail position

Consider this definition:

(defun foo (lis)
  (let ((a 10) (b 9))
    (pprint (list a b lis))
    (list-length lis)))

The function list-length is in the tail position, which means that what it returns will be returned by foo, and so no information about foo is needed on the stack. The compiler will always arrange that the stack is cleared of all but the tail position function when possible (regardless of whether it tail merges or not). Therefore, the form (foo 10) will cause an error when list-length is applied to 10 -- which is not a list -- but the values of locals a and b will not be available.


6.11 Example showing live range

The behaviors of live ranges and their interactions with the :local and :set-local top-level commands are best shown by example. Consider the following function definition in file fun-loc.cl:

(defun loc-fun (arg)
  (let ((alfa (+ arg 1)) ;; alfa alive but see text
    (bravo (+ arg 2)) ;; bravo alive but see text
    (charlie 3)) ;; charlie alive
    (break "break1")
    (setq charlie (+ alfa bravo charlie arg))

    ;; alfa and bravo are now dead
    (let ((delta 4) ;; delta alive
      (echo 5)) ;; echo alive
      (break "break2")
      (print (* delta echo charlie))))

  ;; charlie, delta, and echo are now dead
  (let ((alfa 10)) ;; (new) alfa alive
    (let ((alfa (1+ alfa)) ;; (newest) alfa alive; one alfa shadows.
      (bravo 11) ;; (new) bravo alive
      (delta 12)) ;; (new) delta alive
      (break "break3")
      (print (+ alfa bravo delta)))
    (break "break4")
    (print alfa)))

The comments show where the various locals become alive and when they die. There is an issue about where alfa and bravo become alive. Strictly speaking, alfa, bravo, and charlie all become alive when the binding form completes. That is guaranteed in let so if alfa and bravo had lexically apparent values from above the let form, they could be used later in the binding form. In fact that does not happen, so the compiler may make the assignment at the position indicated rather than later. This is highly architecture-dependent and might or might not occur, depending on debug and speed settings.

The breaks are inserted to allow us to examine what is happening at various points.

Compiling this function with debug 3 ensures that local names are saved. But also, we need to be sure local names are also loaded when the file is loaded.

cl-user(1): (declaim (optimize debug))
t
cl-user(2): (setq *load-local-names-info* t *load-source-file-info* t *load-source-debug-info* t)
t
cl-user(3): (setq *record-source-file-info* t)
t
cl-user(4): :cf loc-fun
;;; Compiling file loc-fun.cl
;;; Writing fasl file /tmp/cfta278160006231
;;; Moving fasl file /tmp/cfta278160006231 to loc-fun.fasl
;;; Fasl write complete
cl-user(5): :ld loc-fun
; Fast loading /net/gemini/home/duane/loc-fun.fasl
cl-user(6): (loc-fun 22)
; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 190 (inclusively) and 216 (exclusively)
;[form: "(+ alfa bravo charlie arg)"]:
Break: break1

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(7):

We are now in the first break; :loc shows what variables are alive:

[1c] cl-user(7): :loc
Compiled lexical environment:
0(Required): arg: 22
1(Local): arg: 22
3(Local): charlie: 3
4(Local): alfa: 23
6(Local): bravo: 24
[1c] cl-user(8):

And the :d option shows all of them:

[1c] cl-user(8): :loc :d
Compiled lexical environment:
0(Required): arg: 22
1(Local): arg: 22
2(Local): (:dead alfa): 1
3(Local): charlie: 3
4(Local): alfa: 23
5(Local): (:dead delta): nil
6(Local): bravo: 24
7(Local): (:dead alfa): 0
8(Local): (:dead bravo): 0
[1c] cl-user(9):

Here we start by trying to set a variable named alfa, but since there are 3 such variables, and since we want to include the dead ones, it refuses:

[1c] cl-user(9): :set-loc :d alfa 10
There are multiple matching variables named alfa that make setting one value
ambiguous.  Use the local (integer) index to specify the variable to set:

2(Local): (:dead alfa): 1
4(Local): alfa: 23
7(Local): (:dead alfa): 0

Couldn't set the variable.
[1c] cl-user(10):

But because delta, which is also a dead variable, is the only one of that name, it works (silently):

[1c] cl-user(10): :set-loc :d delta 10
[1c] cl-user(11):

... and can be called back up again:

[1c] cl-user(11): :loc :d delta
10
[1c] cl-user(12): :loc :d
Compiled lexical environment:
0(Required): arg: 22
1(Local): arg: 22
2(Local): (:dead alfa): 1
3(Local): charlie: 3
4(Local): alfa: 23
5(Local): (:dead delta): 10
6(Local): bravo: 24
7(Local): (:dead alfa): 0
8(Local): (:dead bravo): 0
[1c] cl-user(13):

Now we look to see what delta is, even though it is dead. Because there is at least one dead variable named delta and no live ones, we get a note to that effect:

[1c] cl-user(13): :loc delta
No live values. There is at least one variable named delta that is dead.
Use the :d option to :local see any dead variables.
[1c] cl-user(14):

Let's move on to the second break:

[1c] cl-user(14): :loc :d delta
10
[1c] cl-user(15): :cont
; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 345 (inclusively) and 375 (exclusively)
;[form: "(print (* delta echo charlie))"]:
Break: break2

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(16):

Note that only charlie is live (besides arg - arguments are always live):

[1c] cl-user(16): :loc
Compiled lexical environment:
0(Required): arg: 22
3(Local): charlie: 72
[1c] cl-user(17):

Note that if we ask for a name that is not known, we see no output (as we did before this change):

[1c] cl-user(17): :loc bogus
[1c] cl-user(18):

Here we look at alfa, which has no live values, just like delta earlier.

[1c] cl-user(18): :loc alfa
No live values. There is at least one variable named alfa that is dead.
Use the :d option to :local see any dead variables.
[1c] cl-user(19):

And trying to set this dead variable fails, because no values are live. But using the :d option also fails, because there are multiple dead variables named alfa:

[1c] cl-user(19): :set-loc alfa 10
No live values to set. There is at least one variable named alfa that is dead.
Use the :d option to :local or :set-local to set or see any dead variables.
Couldn't set the variable.
[1c] cl-user(20): :set-loc :d alfa 10
There are multiple matching variables named alfa that make setting one value
ambiguous.  Use the local (integer) index to specify the variable to set:

2(Local): (:dead alfa): 1
4(Local): (:dead alfa): 23
7(Local): (:dead alfa): 0

Couldn't set the variable.
[1c] cl-user(21):

Instead, following the instructions, the desired variable can be set using its index (in this case we chose 7):

[1c] cl-user(21): :set-loc :d 7 10
[1c] cl-user(22): :loc :d
Compiled lexical environment:
0(Required): arg: 22
1(Local): (:dead arg): 22
2(Local): (:dead alfa): 1
3(Local): charlie: 72
4(Local): (:dead alfa): 23
5(Local): (:dead delta): 10
6(Local): (:dead bravo): 24
7(Local): (:dead alfa): 10
8(Local): (:dead bravo): 0
[1c] cl-user(23): :cont

1440 ; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 631 (inclusively) and 659 (exclusively)
;[form: "(print (+ alfa bravo delta))"]:
Break: break3

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(24): :loc
Compiled lexical environment:
0(Required): arg: 22
2(Local): alfa: 10
5(Local): delta: 12
7(Local): alfa: 11
8(Local): bravo: 11
[1c] cl-user(25):

Note that we have two live variables named alfa. This is because alfa has nested bindings, and although one is not visible at this point in the program, it still is live.

[1c] cl-user(25): :loc :d
Compiled lexical environment:
0(Required): arg: 22
1(Local): (:dead arg): 22
2(Local): alfa: 10
3(Local): (:dead charlie): 72
4(Local): (:dead alfa): 23
5(Local): delta: 12
6(Local): (:dead bravo): 24
7(Local): alfa: 11
8(Local): bravo: 11
[1c] cl-user(26):

Here we try to set a value for alfa, which has two live locations.

[1c] cl-user(26): :set-loc alfa 15
There are multiple matching variables named alfa that make setting one value
ambiguous.  Use the local (integer) index to specify the variable to set:

2(Local): alfa: 10
7(Local): alfa: 11

Couldn't set the variable.
[1c] cl-user(27):

In this case, using the index works just as in the case of the multiple dead variables.

[1c] cl-user(27): :set-loc 7 15
[1c] cl-user(28): :loc
Compiled lexical environment:
0(Required): arg: 22
2(Local): alfa: 10
5(Local): delta: 12
7(Local): alfa: 15
8(Local): bravo: 11
[1c] cl-user(29): 

Let's continue on to the last break:

[1c] cl-user(29): :cont

38 ; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 686 (inclusively) and 698 (exclusively)
;[form: "(print alfa)"]:
Break: break4

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(30):

Note that the last break occurs after the scope of the inner alfa has closed, so that inner alfa (which we forced to 15 earlier) is now dead, and only the outer variable is alive with its original value of 10:

[1c] cl-user(30): :loc
Compiled lexical environment:
0(Required): arg: 22
2(Local): alfa: 10
[1c] cl-user(31):

And the newly dead alfa with value 15 can still be seen as dead here:

[1c] cl-user(31): :loc :d
Compiled lexical environment:
0(Required): arg: 22
1(Local): (:dead arg): 22
2(Local): alfa: 10
3(Local): (:dead charlie): 72
4(Local): (:dead alfa): 23
5(Local): (:dead delta): 12
6(Local): (:dead bravo): 24
7(Local): (:dead alfa): 15
8(Local): (:dead bravo): 11
[1c] cl-user(32):

Unfortunately there is no way to know what the hierarchy of variables is after compilation; the debug structures don't currently support info that describes hierarchy.

Note that the above breaks in function operation have all been made via calls to the break function. Executing the :zoom command shows that break is the next function on the stack after the function we're examining:

[1c] cl-user(32): :zo :all t
Evaluation stack:

   (excl::read-eval-print-loop :continue-error-string "return from break." ...)
   (excl::internal-invoke-debugger "Break" #<simple-break @ #x1000886a3c2> ...)
   (break "break4")
 ->(loc-fun 22)
slide point set to: (break "break4")

   [... excl::%eval ]
   (eval (loc-fun 22))
   (tpl::read-eval-print-one-command nil nil)
   (excl::read-eval-print-loop :level 0)
   (tpl::top-level-read-eval-print-loop1)
   (tpl:top-level-read-eval-print-loop)

... more older frames ...
[1c] cl-user(33):

However, if we start over, and use the source-level debugger to step through source arbitrarily (note in this case we're going to stop before the first let form in the function):

cl-user(1): (setq *load-local-names-info* t *load-source-file-info* t *load-source-debug-info* t)
t
cl-user(2): :ld loc-fun
; Fast loading /net/gemini/home/duane/loc-fun.fasl
cl-user(3): :br loc-fun :pos 23
Adding #<Function loc-fun>: 35
Function: #<Function loc-fun> in #P"loc-fun.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
     1/35    1   2/35     nil/nil    nil    nil    (0)   (700) (block loc-fun (let ((alfa (+ arg 1)) (bravo (+ arg 2)) (charlie 3)) (break "break1") ...) ...)
     2/35    1   3/35     nil/nil    nil    nil    nil   nil   (progn (let ((alfa (+ arg ...)) (bravo (+ arg ...)) ...) ...) ...)
->   3/35    0-1   4/35     nil/nil    nil    nil    23    377   (let ((alfa (+ arg 1)) (bravo (+ arg 2)) (charlie 3)) (break "break1") ...)
     4/35    0-1   6/35     nil/nil    nil    nil    35    44    (+ arg 1)
     5/35    1-2   6/35     nil/nil    :early  nil    (35)  (44)  (excl::+_2op arg 1)
  level=0 pc=35 rec=3  in #P"loc-fun.cl" starting at 23 ending at 377.
Possible slide directions: (out over across back)
Current step/slide type: over/0
cl-user(4): :ldb t
[ldb] cl-user(5): (loc-fun 22)

Hit breakpoint at func = #<Function loc-fun>, pc=35
Function: #<Function loc-fun> in #P"loc-fun.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
     1/35    1   2/35     nil/nil    nil    nil    (0)   (700) (block loc-fun (let ((alfa (+ arg 1)) (bravo (+ arg 2)) (charlie 3)) (break "break1") ...) ...)
     2/35    1   3/35     nil/nil    nil    nil    nil   nil   (progn (let ((alfa (+ arg ...)) (bravo (+ arg ...)) ...) ...) ...)
->   3/35    0-1   4/35     nil/nil    nil    nil    23    377   (let ((alfa (+ arg 1)) (bravo (+ arg 2)) (charlie 3)) (break "break1") ...)
     4/35    0-1   6/35     nil/nil    nil    nil    35    44    (+ arg 1)
     5/35    1-2   6/35     nil/nil    :early  nil    (35)  (44)  (excl::+_2op arg 1)
  level=0 
Possible slide directions: (out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(6):

Note here that the :zoom output shows the lisp-breakpoint handler as next on the stack. Because of this, local information is enhanced significantly, using source-debug information as the enhancement:

[ldb-step] cl-user(6): :zo :all t
Evaluation stack:

   (excl::read-eval-print-loop :stepping :ldb-step)
   [... sys::funcall-tramp ]
   (excl::ldb-lisp-prompt-loop #<#> nil)
   [... sys::funcall-tramp ]
   (sys::lisp-breakpoint #<Function loc-fun> 35 ...)
   (sys::.lisp-breakpoint-runtime-handler "lisp_breakpoint")
 ->(loc-fun 22)
slide point set to: (block loc-fun
                      (let (# # #)
                        (break "break1")
                        (setq charlie #)
                        (let # # #))
                      (let (#)
                        (let # # #)
                        (break "break4")
                        (print alfa)))

   [... excl::%eval ]
   (eval (loc-fun 22))
   (tpl::read-eval-print-one-command nil nil)
   (excl::read-eval-print-loop :stepping :ldb)
   (tpl::ldb-command t)

... more older frames ...
[ldb-step] cl-user(7):

Note first that the information presented is the same as before (although since we've stopped before the let, none of the variables to be bound have yet become alive):

[ldb-step] cl-user(7): :loc
Compiled lexical environment:
0(Required): arg: 22
1(Local): arg: 22
[ldb-step] cl-user(8):

However, if we look at dead variables, the story is a bit different:

[ldb-step] cl-user(8): :loc :d
Compiled lexical environment:
0(Required): arg: 22
1(Local): arg: 22
2(Local): (:dead alfa): 1
3(Local): (:dead charlie): nil
4(Local): (:dead alfa): nil
5(Local): (:dead delta): nil
6(Local): (:dead bravo): *print-circle*
7(Local): (:dead alfa): 0
8(Local): (:dead bravo): 0
9(Reg/%rdi): (:dead (:param 0)): 22
10(Reg/%rsi): (:dead (:param 1)): nil
11(Local): (:dead nil): (22)
12(Local): (:dead nil): 22
13(Local): (:dead nil): nil
14(Local): (:dead nil): nil
15(Reg/%rdi): (:dead nil): 22
16(Reg/%rdi): (:dead arg): 22
17(Reg/%rdi): (:dead nil): 22
18(Reg/%rsi): (:dead nil): nil
19(Reg/%rdi): (:dead alfa): 22
20(Reg/%rdi): (:dead nil): 22
21(Reg/%rsi): (:dead nil): nil
22(Reg/%rdi): (:dead bravo): 22
23(Reg/%r13): (:dead nil): (22)
24(Reg/%r13): (:dead nil): (22)
25(Reg/%rsi): (:dead bravo): nil
26(Reg/%rdi): (:dead nil): 22
27(Reg/%r12): (:dead nil): nil
28(Reg/%r12): (:dead nil): nil
29(Reg/%rsi): (:dead charlie): nil
30(Reg/%rdi): (:dead nil): 22
31(Reg/%r13): (:dead nil): (22)
32(Reg/%rsi): (:dead arg): nil
33(Reg/%rdi): (:dead charlie): 22
34(Reg/%rdi): (:dead nil): 22
35(Reg/%rdi): (:dead nil): 22
36(Reg/%rdi): (:dead nil): 22
37(Reg/%rsi): (:dead nil): nil
38(Reg/%rdi): (:dead alfa): 22
39(Reg/%r13): (:dead nil): (22)
40(Reg/%r13): (:dead nil): (22)
41(Reg/%rsi): (:dead bravo): nil
42(Reg/%rdi): (:dead nil): 22
43(Reg/%r12): (:dead nil): nil
44(Reg/%r12): (:dead nil): nil
45(Reg/%rsi): (:dead delta): nil
46(Reg/%rdi): (:dead nil): 22
47(Reg/%rdi): (:dead alfa): 22
[ldb-step] cl-user(9):

So what is different? When at a Lisp breakpoint, any source-debug-info that the current function has is included in the data used by the :local and :set-local commands. We can see variable transitions by using print-function-meta-info (although it is not shown here because the display is huge):

(print-function-meta-info 'loc-fun :mixed t :vars t)

As breakpoints are single-stepped through and registers become alive, they are displayed in the local command, and can be changed by :set-local.

Let's skip through all of the single-stepping and break explicitly on one particular breakpoint record: record 85, which has been selected for this architecture based on the output of print-function-meta-info:

[ldb-step] cl-user(9): :br rec 85
Adding #<Function loc-fun>: 532
Function: #<Function loc-fun> in #P"loc-fun.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    83/518   0-1  84/518    nil/nil    :early  nil    686   698   (print alfa)
    84/518   0-1  85/532    nil/nil    :ref   nil    693   697   alfa
->  85/532   0-1  86/535    nil/nil    nil    :call  686   698   (print alfa)
    86/535   0-1 nil/nil    nil/nil    nil    :noreturn  nil   nil   (defun loc-fun (arg) ...)
    87/560   1-2   8/82     nil/nil    :clone  nil    (35)  (44)  (excl::+_2op arg 1)
  level=0 
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(10):

Note that this record occurs immediately after referencing the variable alfa (it is close to the end of the function). Since it is after all of the calls to :break, we'll also have to continue from each of those breaks before we can actually hit that breakpoint:

[ldb-step] cl-user(10): :step cont
; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 190 (inclusively) and 216 (exclusively)
;[form: "(+ alfa bravo charlie arg)"]:
Break: break1

Restart actions (select using :continue):
 0: return from break.
 1: Return to Debug Level 1 (an "abort" restart).
 2: Return to Top Level (an "abort" restart).
 3: Abort entirely from this (lisp) process.
[2c] cl-user(11): :cont
; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 345 (inclusively) and 375 (exclusively)
;[form: "(print (* delta echo charlie))"]:
Break: break2

Restart actions (select using :continue):
 0: return from break.
 1: Return to Debug Level 1 (an "abort" restart).
 2: Return to Top Level (an "abort" restart).
 3: Abort entirely from this (lisp) process.
[2c] cl-user(12): :cont

1440 ; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 631 (inclusively) and 659 (exclusively)
;[form: "(print (+ alfa bravo delta))"]:
Break: break3

Restart actions (select using :continue):
 0: return from break.
 1: Return to Debug Level 1 (an "abort" restart).
 2: Return to Top Level (an "abort" restart).
 3: Abort entirely from this (lisp) process.
[2c] cl-user(13): :cont

34 ; While evaluating #'loc-fun in #P"loc-fun.cl"
; between file character positions 686 (inclusively) and 698 (exclusively)
;[form: "(print alfa)"]:
Break: break4

Restart actions (select using :continue):
 0: return from break.
 1: Return to Debug Level 1 (an "abort" restart).
 2: Return to Top Level (an "abort" restart).
 3: Abort entirely from this (lisp) process.
[2c] cl-user(14): :cont

Hit breakpoint at func = #<Function loc-fun>, pc=532
Function: #<Function loc-fun> in #P"loc-fun.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    83/518   0-1  84/518    nil/nil    :early  nil    686   698   (print alfa)
    84/518   0-1  85/532    nil/nil    :ref   nil    693   697   alfa
->  85/532   0-1  86/535    nil/nil    nil    :call  686   698   (print alfa)
    86/535   0-1 nil/nil    nil/nil    nil    :noreturn  nil   nil   (defun loc-fun (arg) ...)
    87/560   1-2   8/82     nil/nil    :clone  nil    (35)  (44)  (excl::+_2op arg 1)
  level=0 
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(15):

And now that we have actually hit that breakpoint, we can examine locals:

[ldb-step] cl-user(15): :loc
Compiled lexical environment:
0(Required): arg: 22
2(Local): alfa: 10
47(Reg/%rdi): alfa: 10
[ldb-step] cl-user(16):

And if we try setting the local, it is ambigouous:

[ldb-step] cl-user(16): :set-loc alfa 12
There are multiple matching variables named alfa that make setting one value
ambiguous.  Use the local (integer) index to specify the variable to set:

2(Local): alfa: 10
47(Reg/%rdi): alfa: 10

Couldn't set the variable.
[ldb-step] cl-user(17):

So we set the register based on its local index, and verify that it has changed:

[ldb-step] cl-user(17): :set-loc 47 12
[ldb-step] cl-user(18): :loc
Compiled lexical environment:
0(Required): arg: 22
2(Local): alfa: 10
47(Reg/%rdi): alfa: 12
[ldb-step] cl-user(19):

Finally, if we step past that breakpoint, we now see that the value printed and then returned is in fact the new value we had set:

[ldb-step] cl-user(19): :step cont

12 
12
[ldb] cl-user(20):

7.0 Break on exit

The :boe command allows you to break on exit, that is to stop execution when a frame is returned from.

Here is an example of using the :boe command.

cl-user(1): (defun foo (a b) (let ((c (bar a b))) c))
foo
cl-user(2): (defun bar (a b) (break) (list a b))
bar
cl-user(3): (compile 'foo)
foo
nil
nil
cl-user(4): (compile 'bar)
bar
nil
nil
cl-user(5): (foo 1 2)
Break: call to the `break' function.

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(6): :zo
Evaluation stack:

   (break)
 ->(bar 1 2)
   (foo 1 2)
   [... excl::%eval ]
   (eval (foo 1 2))
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level
      #<terminal-simple-stream [initial terminal io] fd 0/1 @
        #x100006a7182>
      #<Function top-level-read-eval-print-loop> ...)
[1c] cl-user(7): :cont
(1 2)
cl-user(8): (foo 1 2)
Break: call to the `break' function.

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(9): :dn
Evaluation stack:

   (break)
   (bar 1 2)
 ->(foo 1 2)
   [... excl::%eval ]
   (eval (foo 1 2))
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level
      #<terminal-simple-stream [initial terminal io] fd 0/1 @
        #x100006a7182>
      #<Function top-level-read-eval-print-loop> ...)
[1c] cl-user(10): :boe
[1c] cl-user(11): :zo
Evaluation stack:

   (break)
   (bar 1 2)
*->(foo 1 2)
   (eval (foo 1 2))
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level
      #<terminal-simple-stream [initial terminal io] fd 0/1 @
        #x100006a7182>
      #<Function top-level-read-eval-print-loop> ...)
[1c] cl-user(12): :cont
Break: Returning #<1 value: (1 2)> after exiting from function.

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this (lisp) process.
[1c] cl-user(13): :zo
Evaluation stack:

   (break "Returning ~s after exiting from function." #<1 value: #>)
 ->(eval (foo 1 2))
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level
      #<terminal-simple-stream [initial terminal io] fd 0/1 @
        #x100006a7182>
      #<Function top-level-read-eval-print-loop> ...)
[1c] cl-user(14): 

8.0 :return and :restart

The top-level commands :return and :restart both attempt to restart evaluation of a broken process from the current frame (see :current). :return sequentially evaluates the arguments provided and returns their values from the current stack frame. The function associated with the current stack frame is not executed, and its arguments are not again evaluated.

The :return command is useful in situations similar to the following. Suppose that in your code you had taken the log of a value when you meant to take the exp of the value. If you make the call to log be the current stack frame, then typing

:return (exp value) 

will cause the computation to continue as if the code were correct to begin with, that is, exp of value will be calculated and returned from the current frame. The log function will not be re-executed.

:restart works on frame objects. A frame object is a list whose first element is a function and whose remaining elements are the evaluated arguments to the function. The top-level command :zoom prints a backtrace of the current execution stack. If :zoom is printing in moderate mode, it prints frame objects. (In brief mode, it simply prints the function name. In verbose mode, more than the frame object is printed.) :zoom and related commands (:find, :dn, :up, :top, etc.) all display the stack and identify one stack frame as the current stack frame. The value of the lisp variable * is set to the identified frame as part of the action of :zoom or a related command. Thus, if :zoom is printing in moderate mode, * will be set to a frame object after :zoom or a related command completes.

The argument to the :restart command must evaluate to a frame object (or nil, indicating that the current frame object should be used unchanged). Calling :restart restarts computation, replacing the current frame with the argument frame and continuing from there. Contrast this with :return which returns from a frame with a specified value (but does not re-evaluate the current frame). The two commands, :return and :restart can behave quite differently. We will give an example below.

As an example of :restart called with no arguments, suppose you define the following functions:

(defun baz (n) (bar (+ n 3)))
  (defun bar (n) (+ (goo n) 5))
  (defun foo (x) (* x x)) 

We have misspelled foo as goo in the definition of bar. If we evaluate

(baz 7)

we get an error since goo is undefined. The stack as printed by :zoom contains the frame

(bar 10)

We can then correct the definition of bar and load in just the new definition using the Emacs-Lisp interface. Next, if we make

(bar 10) 

the current frame (using :dn or :find), we can call :restart without arguments (or with nil as an argument) and the computation will continue, returning the correct answer (105).

As an example of calling :restart with an argument, consider the following. Suppose bar and foo are defined as:

(defun bar () (+ (hoo 1 2 3) (hoo 4 5 6)))
  (defun foo (a b c) (+ a b c)) 

Here, the call to hoo in bar is a misprint: it should be a call to foo. If we evaluate (bar), we again get an undefined function error. If we call :zoom (with its :all argument specified as t so all frames are printed), we see the following frame:

(excl::%eval (+ (hoo 1 2 3) (hoo 4 5 6))) 

(You may have to set the values of *zoom-print-level* and *zoom-print-length* to nil to get the entire frame without # marks or suspension points.) If we make that frame the current frame (so it is the value of cl:*) and then evaluate

:restart (subst 'foo 'hoo *) 

the frame will be re-evaluated so that foo is called rather than hoo and the correct result (21) will be returned.

The argument to :restart must evaluate to a frame object and the arguments in a frame object (the elements of the cdr) will not be further evaluated. Thus the following are not equivalent:

(+ 1 2 3 5)
  (+ 1 2 3 (- 6 1)) 

The first will evaluate correctly. The second will bomb since a list is not a legal argument to +.

One might think at first that :restart called with a frame object and :return called with a form which, when the arguments are evaluated, is the same since the frame object will have the same result. This is not, however, correct.

The bindings set up on the stack are (in some cases) handled differently by the two :restart and :return. Like many binding issues in Lisp, the example is not immediately intuitive (at least to Lisp beginners). Let us give an example and then discuss it. Consider the following functions:

(defun bar () 
  (let ((*special* 1))
    (declare (special *special*))
    (foo 5)))
(defun foo (x) 
  (let ((*special* (1+ *special*)))
    (declare (special *special*))
    (if (eq x 5) (break "sorry, bad number") *special*))) 

When we evaluate (bar), foo will be called with argument 5 and break will put Lisp into a break loop. We decide to go to the frame where foo is called and recall foo with a different argument. Here is the stack after the break. We go down two frames to reach the frame where foo is called.

;; Note: this manual is generic over several implementations.
;; The stack may differ from one implementation to another
;; so the stack you see may differ in detail from what is
;; reproduced here.
 
USER(4): (bar)
Break: sorry, bad number
 
Restart actions (select using :continue):
  0: return from break.
[1c] USER(5): :zoom
Evaluation stack:
 
  (BREAK "sorry, bad number")
->(IF (EQ X 5) (BREAK "sorry, bad number") ...)
  (LET (#) (DECLARE #) ...)
  (FOO 5)
  (LET (#) (DECLARE #) ...)
  (BAR)
  (EVAL (BAR))
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
  (TPL:START-INTERACTIVE-TOP-LEVEL
      #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @ #x162f5e>
      #<Function TOP-LEVEL-READ-EVAL-PRINT-LOOP @ #x282156>
...)
 
... more older frames ...
[1c] USER(6): :dn 2
Evaluation stack:
 
  (BREAK "sorry, bad number")
  (IF (EQ X 5) (BREAK "sorry, bad number") ...)
  (LET (#) (DECLARE #) ...)
->(FOO 5)
  (LET (#) (DECLARE #) ...)
  (BAR)
  (EVAL (BAR))
  (TPL:TOP-LEVEL-READ-EVAL-PRINT-LOOP)
  (TPL:START-INTERACTIVE-TOP-LEVEL
      #<EXCL::BIDIRECTIONAL-TERMINAL-STREAM @ #x162f5e>
      #<Function TOP-LEVEL-READ-EVAL-PRINT-LOOP @ #x282156>
...)
 
... more older frames ...
[1c] USER(7): 

Let us compare the following two commands that could be entered at this point. We show the commands and what is returned and then we explain why the returned values are different.

:return (foo 6) -> 3
  :restart '(foo 6) -> 2

Both avoid the call to break since the argument to foo is not 5. However, the :return command causes bar to return the value 3 while the :restart command causes bar to return the value 2. The binding of *special* which took place in foo before the call to break is not undone by :return. It is undone by :restart.

The moral is that you can use :return with a form argument when you know what value is expected and there are no side effects that you care or are concerned about. You should use :restart when side effects are important.

In the following example, we show :zoom displaying the evaluation stack, and show how to use some of the other stack manipulation commands.

;; Note: this manual is generic over several implementations.
;; The stack may differ from one implementation to another
;; so the stack you see may differ in detail from what is
;; reproduced here.
user(2): (defun func (x) (* x (bar x)))
func
user(3): (defun bar (x) (car x))
bar
user(4): (func 10)
Error: Attempt to take the car of 10 which is not a cons.
  [condition type: simple-error]
[1] user(5): :zo
Evaluation stack:
 
  (error simple-error :format-control ...)
->(car 10)
  (bar 10)
  (func 10)
  (eval (func 10))
  (tpl:top-level-read-eval-print-loop)
  (tpl:start-interactive-top-level
      #<excl::bidirectional-terminal-stream @ #x2a21de>
      #<Function top-level-read-eval-print-loop @ #x2d59c6>
...)
[1] user(6): :find bar
Evaluation stack:
 
  (error simple-error :format-control ...)
  (car 10)
->(bar 10)
  (func 10)
  (eval (func 10))
  (tpl:top-level-read-eval-print-loop)
  (tpl:start-interactive-top-level
      #<excl::bidirectional-terminal-stream @ #x2a21de>
      #<Function top-level-read-eval-print-loop @ #x2d59c6>
...)
[1] user(7): :return 5
;; :RETURN simply returns 5 from the frame
;; resulting in FUNC returning 50.
50
user(8): (func 10)
Error: Attempt to take the car of 10 which is not a cons.
  [condition type: simple-error]
[1] user(9): :find func
Evaluation stack:
 
  (error simple-error :format-control ...)
  (car 10)
  (bar 10)
->(func 10)
  (eval (func 10))
  (tpl:top-level-read-eval-print-loop)
  (tpl:start-interactive-top-level
      #<excl::bidirectional-terminal-stream @ #x2a21de>
      #<Function top-level-read-eval-print-loop @ #x2d59c6>
...)
[1] user(10): (defun bar (x) (car (list x)))
;; BAR is now just the identity function, but it will work
bar
[1] user(11): :error
Attempt to take the car of 10 which is not a cons.
[1] user(12): :current
(func 10)
[1] user(13): :restart
100
user(14): (defun fact (n) 
           (cond ((= 1 n) 1) (t (* n
(fact (1- n))))))
fact
user(15): (fact nil)
Error: nil is an illegal argument to =
  [condition type: type-error]
[1] user(16): :zo
Evaluation stack:
 
  (error type-error :datum ...)
->(= 1 nil)
  (cond (# 1) (t #))
  (fact nil)
  (eval (fact nil))
  (tpl:top-level-read-eval-print-loop)
  (tpl:start-interactive-top-level
      #<excl::bidirectional-terminal-stream @ #x2a21de>
      #<Function top-level-read-eval-print-loop @ #x2d59c6>
...)
[1] user(17): :pop
user(18): 

9.0 Ghost frames in backtraces


9.1 Summary of the ghost frames section

In the following backtrace, the frame [... foo] is a ghost frame. It indicates that start-fun called foo and that foo did not itself call list-length, so foo must have called something else before a call to list-length:

(error type-error :datum ...)
->(list-length 1)
  [... foo]
  (start-fun 1)
  (eval (start-fun 1))

The example that generated this backtrace is given in What kinds of optimizations cause ghost frames? below. From the example, we know that start-fun called foo, which called bar, which called baz, which called list-length, and looking at the backtrace, we know that calls to foo, bar, and baz have been optimized out, but the system can still detect that foo must have been called.

The remainder of this section contains the following headings:


9.2 What is a ghost frame?

Typically, Lisp executes a series of function calls. It maintains a stack of function call frames that show the sequence of functions, each calling the next, that resulted in the current state of Lisp. However, compiled code may cause the frames associated with functions actually called to be removed from the stack. Doing so makes code run faster but means that a backtrace (displayed by the top-level commands :zoom and :bt) does not show calls to some functions actually called, thus making debugging more difficult.

The debugger will where possible insert frames into a backtrace to indicate either that a specific function was called but optimization has removed the associated frame from the stack or that some function was called and removed from the stack by optimization although the debugger cannot determine exactly which function.

These inserted frames are called ghost frames. They are indicated in moderate or verbose printed backtraces by being surrounded with brackets ([]). Consider the following backtraces. The first shows the interpreted code. The second shows compiled code at maximum optimization. The [... foo] frame in the second backtrace is a ghost frame, indicating that there was a call to foo whose associated frame is no longer on the stack. The suspension points (...) further indicate that other functions were called whose associated frames have been removed from the stack, although the debugger cannot determine which ones (we of course know from the interpreted backtrace that the missing functions are bar and baz):

; Interpreted code backtrace:
  (error type-error :datum ...)
->(list-length 1)
  (baz 1)
  (bar 1)
  (foo 1)
  (start-fun 1)
  (eval (start-fun 1))
 
; Compiled code backtrace:
  (error type-error :datum ...)
->(list-length 1)
  [... foo]
  (start-fun 1)
  (eval (start-fun 1))

9.3 What kinds of optimizations cause ghost frames?

Frames get removed from the stack by a compiler optimization called non-self tail merging. Consider the following example (which generated the backtraces shown above when compiled with speed 2, debug 1):

(defun start-fun (x) (foo x) nil)
(defun foo (x) (print x) (bar x))
(defun bar (x) (princ x) (baz x))
(defun baz (x) (pprint x) (list-length x))

The error for which the backtrace is displayed above occurs when the form (start-fun 1) is evaluated. 1 is not a legal argument to list-length (which expects a list) and so an error is signaled.

This is a pretty trivial example but it is not uncharacteristic of real code. foo, bar, and baz each accepts an argument, does something with the argument, and then passes the argument to the next function. foo, bar, and baz each has the property that once the system has reached the last form, nothing further is required of the function.

Thus, foo calls print and then calls bar. foo is going to return whatever bar returns and there are no possible side effects that are not caused by bar. At the point where foo calls bar, there is no reason for a frame associated with foo to be on the stack, except for debugging. Indeed, keeping the frame on the stack causes the stack to grow more than is absolutely necessary and, when bar returns, causes extra instructions (to exit from foo) to be executed. Things would run faster if the frame associated with foo were simply removed from the stack and the system acted as if start-fun had called bar directly (the print called for in foo has already been completed by the time bar is called).

The compiler will optimize this code to remove frames as described as long as the compiler optimization qualities safety, space, speed, and debug have values such that the switch tail-call-non-self-merge-switch is either a function returning true or is itself true. In that case, the compiler will generate code which will make the stack look as if the functions were defined as follows:

(defun start-fun (x)
  (foo x)
  (bar x)
  (baz x)
  (list-length x))
(defun foo (x) (print x))
(defun bar (x) (princ x))
(defun baz (x) (pprint x))

This code will run faster and suppress unnecessary stack growth but the stack will not reflect the actual sequence of function calls. Thus, when start-fun is called with the argument 1, list-length will signal an error because its argument is not a list and the backtraces shown above will result.

Compiler optimization switches in general and tail-call-non-self-merge-switch in particular are described in compiling.html.


9.4 How does the debugger know about ghost frames?

Since all references to foo, bar, and baz in our example have disappeared from the stack by the time Lisp enters a break loop (when list-length signals an error), the information on the stack is insufficient to print ghost frames. One piece of information, is available, however: the location to which control should return in start-fun when list-length has completed. The debugger can access that return location and can (assuming the disassembler has been loaded) disassemble the code for the function named by start-fun. Looking at that disassembly, the debugger can determine that the return location is just after a call to foo.

Therefore, it knows that foo was called but the foo frame has been optimized off the stack. From that information, the debugger generates the foo ghost frame.

The debugger also knows that some function must have called list-length (since the list-length frame is still on the stack). Examining the disassembly of foo shows that foo could not have called list-length itself and so the debugger also knows that at least one other function must have been called between foo and list-length (we know that bar and baz were called). Therefore, the debugger puts the suspension points in the ghost frame to indicate the missing ghost frames ([... foo]). However, the debugger is not able to tell which function called by foo called list-length.


9.5 When will the debugger display ghost frames?

In order for the debugger to determine that frames have been optimized off the stack, it must have access to the disassembler. The disassembler in Allegro CL is not included as part of the base system. Instead, it is loaded when needed, typically when the function disassemble is called.

When the disassembler is loaded, the system adds the string "disasm" to the list that is the value of *modules*. The debugger will not load the disassembler just to print ghost frames. So, when the disassembler is loaded, the debugger will print ghost frames in backtraces. When the disassembler is not loaded, the debugger will not print such frames. In that case, the following message will be printed at the end of the backtrace:

(to see any ghost frames, the disassembler must be loaded)

9.6 Can I return from or restart a ghost frame?

The short answer is no. Various debugger commands in Allegro CL operate on the current frame. These include :restart, :return, :current, :local, etc. Since ghost frames are not really on the stack, applying these commands to a ghost frame does not make sense. In fact, it is not possible to make a ghost frame the current frame. Here is the backtrace shown above:

  (error type-error :datum ...)
->(list-length 1)
  [... foo]
  (start-fun 1)
  (eval (start-fun 1))

The arrow (->) indicates the current frame is the list-length frame. If you call the command :dn (which moves the current frame down one frame on the stack), the display changes as follows:

  (error type-error :datum ...)
  (list-length 1)
  [... foo]
->(start-fun 1)
  (eval (start-fun 1))  

That is, the ghost frame is skipped and the next real frame, the start-fun frame, becomes the current frame.


9.7 What do the suspension points (...) mean in a ghost frame?

The backtrace shown just above indicates the ghost frame [... foo]. The suspension points indicate that other functions were called between foo and list-length, although the debugger does not know what these functions are.

As described under the heading How does the debugger know about ghost frames? above, the debugger can sometimes tell that a ghost frame belongs in the stack and that the function associated with the ghost frame cannot have called the function associated with the next real frame on the stack. Thus, in our example, the debugger can tell that a foo frame must have been on the stack (so it adds a ghost frame) and can further tell foo did not call list-length (and so adds suspension points to the ghost frame).


9.8 The ghost frame has no ...'s; are all possible frames displayed?

We have already said that suspension points (...) indicate that other functions were called between the ghost frame and the next real frame. However, if there are no suspension points, you cannot immediately conclude that no other functions were called.

Consider the following modification of our example above. Instead of defining foo this way:

(defun foo (x) (print x) (bar x))

we define it as follows:

(defun foo (x) (let ((y (list-length (list x)))) (print y)) (bar x))

(Again, a trivial example since y is obviously 1.) We compile foo again and again evaluate (start-fun 1). Again, we get an error (from the call to list-length in baz) and the following backtrace is displayed:

  (error type-error :datum ...)
->(list-length 1)
  [foo]
  (start-fun 1)
  (eval (start-fun 1))

Here, the ghost frame for foo has no suspension points even though we know that foo called bar called baz called list-length, where the error occurred. However, because foo itself calls list-length (for a different purpose), the debugger could not conclude that foo did not call list-length with an argument which resulted in an error.

Therefore, suspension points indicate missing ghost frames for sure. No suspension points does not by itself mean there are no missing ghost frames.


9.9 No ghost frames are displayed. Do all functions appear on the stack?

First, some functions are hidden by the system from backtraces to avoid unnecessary clutter. These functions are printed when :zoom is called with arguments :all t.

However, ghost frames may also be missing in cases similar to our example. Suppose start-fun, which was originally defined (in The ghost frame has no ...s; are all possible frames displayed?) above:

  (defun start-fun (x) (foo x) nil)

was instead defined as follows:

  (defun start-fun (x) (funcall 'foo x) nil)

Again, we evaluate (start-fun 1) and do a :zoom. The following backtrace is printed:

  (error type-error :datum ...)
->(list-length 1)
  (start-fun 1)
  (eval (start-fun 1))

The moral of this and the information under the previous heading, therefore, is that printed information is always guaranteed but missing information should not be depended upon to accurately reflect the situation.


9.10 Ghost frames in a brief backtrace

Calling :zoom with arguments :brief t or calling the :bt command prints an abbreviated backtrace. Here is roughly what is printed when :bt is called after evaluating (start-fun 1). The original definitions of foo and start-fun are in What kinds of optimizations cause ghost frames? above. Note that you may see additional frames.

list-length <-
[... foo] <- start-fun <- eval <- tpl:top-level-read-eval-print-loop <-
tpl:start-interactive-top-level

(This backtrace includes frames for functions below start-fun in the stack (such as eval) that were left out in the backtraces printed above.)


9.11 Can I turn off printing of ghost frames?

If the disassembler is not loade and so "disasm" is not on the cl:*modules* list, ghost frames will not be printed. However, if that string is on *modules*, ghost frames will be printed. The debugger will always print ghost frames if it can. Load the disassembler with

(require :disasm)

There is no way to unload the disassembler. It is typically already loaded at startup in development images. (See the discussion of the include-devel-env keyword argument to build-lisp-image in Arguments to build-lisp-image 1 in building-images.html.)


9.12 Can backtraces involving system functions have ghost frames?

Yes, they can. Many system (i.e. predefined) functions in Allegro CL are compiled at a settings of speed and debug which cause tail-call-non-self-merge-switch to be true (or if the switch itself has a non-functionp true value. Therefore, it is possible that ghost frames can appear identified with symbols naming system functions.


10.0 The tracer

The tracer provides a way to track or trace when functions are called. For example, when tracing a function, a message is printed upon entering and exiting the function.

The tracer is invoked at the top level using :trace and turned off using :untrace. The tracer can also be invoked and exited using the macros trace and untrace, which have the same argument syntax as their top-level command counterparts.

The output from :trace is designed to be readable - a function being traced may be called many times, and the entrance and exit from each instance should be obvious, by the numbers at the beginning of the lines and the indentation of the lines printed by the traced function. Also printed at the beginning is a number in brackets, such as [1]. This number indicates the process and can be associated with an actual process by looking at the information printed by the :processes top-level command, in the Bix (Bindstack Index) column.

A couple of notes on tracing in the IDE

:trace prints function names with a package qualifier for functions on symbols not accessible in the current package. The full package name will be printed unless the variable *print-nickname* is t, in which case the package nickname is printed.

The following are valid options to :trace:

Option Arguments Description
:condition expr Trace this function if expr evaluates to a true value.
:break-before val The expression val is evaluated just before entering a function. If val evalates to a non-nil value, then a new break level is entered. Otherwise, execution continues. When used in combination with :inside and :not-inside, breaks only occur when the :inside and :not-inside conditions are satisfied.
:break-after val The expression val is evaluated just after exiting a function. If val is t, then a new break level is entered. Otherwise, execution continues. When used in combination with :inside and :not-inside, breaks only occur when the :inside and :not-inside conditions are satisfied.
:break-all val The expression val is evaluated just before entering a function and just after exiting a function. If val is t, then a new break level is entered. Otherwise, execution continues. When used in combination with :inside and :not-inside, breaks only occur when the :inside and :not-inside conditions are satisfied.
:inside func Trace this function if Lisp is currently inside a call of the function func.

func may also be a list of functions, optionally starting with the symbols cl:and or cl:or. If neither symbol or cl:and starts the list (e.g. (cl:and foo1 bar2) or (foo1 bar2), tracing is done when the function to be traced has been called directly or indirectly by all the functions in the list (by foo1 and bar2 in the example). If cl:or starts the list (e.g. (cl:or foo1 bar2)) tracing is done when the function to be traced has been called directly or indirectly by any of the functions in the list (by foo1 or bar2 in the example).

:inside works in combination with :not-inside, described next. Both must be satisified for tracing to occur.

For example, (trace (deeper :inside deep)) would trace the function deeper only when called from within a call to deep. (trace (deeper :inside deep :not-inside (cl:or foo1 bar2))) would trace the function deeper only when called from within a call to deep but not within a call to foo1 or bar2. See Note on inside and not inside for a fuller description of a call being inside another.

:not-inside func Trace this function if Lisp is not currently inside the evaluation of the function func.

func may also be a list of functions, optionally starting with the symbols cl:and or cl:or. If neither symbol or cl:and starts the list (e.g. (cl:and foo1 bar2) or (foo1 bar2), tracing is done when the function to be traced has not been called directly or indirectly by all the functions in the list (by foo1 and bar2 in the example). If cl:or starts the list (e.g. (cl:or foo1 bar2)) tracing is done when the function to be traced has not been called directly or indirectly by any of the functions in the list (by foo1 or bar2 in the example).

:not-inside works in combination with :inside, described above. Both must be satisified for tracing to occur.

For example, (trace (deeper :not-inside deep)) would trace the function deeper except when called from within a call to deep. (trace (deeper :not-inside deep :inside (cl:or foo1 bar2))) would trace the function deeper except when called from within a call to deep and within a call to foo1 or bar2. See Note on inside and not inside for a fuller description of a call being inside another.

:print-before expr expr should either be a single object or a list of objects which will be evaluated. The results will be printed before entering the function.
:print-after expr expr should either be a single object or a list of objects which will be evaluated. The results will be printed after leaving the function.
:print-all expr expr should either be a single object or a list of objects which will be evaluated. The results will be printed before entering and after leaving the function.
:show-stack n Causes stack frames (as if approximately printed by :zoom with :moderate t and :function nil as arguments) to be shown just before the function being traced is entered. n should a positive integer, t, or nil. If a positive integer, that many stack frames will be shown. n can also be t, which requests all frames on the stack to be printed; or nil, which turns off showing the stack. See the example below.

Note on inside and not inside

A call to function foo is inside a call to function bar if bar appears on the stack when foo is called. bar can call foo directly, meaning there is an explicit call to foo in the code defining bar, or indirectly, meaning bar calls another function which (perhaps calls more itermediate functions) which calls foo directly.

There are a few special cases. One is caused by tail-merging. Thus, if bar calls baz which does a tail-merged call to foo, then foo is considered inside bar but not inside baz.

The other special case is generic functions. If a method is on the stack, its generic function is also considered to be on the stack, even though it never will be seen there (meaning you can specify the generic function rather than the specific method). Thus the following all work for tracing foo inside a call to device-read called on a terminal-simple-stream, but the first will catch calls to foo inside any device-read method.

:trace (foo :inside device-read)
:trace (foo :inside #'(method device-read (terminal-simple-stream t t t t)))
:trace (foo :inside ((method device-read (terminal-simple-stream t t t t))))

Stopping tracing and trace output

Tracing of individual objects or all tracing can be stopped with the :untrace command.

The value of *trace-output* is the where :trace sends output, which is normally *terminal-io*. *print-level*, *print-length*, *print-array*, and *print-circle* are bound to *trace-print-level*, *trace-print-length*, *trace-print-array*, and *trace-print-circle* during trace output. *print-long-string-length* is bound to the value of *trace-print-long-string-length* during trace output.

:show-stack example

cl-user(1): :tr (car :show-stack 10)
(car)
cl-user(2): (car '(a b))
 0[2]: (car (a b))
 0^      <- (system::..runtime-operation (a b))
 0^      <- (eval (car '(a b)))
 0^      <- (top-level::read-eval-print-one-command nil nil)
 0^      <- (excl::read-eval-print-loop :level 0)
 0^      <- (top-level::top-level-read-eval-print-loop1)
 0^      <- (top-level:top-level-read-eval-print-loop)
 0^      <- ((:runsys system::lisp_apply))
 0^      <- (top-level:start-interactive-top-level
               #<terminal-simple-stream [initial terminal io] fd 0/1 @
                 #x4029b84a>
               #<Function top-level-read-eval-print-loop> nil)
 0^      <- (excl::start-lisp-execution t)
 0[2]: returned a
a
cl-user(3): 

10.1 Tracing function objects

trace and :trace may be passed function names but not function objects. Allegro CL allows specifying function objects to be traced using the ftrace and funtrace functions.

Each function takes a function specification as an argument. The function specification can be a function name (as with trace and untrace) or a function object (which is not accepted by trace and untrace).

untrace and :untrace called with no arguments stop all tracing, including tracing started by ftrace with a function object as an argument, but you should use funtrace to stop tracing of a function object if you want some tracing to continue.

Here is an example:

cl-user(1): (defun foo () (break "hello"))
foo
cl-user(2): (foo)
Break: hello

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this process.
[1c] cl-user(3): :zo
Evaluation stack:

   (break "hello")
 ->(foo)
   (eval (foo))
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level
      #<terminal-simple-stream [initial terminal io] fd 0/1 @
        #x7115d9da>
      #<Function top-level-read-eval-print-loop> ...)
[1c] cl-user(4): :func
#<Interpreted Function foo>
[1c] cl-user(5): (ftrace *)
#<Interpreted Function foo>
[1c] cl-user(6): :prt
cl-user(7): (foo) ;; :prt evaluation
 0: (foo)
Break: hello

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this process.
[1c] cl-user(8): :cont
 0: returned nil
nil
cl-user(9): (funtrace #'foo)
#<Interpreted Function foo>
cl-user(10):

10.2 Trace example

In the following example, we trace the factorial function and then give an example of tracing one function inside another.

;; Note: this manual is generic over several implementations.
;; The stack may differ from one implementation to another
;; so the output you see may differ in detail from what is
;; reproduced here.
cl-user(38): (defun fact (n)
               (cond ((= n 1) 1)
                     (t (* n (fact (1- n))))))
fact
cl-user(39): (fact 4)
24
cl-user(40): :trace fact
(fact)
cl-user(41): (fact 4)
 0[1]: (fact 4)
   1[1]: (fact 3)
     2[1]: (fact 2)
       3[1]: (fact 1)
       3[1]: returned 1
     2[1]: returned 2
   1[1]: returned 6
 0[1]: returned 24
24
cl-user(42): (defun deep (x) (deeper (list x)))
deep
cl-user(43): (defun deeper (x) (format t "~&~s~%" x))
deeper
cl-user(44): (deep 10)
(10)
nil
cl-user(45): :tr (deeper :inside deep)
(deeper)
cl-user(46): (deeper 10)
10
nil
cl-user(47): (deep 10)
 0[1]: (deeper (10))
(10)
 0[1]: returned nil
nil
cl-user(48): :tr (deeper :break-before t)
(deeper)
cl-user(49): (deep 10)
 0[1]: (deeper (10))
Break: traced call to deeper

Restart actions (select using :continue):
 0: return from break.
 1: Return to Top Level (an "abort" restart).
 2: Abort entirely from this process.
[1c] cl-user(50): :zo
Evaluation stack:

   (break "traced call to ~s" deeper)
 ->(deep 10)
   (eval (deep 10))
   (tpl:top-level-read-eval-print-loop)
   (tpl:start-interactive-top-level
      #<terminal-simple-stream [initial terminal io] fd 0/1 @ #x4095faa>
      #<Function top-level-read-eval-print-loop> ...)
[1c] cl-user(51): :cont
(10)
 0[1]: returned nil
nil

;;  The [1] that appears in the trace output identified the process
;;  being traced. It can be associated with an actual process
;;  by looking at the output of the :processes top-level
;;  command (nicknamed :proc), under the Bix (bindstack index)
;;  column. We see from the output it is the Initial 
;;  Lisp Listener.
cl-user(52): :proc
P Bix Dis   Sec  dSec  Priority  State   Process Name, Whostate, Arrest
*   1  16   279   4.5         0 runnable Initial Lisp Listener
*   3   0     0   0.0         0 waiting  Connect to Emacs daemon,
                                           waiting for input
*   4   0     0   0.0         0 inactive Run Bar Process
*   6   0     2   0.0         0 waiting  Editor Server, waiting for input
cl-user(53): 

;;  In the next example, we define functions that call other
;;  functions after printing messages about what they are calling
;;  what called them. This illustrates the :inside and :not-inside
;;  options.

cl-user(53): (defun foo ()
          (progn (format t "foo calling bar~%") (bar 'foo))
          (progn (format t "foo calling baz~%") (baz 'foo))
          (progn (format t "foo calling bzz~%") (bzz 'foo)))
foo
cl-user(54): (defun bar (from)
          (progn (format t "bar calling baz from ~S~%" from)
             (baz 'bar))
          (progn (format t "bar calling bzz from ~S~%" from)
             (bzz 'bar)))
bar
cl-user(55): (defun baz (from)
          (progn (format t "baz calling bzz from ~S~%" from)
             (bzz 'baz)))
baz
cl-user(56): (defun bzz (from)
          (format t "bzz called from ~S~%" from))
bzz

;;  Here is an untraced call to FOO:
cl-user(57): (foo)
foo calling bar
bar calling baz from foo
baz calling bzz from bar
bzz called from baz
bar calling bzz from foo
bzz called from bar
foo calling baz
baz calling bzz from foo
bzz called from baz
foo calling bzz
bzz called from foo
nil

;;  Here we trace BZZ when called inside both FOO and BAZ (:inside
;;  followed by a list contains an implicit AND, so the call
;;  must be inside all listed functions):
cl-user(58): :trace (bzz :inside (foo baz))
(bzz)
cl-user(59): (foo)
foo calling bar
bar calling baz from foo
baz calling bzz from bar
 0[1]: (bzz baz)
bzz called from baz
 0[1]: returned nil
bar calling bzz from foo
bzz called from bar
foo calling baz
baz calling bzz from foo
 0[1]: (bzz baz)
bzz called from baz
 0[1]: returned nil
foo calling bzz
bzz called from foo
                     ;; this call to BZZ is not traced since
                     ;; it is not inside BAZ.
nil
cl-user(60): :untrace   ;;  We remove all tracing
nil

;;  Now tracing will happen when inside FOO but not inside BAR:
cl-user(61): :trace (bzz :inside foo :not-inside bar)
(bzz)
cl-user(62): (foo)
foo calling bar
bar calling baz from foo
baz calling bzz from bar
bzz called from baz           ;;  inside BAR so not traced.
bar calling bzz from foo
bzz called from bar
foo calling baz
baz calling bzz from foo      ;;  not inside BAR
 0[1]: (bzz baz)
bzz called from baz
 0[1]: returned nil
foo calling bzz
 0[1]: (bzz foo)
bzz called from foo           ;;  Again, not inside BAR
 0[1]: returned nil
nil
cl-user(63): 

Tracting compiled code

Note that the factorial function is recursive. When traced interpreted, all the calls made during the evaluation are shown. But the behavior when compiled may be different since on some architectures, self-calls by recursive functions are compiled in such a way that they avoid going through the normal function calling sequence (which is not necessary on such platforms, and so unnecessary instructions can be avoided). Because since the tracer catches function calls from the normal function calling sequence, it will not see most of the recursive self calls and it will look like the function was called only once. See the Why doesn't tracing a self-calling function trace the inner calls? FAQ iten in the Allegro CL FAQ for more information. Compilation may also affect trace output in other ways. You always get maximum information by tracing interpreted code.

CLOS methods can be traced by name. Here is an example.

user(16): (defmethod my-function ((x integer))
            (cons x :integer))
#<standard-method my-function (integer)>
user(17): (my-function 1)
(1 . :integer)
user(18): (trace ((method my-function (integer))))
(method my-function (integer)))
user(19): (my-function 1)
0: ((method my-function (integer)) 1)
0: returned (1 . :integer)
(1 . :integer)
user(20): (untrace (method my-function (integer)))
((method my-function (integer)))
user(21): (my-function 1)
(1 . :integer)
user(22):

10.3 Tracing setf, :before, and :after methods and internal functions

Methods and internal functions are typically named by function specs (see Function specs (fspecs) in implementation.html). This section describes how to trace such things, concetrating on setf, :before and :after methods and internal functions (such as those defined by flet or labels).

Here is how to trace setf, :before and :after methods. Note that the methods must be defined before they can be traced (we have not done that -- the examples simply show the format of the calls to trace and untrace):

(trace ((method (setf slot-1) (t baz))))
(trace ((method foo :before (integer))))
(trace ((method foo :after (integer))))

The extra set of parentheses is required to avoid confusion with specifying trace options (they are specified with a list whose car is the function to be traced and whose cdr is a possibly empty list of options). Note that the extra set of parentheses is not used with untrace:

(untrace (method (setf slot-1) (t baz)))
(untrace (method foo :before (integer)))
(untrace (method foo :after (integer)))

A generic function itself can be traced exactly like any other function.

Here is an example tracing an internal function, defined in a labels or flet. Note that the form containing the call to labels or flet must be compiled.

cl-user(1): (defun myfunc (a b c)
              (labels ((cubit (arg) (expt arg 3)))
                (if (> a b)
                    (+ (cubit a ) (cubit c))
                  (+ (cubit b) (cubit c)))))
myfunc
cl-user(2): (compile 'myfunc)
myfunc
nil
nil
cl-user(4): (trace ((labels myfunc cubit)))
((labels myfunc cubit))
cl-user(5): (myfunc 1 2 3)
 0: ((labels myfunc cubit) 2)
 0: returned 8
 0: ((labels myfunc cubit) 3)
 0: returned 27
35
cl-user(6):

Here is another example. Again, the form must be compiled.

;;; File foo.cl:

(in-package :cl-user)
(defclass thing ()
  ((s1 :initform 1 :initarg :s1 :accessor s1)
   (s2 :initform 2 :initarg :s2 :accessor s2)
   (s3 :initform 3 :initarg :s3 :accessor s3)))

(defmethod doit ((arg thing))
  (labels ((cubit (arg) (expt arg 3)))
             (if (> (s1 arg) (s2 arg))
                 (+ (cubit (s1 arg) ) (cubit (s3 arg)))
              (+ (cubit (s2 arg)) (cubit (s2 arg))))))

;;; End of file foo.cl:

cl-user(1): :ld foo.cl
; loading /home/user1/foo.cl
cl-user(2): (trace ((labels (method doit (thing)) cubit)))
Error: `(labels (method doit (thing)) cubit)' is not fbound
 [condition type: undefined-function]

restart actions (select using :continue):
0: return to Top Level (an "abort" restart).
1: Abort entirely from this process.

        ;;;  The form must be compiled so that internal functions
        ;;;  can be traced.

[1] CL-USER(3): :res
cl-user(4) :cload foo.cl
;;; Compiling file foo.cl
;;; Writing fasl file foo.fasl
;;; Fasl write complete
cl-user(5): (trace ((labels (method doit (thing)) cubit)))
((labels (method doit (thing)) cubit))
cl-user(6): (setf thing1 (make-instance 'thing))
#<thing @ #x7150fac2>
cl-user(7): (doit thing1)
 0: ((labels (method doit (thing)) cubit) 2)
 0: returned 8
 0: ((labels (method doit (thing)) cubit) 2)
 0: returned 8
16
cl-user(8): 

10.4 The memlog tracer

The memlog module is a trace facility which stores data in an internal data structure rather than printing it. This can be useful in any Lisp but is particularly useful in an SMP lisp as it mitigates the problem of coordinating output and the problem of affecting operation because of the I/O cost. It is documented in smp.html in the section Memlog: A Facility for Minimally Intrusive Monitoring of Complex Application Behavior.


11.0 The original stepper

The stepper described in this section is triggered by the step macro and associated tools. There is also a separate, modern stepper, the Lisp DeBug (LDB) stepper, which is described below in two sections. The first section, The Lisp DeBug (ldb) stepper describes the LDB stepper itself, and the second section, The source stepper describes its source stepping variant. The LDB stepper is not related to the original stepper described in this section and its subsections. We recommend that users consider using that more modern tool before using the original stepper.

The stepper allows the user to watch and control the evaluation of Lisp expressions, either inside certain functions or over certain expressions. When stepping is turned on, evaluation of all expressions is done in single-step mode - after evaluating one form, a step read-eval-print loop is entered, from which the user may continue or abort.

It is only useful to step through compiled code. You cannot effectively step through interpreted code. (Doing so results in stepping through the interpreter itself, with hundreds of uninteresting steps). Further, see verify-funcalls-switch. funcall'ed functions in certain cases in compiled code will not be caught by the stepper if the call was compiled with that compiler switch false.

If you ever see the entry

<excl::interpreted-funcall ...>

immediately enter the command :sover.

If you instead take another step (by hitting Return, for example), get out of stepping by entering

[step] cl-user(20): :step nil
[step] cl-user(21): :sover

and then you can start stepping afresh.

The :step top-level command is similar to the standard Common Lisp macro step.

With no arguments or an argument of nil, :step turns off stepping. With an argument of t, stepping is turned on globally. Otherwise the arguments must be symbols naming functions, and stepping is done only when inside one of the functions given to :step.

Once stepping is turned on, the top level recognizes three more commands: :scont, :sover, and carriage return (which is a synonym for :scont 1). Also, the top-level prompt for read-eval-print loops when stepping is enabled is prefixed with [step], as a reminder that the above step commands are available.


11.1 Turning stepping off

Entering :step nil turns off stepping. You may have to then enter :sover to end the current stepping through a function. Turning off stepping is very useful when you accidently start stepping through an interpreted function.

user(45): (defun fact (n) (if (= n 1) 1 (* n (fact (1- n)))))
fact
user(46): :step fact
user(47): (fact 3)
 1: (fact 3)

[step] user(48): 
  2: (excl::interpreted-funcall #<Interpreted Function fact> 1 32864546 0)

    ;; :sover here would get you out

[step] user(48): 
   3: (excl::extract-special-decls ((block fact (if # 1 #))))

    ;; it is now too late. Turn stepping off.

[step] user(48): :step nil
[step] user(49): :sover
   result 3: nil  ((block fact (if (= n 1) 1 (* n #))))
  result 2: 6
 result 1: 6
6
user(50): 

11.2 Other stepping commands and variables

Command Arguments Description
:scont n Continue stepping, for n expressions, beginning with the evaluation of the last expression printed by the stepper. If n is not provided, it defaults to 1.
:sover Evaluate the current expression in normal, non-stepping mode.

*print-level* and *print-length* are bound to *step-print-level* and *step-print-length* during stepper output. *print-long-string-length* is bound to the value of *step-print-long-string-length* during stepper output.


11.3 Stepping example

The example below demonstrates the use of the stepper. In it, we define fact, the factorial function, and then step through it. We use carriage returns for each single step. Notice particularly the difference between stepping through fact when it runs interpreted and when it runs compiled.

;; Note: this manual is generic over several implementations.
;; The stack may differ from one implementation to another
;; so the output you see may differ in detail from what is
;; reproduced here.
 
user(3): (defun fact (n) 
           (if (= n 1) 1 (* n (fact (1- n)))))
fact
user(4): (fact 3)
6
user(5): (compile 'fact)
fact
user(6): :step nil
                      ;; we turn all stepping off
user(7): (step fact)
;; Here we use the function form of step.
;; It has the same effect as :step fact.

user(8): (fact 3)
 1: (fact 3)

[step] user(9): 
  2: (excl::*_2op 2 1)

[step] user(9): 
  result 2: 2
  2: (excl::*_2op 3 2)

[step] user(8): 
  result 2: 6
 result 1: 6
6
user(9): 

12.0 The Lisp DeBug (ldb) stepper

The ldb stepping facility allows the user to place breakpoints at various instruction-level locations. Note that the ldb-stepping functionality may not be included in the running image. Evaluate (require :lldb) to ensure that the functionality is loaded before trying to use the facility.

Note there are two steppers in Allegro CL, the original stepper (associated with the step macro and described in the section The original stepper above), and the LDB stepper described in this section, which has a source stepper variant described in the section The source stepper below. We recommend that users start with the LDB stepper and the source variant, which are more powerful that the original stepper.

The ldb stepper utilizes several top-level commands to create the stepping environment.

They are:

:ldb Controls the installing (enabling) and uninstalling (disabling) of breakpoints, and the establishment of ldb mode.
:break Allows the user to add or delete breakpoints.
:step When in ldb-step mode, the step command recognizes several sub-commands as its first argument. See the full description for more information.
:local When a breakpoint is hit, the default current frame is the register context in which the breakpoint occurred (and thus ldb-step mode was entered). See the full description for more information.
:register At a breakpoint, print the saved context (ie its registers) for the current frame. See the full description for more information.
[return] When in ldb-step mode, causes the previous step subcommand to be re-executed. If there was no previous step subcommand, a :step over is assumed.

12.1 Breakpoints vs slide points

Breakpoints

A Lisp breakpoint is a stopping point for execution of Lisp functionality. It is represented by a structure which carries information, including the function it represents and the relative program counter within that function. It may be installed (causing execution of the function to be diverted to the breakpoint facility) or uninstalled (causing the breakpoint to have no effect on executuon of the function).

Every breakpoint designates a language. The two languages that are currently supported are :asm (which steps instruction-by-instruction) and :lisp (which steps form-by-form. Other languages can be added, and as a proof-of-concept the :python language was added by Willem Broekema and can be loaded from cl-python. This language front-end steps line-by-line.

Asm breakpoints

The :asm language personality uses the basic breakpoint structure, and other languages can include this structure in their own larger structures. The breakpoint structure includes slots common to all breakpoints, which include function and pc values which identify the breakpoint, and also next-pc, branch-pc, branch-type, and other internal and external slots which describe or maintain the state of the breakpoint.

Lisp breakpoints

A Lisp breakpoint is also called a Lisp source record. It includes the breakpoint struct and has all of its slots, and in addition it has several slots which describe whatever expression the Lisp breakpoint is representing. Unlike the asm breakpoint, whose function and pc slot uniquely identify the instruction it replaces, multiple Lisp breakpoints can have the same function and pc designation, yet can remain distinct. Source record structs are built by the Lisp compiler and is made available in the fasl file via the save-source-level-debug-info-switch and is loaded when *load-source-file-info* is true.

When possible, a Lisp breakpoint will contain position information about its source location. As with the pc location, position information isn't always unique (e.g. a macro form may be expanded in place to another form, and each of those forms will have its own record but will have the same start and end character positions).

Selection of Lisp breakpoints

Because Lisp breakpoints have unique record numbers, they may be selected absolutely via their record number, which is an index into the array of Lisp records in the function's source record vector. But it is also possible to select a Lisp breakpoint using either the pc value or the start position, although it is not possible to map either characteristic to a particular record; the selection process is done algorithmically according to the need. With the :break top-level-command, the :rec option will select the precise breakpoint by record, but the default :pc option will tend to select the record that is the closest to the source form, and the :pos option will select the outermost record in a macro-expansion group of forms.

Slide points and sliding

Because of the slight ambiguity of selecting Lisp breakpoints, sliding was invented. This is a mechanism for navigating the source record set with accuracy. It also allows position information to be conveyed to the user even when not currently stepping through breakpoints. The Lisp has a single slide point, which is a cursor of sorts which when non-nil holds a Lisp source record. It is usually transient, and gets set whenever:

  1. the :break top-level command is used to set a breakpoint,

  2. the :break top-level command is used with the :slide option to only slide to that breakpoint,

  3. a breakpoint is encountered during execution,

  4. the :zoom command shows the current frame and the current function has source debug info available, or

  5. the :slide top-level command is used to move the slide point.

If :zoom output shows "slide point set to ..." just after the current frame, typing :slide point or :slide here will display the current slide point, which may have position information in it. See the :zoom example for an example.


12.2 Entering and Exiting the ldb stepper

Before starting ldb-mode stepping, be sure that the functionality is loaded by evaluating:

(require :lldb)

Entering ldb-mode can then be done in one of two ways:

  1. Use the :ldb t command to start debugging

  2. Use the with-breakpoints-installed macro around a form.

Either method allows breakpoints to be hit by installing them. If the :break command is used after the :ldb command, any new breakpoints will be properly installed.

Once a break is hit, a message is printed out, and ldb-step mode is entered. In this mode, breakpoints are not installed, but the top-level is primed to do fast installation/deinstallation/stepping of the breakpoints with little keyboard input.


12.3 Ldb stepper functional interface

The ldb stepper provides the following functional interface.

add-breakpoint Adds a breakpoint.
delete-breakpoint Deletes an existing breakpoint.
with-breakpoints-installed Evaluates a body with breakpoints installed during the evaluation.

12.4 Ldb stepping example run

This example run is on a linux x86 version. The output on different platforms will be different. Comments are added within ;[ ]:

cl-user(1): (require :lldb)
Fast loading lldb.fasl
cl-user(2): :br count 17 ;[add a breakpoint (but do not install)]
Adding #<Function count>: 17
17: 89 45 dc movl [ebp-36],eax ; item
cl-user(3): (count #\a "abacad")
3 ;[no breakpoints had been installed]
cl-user(4): :ldb t ;[start things out]
[ldb] cl-user(4): (count #\a "abacad")
Hit breakpoint at func = #<Function count>, pc=17
breakpoint-> 17: 89 45 dc movl [ebp-36],eax ; item
[ldb-step] cl-user(5): 
Hit breakpoint at func = #<Function count>, pc=20
temp brkpt-> 20: 89 55 cc movl [ebp-52],edx ; sequence
[ldb-step] cl-user(6): :br ;[ask to show current breakpoints]
#<Function count>: (17 (20 :temp))
[ldb-step] cl-user(7): :loc :%eax ;[eax is the first arg on x86]
#\a
[ldb-step] cl-user(8): :loc :%edx ;[edx is the second arg on x86]
"abacad"
[ldb-step] cl-user(9): :step into ;[this will have no effect until later]
Hit breakpoint at func = #<Function count>, pc=23
temp brkpt-> 23: 8d 49 fe leal ecx,[ecx-2]
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=26
temp brkpt-> 26: 8d 45 10 leal eax,[ebp+16] ; (argument 2)
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=29
temp brkpt-> 29: 8d 95 5c ff leal edx,[ebp-164]
ff ff 
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=35
temp brkpt-> 35: 33 db xorl ebx,ebx
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=37
temp brkpt-> 37: b3 c8 movb bl,$200
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=39
temp brkpt-> 39: ff 57 4f call *[edi+79] ; kwdscan
[ldb-step] cl-user(9): ;[note that primcalls are _not_ stepped into]
Hit breakpoint at func = #<Function count>, pc=42
temp brkpt-> 42: 8d 9d 5c ff leal ebx,[ebp-164]
ff ff 
[ldb-step] cl-user(9): 
[some stepping deleted here]
Hit breakpoint at func = #<Function count>, pc=155
temp brkpt-> 155: 3b 7d e0 cmpl edi,[ebp-32] ; end
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=158
temp brkpt-> 158: 0f 85 93 00 jnz 311
00 00 
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=164
temp brkpt-> 164: 8b 45 cc movl eax,[ebp-52] ; sequence
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=167
temp brkpt-> 167: 8b 9f 9f fe movl ebx,[edi-353] ; length
ff ff 
[ldb-step] cl-user(9): 
Hit breakpoint at func = #<Function count>, pc=173
temp brkpt-> 173: b1 01 movb cl,$1
[ldb-step] cl-user(9): :loc :%eax
"abacad"
[ldb-step] cl-user(10): :step into
Hit breakpoint at func = #<Function count>, pc=175
temp brkpt-> 175: ff d7 call *edi
[ldb-step] cl-user(11): 
Hit breakpoint at func = #<Function length>, pc=0
temp brkpt-> 0: 8b c8 movl ecx,eax
[ldb-step] cl-user(11): 
Hit breakpoint at func = #<Function length>, pc=2
temp brkpt-> 2: 80 e1 03 andb cl,$3
[ldb-step] cl-user(11): :br ;[check breakpoints again]
#<Function count>: (17)
#<Function length>: ((2 :temp))
[ldb-step] cl-user(12): :br nil ;[turn off all breakpoints]
[ldb-step] cl-user(13): 
3 ;[get right answer]
[ldb] cl-user(13): 

13.0 The source stepper

Note there are two steppers in Allegro CL, the original stepper (associated with the step macro and described in the section The original stepper above), and the LDB stepper described in part in the section The Lisp DeBug (ldb) stepper above. The source variant of the LDB stepper is described in this section. We recommend that users start with the LDB stepper, which is more powerful that the original stepper.

The source stepper displays source code while stepping through a form. In the IDE, the source stepper is associated with the new Stepper Dialog. It can also be run in terminal mode as described here.

In order for information to be available to the source stepper, files must be compiled while the compiler switch save-source-level-debug-info-switch is either a function returning true or is true itself, which is initially the case whenever the debug optimization quality is 3.

When save-source-level-debug-info-switch is either a function returning true or is itself true, the compiler writes annotation information to the fasl file it produces. It does not produce different code: the same code is produced regardless of the value of save-source-level-debug-info-switch. But when the switch is either a function returning true or is itself true, the annotations added to the fasl file make the fasl file bigger.

The annotated file should be loaded while the variable *load-source-file-info* is true, but the system will prompt for the name of the necessary file if the information is not loaded when the fasl file is loaded. If the annotated file is loaded while *load-source-debug-info* is true, then the source debug information will be also loaded into Lisp at that time. This is not recommended since the information is voluminous and may not be needed, but if you have problems with ensuring that the information is loaded, loading while that variable is true ensures it.

We illustrate the interface with a simple example, showing both styles of tty-backend. The function foo is defined in the file step.cl:

(in-package :user)

(defun foo (file) 
  (with-open-file (s file :direction :input)
    (let ((fm (read s)))
      (format t "~S~%" fm))))

As the transcript shows, we set the value of the debug optimize quality to 3, then compile step.cl and load step.fasl. We then put a breakpoint at foo and start the ldb debugger. Then we call foo. As we step through, the form being executed is shown. Note that the default step mode is over/0 so function calls are jumped over and we only stop at level 0 source records

The :tty style backend is shown here:

cl-user(1): (declaim (optimize debug))
t
cl-user(2): (setq *load-local-names-info* t *load-source-file-info* t *load-source-debug-info* t)
t
cl-user(3): (setq *record-source-file-info* t)
t
cl-user(4): :cf step
;;; Compiling file step.cl
;;; Writing fasl file /tmp/cfta225001426171
;;; Moving fasl file /tmp/cfta225001426171 to step.fasl
;;; Fasl write complete
cl-user(5): :ld step
; Fast loading /net/gemini/home/duane/step.fasl
cl-user(6): :br :backend :tty foo
Adding #<Function foo>: 0
  Form=(defun foo (file)
         (with-open-file (s file :direction :input)
           (let (#) (format t "~S~%" fm))))
  level=0 pc=0 rec=0  in #P"step.cl" starting at 20 ending at 138.
Possible slide directions: (into over across)
Current step/slide type: over/0
cl-user(7): :ldb t
[ldb] cl-user(8): (foo "step.cl")

Hit breakpoint at func = #<Function foo>, pc=0
  Form=(defun foo (file)
         (with-open-file (s file :direction :input)
           (let (#) (format t "~S~%" fm))))
  level=0 pc=0 rec=0  in #P"step.cl" starting at 20 ending at 138.
Possible slide directions: (into over across)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=19
  Form=(with-open-file (s file :direction :input)
         (let ((fm #)) (format t "~S~%" fm)))
  level=0 pc=19 rec=2  in #P"step.cl" starting at 41 ending at 137.
Possible slide directions: (into out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Sliding to same pc at level=0
  Form=file
  level=0 pc=19 rec=5 style=:ref  in #P"step.cl" starting at 60 ending at 64.
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=156
  Form=(let ((fm (read s))) (format t "~S~%" fm))
  level=0 pc=156 rec=12  in #P"step.cl" starting at 88 ending at 136.
Possible slide directions: (out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

(Stopping early at call-like form before processing arguments):
  Form=(read s)
  level=0 pc=156 rec=13 style=:early  in #P"step.cl" starting at 98 ending at 106.
Possible slide directions: (out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Entering form from contour:
  Form=s
  level=0 pc=156 rec=14 style=:ref  in #P"step.cl" starting at 104 ending at 105.
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=170
  Form=(read s)
  level=0 pc=170 rec=15  in #P"step.cl" starting at 98 ending at 106.
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=173
  break initializing fm to the result of evaluating (read s)
  subform: (let ((fm (read s))) ...)
  level=0 pc=173 rec=16  in #P"step.cl" starting at 94 ending at 107.
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): :step cont  ;; this goes to the next breakpoint
(in-package :user)
nil
[ldb] cl-user(10): :ldb nil  ;; this stops stepping
cl-user(11): 

The :flip-tty style (which is the default) is shown here:

cl-user(1): (declaim (optimize debug))
t
cl-user(2): (setq *load-local-names-info* t *load-source-file-info* t *load-source-debug-info* t)
t
cl-user(3): (setq *record-source-file-info* t)
t
cl-user(4): :cf step
;;; Compiling file step.cl
;;; Writing fasl file /tmp/cfta244811440231
;;; Moving fasl file /tmp/cfta244811440231 to step.fasl
;;; Fasl write complete
cl-user(5): :ld step
; Fast loading /net/gemini/home/duane/step.fasl
cl-user(6): :br foo
Adding #<Function foo>: 0
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
->   0/0     0   2/19     nil/nil    nil    nil    20    138   (defun foo (file) ...)
     1/19    1   2/19     nil/nil    nil    nil    (20)  (138) (block foo (with-open-file (s file :direction :input) (let ((fm #)) (format t "~S~%" fm))))
     2/19    0-1   4/19     nil/nil    nil    nil    41    137   (with-open-file (s file :direction :input) (let ((fm (read s))) (format t "~S~%" fm)))
  level=0 pc=0 rec=0  in #P"step.cl" starting at 20 ending at 138.
Possible slide directions: (into over across)
Current step/slide type: over/0
cl-user(7): :ldb t
[ldb] cl-user(8): (foo "step.cl")

Hit breakpoint at func = #<Function foo>, pc=0
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
->   0/0     0   2/19     nil/nil    nil    nil    20    138   (defun foo (file) ...)
     1/19    1   2/19     nil/nil    nil    nil    (20)  (138) (block foo (with-open-file (s file :direction :input) (let ((fm #)) (format t "~S~%" fm))))
     2/19    0-1   4/19     nil/nil    nil    nil    41    137   (with-open-file (s file :direction :input) (let ((fm (read s))) (format t "~S~%" fm)))
  level=0 
Possible slide directions: (into over across)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=19
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
     0/0     0   2/19     nil/nil    nil    nil    20    138   (defun foo (file) ...)
     1/19    1   2/19     nil/nil    nil    nil    (20)  (138) (block foo (with-open-file (s file :direction :input) (let ((fm #)) (format t "~S~%" fm))))
->   2/19    0-1   4/19     nil/nil    nil    nil    41    137   (with-open-file (s file :direction :input) (let ((fm (read s))) (format t "~S~%" fm)))
     3/19    1-2   4/19     nil/nil    nil    nil    (41)  (137) (let ((s (open file ...)) (#:with-open-file-abort-160 t)) ...)
     4/19    1-2   5/19     nil/nil    :early  nil    nil   nil   (open file :direction :input)
  level=0 
Possible slide directions: (into out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Sliding to same pc at level=0
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
     3/19    1-2   4/19     nil/nil    nil    nil    (41)  (137) (let ((s (open file ...)) (#:with-open-file-abort-160 t)) ...)
     4/19    1-2   5/19     nil/nil    :early  nil    nil   nil   (open file :direction :input)
->   5/19    0-2   6/41     nil/nil    :ref   nil    60    64    file
     6/41    1-2   7/52     nil/nil    nil    :call  nil   nil   (open file :direction :input)
     7/52    1-2   8/52     nil/nil    :const  nil    nil   nil   (quote t)
  level=0 
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=156
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    10/64    1-2  11/156    nil/nil    nil    nil    nil   nil   (unwind-protect (multiple-value-prog1 (progn (let (#) ...)) ...) ...)
    11/156   1-2  12/156    nil/nil    nil    nil    nil   nil   (multiple-value-prog1 (progn (let ((fm #)) ...)) ...)
->  12/156   0-2  13/156    nil/nil    nil    nil    88    136   (let ((fm (read s))) (format t "~S~%" fm))
    13/156   0-2  14/156    nil/nil    :early  nil    98    106   (read s)
    14/156   0-2  15/170    nil/nil    :ref   nil    104   105   s
  level=0 
Possible slide directions: (out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

(Stopping early at call-like form before processing arguments):
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    11/156   1-2  12/156    nil/nil    nil    nil    nil   nil   (multiple-value-prog1 (progn (let ((fm #)) ...)) ...)
    12/156   0-2  13/156    nil/nil    nil    nil    88    136   (let ((fm (read s))) (format t "~S~%" fm))
->  13/156   0-2  14/156    nil/nil    :early  nil    98    106   (read s)
    14/156   0-2  15/170    nil/nil    :ref   nil    104   105   s
    15/170   0-2 #(16 22)/#(173 391)  nil/nil    nil    :call  98    106   (read s)
  level=0 
Possible slide directions: (out over across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Entering form from contour:
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    12/156   0-2  13/156    nil/nil    nil    nil    88    136   (let ((fm (read s))) (format t "~S~%" fm))
    13/156   0-2  14/156    nil/nil    :early  nil    98    106   (read s)
->  14/156   0-2  15/170    nil/nil    :ref   nil    104   105   s
    15/170   0-2 #(16 22)/#(173 391)  nil/nil    nil    :call  98    106   (read s)
    16/173   0-2  17/173    nil/nil    :init  nil    94    107   ((fm (read s)) fm nil)
  level=0 
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=170
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    13/156   0-2  14/156    nil/nil    :early  nil    98    106   (read s)
    14/156   0-2  15/170    nil/nil    :ref   nil    104   105   s
->  15/170   0-2 #(16 22)/#(173 391)  nil/nil    nil    :call  98    106   (read s)
    16/173   0-2  17/173    nil/nil    :init  nil    94    107   ((fm (read s)) fm nil)
    17/173   0-2  19/173    nil/nil    nil    nil    115   135   (format t "~S~%" fm)
  level=0 
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): 

Hit breakpoint at func = #<Function foo>, pc=173
Function: #<Function foo> in #P"step.cl":
    rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form
    14/156   0-2  15/170    nil/nil    :ref   nil    104   105   s
    15/170   0-2 #(16 22)/#(173 391)  nil/nil    nil    :call  98    106   (read s)
->  16/173   0-2  17/173    nil/nil    :init  nil    94    107   ((fm (read s)) fm nil)
    17/173   0-2  19/173    nil/nil    nil    nil    115   135   (format t "~S~%" fm)
    18/173   1-3  19/173    nil/nil    :early  nil    (115) (135) (format t (quote (excl::lazy-formatter-tree . "~S~%")) fm)
  level=0 
Possible slide directions: (out across back)
Current step/slide type: over/0
[ldb-step] cl-user(9): :step cont  ;; this goes to the next breakpoint
(in-package :user)
nil
[ldb] cl-user(10): :ldb nil  ;; this stops stepping
cl-user(11): 

slide options

At each step, you are given some slide options, such as over, into, across, out, and back. Not all options are available at every step. You can use the :slide top-level command with the type of sliding or the :step command at the prompt.

In general, sliding is a conceptual way to navigate around a function, in spite of the fact that there is no mouse to point to select the form you want (with the Stepper Dialog in the IDE, you can use the mouse to indicate what you wish to do). In the tty interface, the :slide command is the mechanism. The :slide command does not alter the state of computation (that is done with actions like a carriage return or :step cont) but rather affects what form is being examined.


14.0 Source stepper backends

A backend is a presentation style for the source stepper. It prints information in the format required for the particular context in which the stepper is running. Examples:

These backends have used an internal, undocumented macro called excl::with-ldb-backend to temporarily switch from one backend to another. In addition, these backends can now be switched on demand by the :backend subcommand of the :break top-level command. It is only useful to switch between the two tty-style backends.

The command syntax is:

:br[eak] :backend name

where name is the name of one of the tty style backends.

For 11.0 and later, the default backend is the :flip-tty backend. The :tty backend can be installed via

:br :backend :tty

and the default backend can be restored via

:br :backend :flip-tty

If a breakpoint was being displayed at the time of the command, it is redisplayed using the new backend.


14.1 The :tty backend

Prior to 11.0, stepper output for Lisp source records was not very consistent. That style of output for each breakpoint depended on the type of breakpoint that was being displayed. The style includes complete form representation based on pretty-printing criteria, and location information that is needed to navigate the stepping process.

An example run is provided which contrasts the two tty-style backends here, with the :tty style being demonstrated here


14.2 New flip-book style backend

As of 11.0 and later, the default style for stepping through lisp code is called flip-book style. It uses the :flip-tty ldb-backend, as opposed to the older style, which uses the :tty backend.

The :tty backend was cumbersome in that it was inconsistent in its formatting. The source form being printed might be one line, or even one word, or it might be 100 lines long due to the complexity of the source. Stepping though the source records force the user to look for the relevant information, and there is no continuity between the current breakpoint and any prior or upcoming breakpoints.

An example run is provided which contrasts the two tty-style backends here, with the :flip-tty style being demonstrated here

The :flip-tty backend shows records in a style which is similar to the format printed by print-function-meta-info, except that only the current breakpoint is shown along with a few records before and after it (the default is 2 records on either side). Because the output is always the same size, it is more consistent in its appearence, and if the terminal buffer or window is consistent in keeping the prompt at the bottom of the buffer or window, then successive typing of the "return" or "enter" key will cause the next steps to advance in such a way that the small group of records appear to be animated in-place, rather than moving in fits and starts.

For more info on the following source-record fields, see Source record description.

The current breakpoint is seen with a -> arrow pointing to that record. Each group of up to 5 records is preceeded with a title line which describes the fields of each line:

 rec/pc lev nxt(rec/pc) br(rec/pc) e-type br-type  st  end       form

where

Note that as you step through code, not all records are necessarily stopped at. With the default step/slide type of over/0, only records that can be at level 0 will be stopped at. To step through all records in the current function, set the step/slide type to over/into via the top-level command:

:step over into

To step through all records in every function, into/into should be used:

:step into into

See :step in ldb mode for info on stepping in the source-level stepper.


The functions validate-lisp-source and print-function-meta-info print information about function definitions, compilation and debugging. The information is complex. The following sections describe aspects of the output and so provide a single description for things done by various functions.


15.1 Source record description

A source record is printed specially by print-function-meta-info, because source records serve as breakpoints and the ldb-code struct which implements a source record inherits from the breakpoint struct. Each of these structs print differently (more concisely) when using their print-object methods. When print-function-meta-info is called with :mixed or :vars non-nil and available, each source record line is prepended with "S", otherwise no "S" is prepended. Before the first record. a header is printed, which gives a general feel for the alignments of fields. Not all fields align perfectly on every record: some fields are longer than others. For orientation, a vertical bar (|) is placed between fields in two places, thus dividing the record into 3 sections.

Below is a chart of slot names and their field names in the printout. Also present is an indication of whether the field represents a breakpoint slot (otherwise it is a lisp record slot). Note that some field names are duplicated; they are short to save space and the correct sense can easily be seen by what section the field is in

slot name     field name    breakpoint slot?     Comment
=========================================================
                                          Start of record section
ics-passa      i
index          ix
level          lev
prev-rec       prev
next-rec       nxt
branch-rec     br
enclosing      encl
end            end
(separator)    |                          Signals start of macroexpand section
parent         par
child          chld
(separator)    |                          Signals start of pc section
pc             pc              yes
next-pc        nxt             yes
branch-pc      br              yes
entry-type     e-type
branch-type    b-type          yes
start-char     st
end-char       end
source         form

Slot/Field descriptions


15.2 Disassembler output description

This description is only general, since different architectures have different assembler languages and thus different disassembler output. Also, this description only covers instructions: headers and other information normally printed by disassemble are not covered.

The general instruction format is

|<annotation>| <pc> <hex data> |[<real mnemonic>]
  | <mnemonic> <args> |<comments>| |<more hex data>|

Where || denotes an optional field.

Descriptions:


15.3 Variable transition record description

A variable transition is always printed as an object, using the variable-transition's print-object method. Within the print-function-meta-info output, the first character of the line is always "V". The object is positioned in a column that generally aligns the pc value with other values in the print-function-meta-info output.

Fields that are printed are:

Transitions descriptions

As of Allegro CL version 11.0 transition types have been enhanced to become more acurate and descriptive.

Pre-11.0 transition types

In a perfect world, there would only be two transition types: :def and :use, which would describe the start of a live range for a variable (a la :def, where the variable is defined), and the end of the live range (the last use of the variable, a la :use). Pre-11.0 lisps note these two transition types and also :def-arg transitions which represent function arguments receiving a value.

Challenges to this simple scheme:

Consider the following flow chart with basic-blocks (units of program flow which may be comprised of zero or more instructions, and which have one or more inputs and one or more outputs, but which are otherwise essentially linear within themselves). Within each basic block might be any number of quads, which are individual units of execution which have 4 parameters: one or more inputs, one or more outputs, and two extra qualifying arguments. Here we are considering three variables to be live: a, b, and c:

        |
      -----
     | bb1 |
     |     | ---  bcc quad: live variables: a, b, c
      -----      \
        |         \
        |          v
        |        -----
        |       | bb2 |  any quad: live variables: a, b, c
        v       |     |
      -----      -----
     | bb3 |       |
     |     | < - -/ - - - (setq c 10)
      -----      /
        |      /
        |    /
        |  /
        |/
        v
      -----
     | bb4 | any quad: live variables: a, b, c
     |     |
      -----
        |
       ...
        |
        |
        v
      -----
     | bb10|
     |     |  throw quad: (no live variables)
      -----

      (bb2's placement here).

The flow chart for this set of basic-blocks requires two dimensions to represent, but in order to place the code for these blocks, bb2 must be moved elsewhere. One way to do this is to just place the code for bb2 between bb1 and bb3 and create extra jumps from the end of bb1 to bb3 and from the end of bb2 to bb4. Another way, which better preserves linearity, is to place the code for bb2 after the whole line of code has been placed (in this case after bb10), and then to have a reverse branch from the end of bb2 to bb4. The branch-on-condition quad (bcc) already has two targets, the fall-through and the branch target, so only one extra branch is generated when the code is placed. If furthermore the selection is made wisely as to whether it is bb2 or bb3 which is placed linearly, the most likely path through the function might be executed with the fewest branches.

If in bb3 the quad which executes the lisp source (setq c 10) resides, then c will be live in bb1 but dead in the first part of bb3, finally become alive again when the setq occurs.

Since the throw occurs on bb10, variable c cannot be live after that throw quad. However, if bb2 has been placed after bb10, variable c becomes alive again, as a continuation into the basic block by the code in bb1. Likewise, any code afer bb2 will render c dead, because it just jumped back to bb4.

Transition types, 11.0 and later

Starting in the 11.0 release, the variable transition types have been expanded more appropriately represent the challenges mentioned above.


15.4 Census point description

A census point is always printed as an object, using the census-point's print-object method. Within the print-function-meta-info output, the first character of the line is always "C". The object is positioned in a column that generally aligns the pc value with other values in the print-function-meta-info output.

Fields that are printed are:


16.0 gdb (or lldb or windbg) interface

The Allegro CL gdb interface is a mixture of gdb call commands and other low-level gdb commands to produce debugging output useful for lisp debugging. Some other debuggers, such as windbg on Windows systems, also provide a call command (it is called .call on windbg) and can thus also be used with this interface. On MacOS's lldb program, the expr command can be used.

Note this interface is by no means complete, and is known to have bugs. It has been tested on the following platforms:

There are currently 16 external functions and four variables in this interface. Eight of the functions are paired, differing only in where the output is directed. Five of the functions are abbreviations for longer ones that contain underscores in their names, and are provided for typing convenience.

void lisp_output_object(LispVal obj)

void loo(LispVal obj)

Output a printed representation of the object to file-descriptor 1. obj is a tagged representation of a lisp value.

Current limitations: many, including:

  1. Symbols are not qualified with their packages.

  2. Many objects are only described in a very high-level way, such as <struct: 0x12345> rather than describing them in more detail, e.g. like stating the name of the struct.

  3. Element types of vectors are not stated, except for strings, which are specially printed as strings.

Such objects are printed with angle-brackets only, rather than in a way that might confuse the reader into thinking that the printing was being done in a lispy way (such as #<...> for unreadable objects).

Notes about current frame functions

This interface has a notion of a "current" frame. When lisp_zo() (or lisp_zo_aux()) is successful, the frame it first finds is remembered as the current frame. Other functions can operate on the current frame to move up or down in the stack. Care must be taken to call lisp_zo() with a nonzero frame argument after a breakpoint, in case the running program has removed the stack frames that had been remembered by the debugger.

The arguments are unsigned nats, which are defined formally in the file misc/lisp.h (in the misc/ subdirectory of the Allegro directory), but which are typically positive integers. The out argument to the _aux functions should be a pointer to a C stream suitable for output. Variables named frame are expected to be pointers into the stack, but what registers might be used to start the stacks might vary. Most architectures have a stack pointer register generically named sp and a frame pointer register named fp. They might also be adorned with punctuation; gdb generally wants a register name to be preceded by a dollar sign ($), but other debuggers might want either to see a percent sign (%) or at (@) sign. Also, some systems have alternate names for sp and fp - for gdb, $fp might be $ebp on 32-bit x86, $rbp on x86-64, or $x29 on arm64/Apple-Silicon systems. The best value to pass into a lisp_zo() or equivalent function is $sp on x86-64 and $fp on 32-bit x86 and on arm64 architectures.

Other notes:

  1. On Windows the functions below can be called by referring to them by their fully-qualified names including the library name; for example .call acli11b156!lisp_zo($sp,10)

  2. Sometimes gdb will give a complaint about an invalid cast. In these cases, call (void)lisp_zo(...) should work.

void lisp_zo(unsigned nat frame, unsigned nat n)

void lzo(unsigned nat frame, unsigned nat n)

void lisp_zo_aux(unsigned nat frame, unsigned nat n, FILE * out)

Print to file descriptor 1 a backtrace from the specified frame, for n frames. lisp_zo() prints to stdout, while lisp_zo_aux() prints to the location specified by the out argument.

Frames are printed in function call or funcall format when possible, otherwise just the names of the functions are printed as function objects. When a frame represents a C function, its saved program counter is printed in a C form. If the frame is a "runtime system" frame (linked in as C/asm, but lisp-savvy) it is annotated as "rs:".

If frame is given as 0, then the current frame is used to start the output. The current frame is determined as follows:

The lisp_zo() function now heeds a "focus", that is, a particular thread which is pre-selected. The focus is set by lisp_focus(), described below.

Current limitations include:

  1. locals are not printed.

  2. Some architectures can't yet walk the stack.

  3. Some architectures don't support dladdr() functionality, and so the C frames printed as "".

  4. If a frame appears to be a lisp frame, but doesn't have a valid function object in the expected spot, then the frame is annotated as "incomplete:" and an attempt to print it as a C frame is made. This tends to happen at the beginning of a function where the stack has not yet been completely built, or at the end of a backtrace where the stack is a little ragged on some architectures.

void lisp_cur(void)

void lcur(void)

void lisp_cur_aux(FILE * out)

Prints the current frame, if there is one. May fail catastrophically if no current frame exists.

void lisp_up(int n)

void lup(int n)

void lisp_up_aux(int n, FILE *out)

Moves up n frames, where "up" is the direction toward the top, or frontier, of the stack, then prints the new current frame. Note that this is the same concept of "up" as Allegro CL debugger has, and opposite of gdb.

void lisp_dn(int n)

void ldn(int n)

void lisp_dn_aux(int n, FILE *out)

Moves down n frames, where "down" is the direction away from the top, or frontier, of the stack, then prints the new current frame. Note that this is the same concept of "down" as Allegro CL debugger has, and opposite of gdb.

int lisp_print_level = 5

int lisp_print_length = 10

These two variables form a parallel to *print-level* and *print-length* in Lisp. They must be integers, though; do not set them to nil (i.e. nilval). Currently, they only affect the printing of lists.

char *(*lisp_demangle_hook)(char *) = NULL

void(*lisp_demangle_deallocate_hook)(char *) = NULL

If non-null, these variables hook into the printing of non-lisp names; when in the gdb interface a frame is being printed which might be a C++ name, the lisp_demangle_hook is called, which should either return a non-null string which has been demangled, or a NULL (which will result in the frame-printer using the original string). After the name is printed, if the original name was demangled (i.e. the hook returned a non-null string) and if the lisp_demangle_deallocate_hook is non-null, then that hook is called in order to deallocate the string which might have been allocated by the demangle hook. Users should ensure that these hooks match in their operation; e.g. deallocation should not occur if there was never an allocation by the demangling functionality.

unsigned nat lisp_focus(unsigned nat threadspec)

unsigned nat lfoc(unsigned nat threadspec)

Set up to focus on the thread specified by threadspec. After the function is run any lisp_zo, lisp_cur, lisp_up, or lisp_dn calls will pertain only to that sepected thread's stack.

threadspec can be one of

Once the thread is selected, the "current" frame (i.e. what will print if lisp_zo is given a first argument of 0) will be adjusted appropriately and a message will be shown in stdout which indicates what stack frame will be current. From there, lisp_zo, lisp_cur, lisp_up, and lisp_dn can be used to navigate frames within the focussed thread.

Issues:

  1. If running in an SMP lisp, care must be taken when focussing on a thread that is not the current thread, because threads are not stoppped like they are in the lisp debugger with the :focus top-level-command, and stack addresses which were valid in one invocation of lisp_zo may likely not be valid in the next invocation.

  2. For virtual-threaded lisps, there is only one stack, and a thread-switch operation is performed to offload the current stack to the saved area for that thread and to then load the saved stack into the stack area for the thread that will start executing next. The thread switch operation is tracked by a counter, and if the number of thread switches has changed between one invocation of lisp_zo and the next, the concept of "current" stack frame is reset and the cache of saved frames is erased.

Useful C variables

acl_thread_control

A struct which controls all Allegro CL threads. Two useful slots are

lisp_frame_cache

A 4096-integer array which may hold cached stack frames.

lisp_frame_current_index

An integer index into the lisp_frame_cache. If lisp_frame_cache[lisp_frame_current_index] is non-zero, then it represents the (purportedly valid) current stack frame.

Example 1

See Example 2 for usage for 64-bit lisps on Intel chips

At a prompt on a linux 32-bit lisp:

Note here that:

  1. The stack frame is denoted by $ebp. Sometimes $fp can be used, but be sure it truly does represent the current value of the frame (on some architectures gdb makes a best guess as to what was "meant" by the frame-pointer in specifying $fp, and it doesn't always match the value of $ebp).

  2. The second arg is 0, asking for as many frames as possible.

(gdb) call lisp_zo($ebp,0)
0xbf83a538: <unknown C name>
0xbf83a870: (read-octets <stream: 0x71208f92> nil 0 4096 peek)
0xbf83a8d8: (funcall #'(((method) . device-read) terminal-simple-stream t t t t) <stream: 0x71208f92> nil 0 nil peek)
0xbf83a958: (funcall #'(((internal) (((#))) . t) . 0) <stream: 0x71208f92> nil 0 nil peek)
0xbf83a9a0: (erroring-device-read <stream: 0x71208f92> nil 0 nil peek)
0xbf83aa38: #'(((efft) . dc-read-char) . latin1-base)
0xbf83aa90: (simple-stream-peek-char nil <stream: 0x71208f92> nil eof)
0xbf83aae8: (peek-char nil <stream: 0x71208f92> nil eof)
0xbf83ab28: (peek-char-non-whitespace <stream: 0x71208f92>)
0xbf83ac68: (read-top-level-command <stream: 0x71208f92>)
0xbf83af40: (read-eval-print-one-command nil nil)
0xbf83b2a0: (read-eval-print-loop continue-error-string nil condition <standard-instance: 0x71a9b802>)
0xbf83b560: (internal-invoke-debugger "Error" <standard-instance: 0x71a9b802> t)
0xbf83b640: (error <standard-instance: 0x71a9b802>)
0xbf83b8b8: (unbound-variable-handler a)
0xbf83b900: (general-error-handler 5 a t t)
0xbf83b938: (cer-general-error-handler-one 5 a)
0xbf83ba08: rs: stopped at "unbound+158"
0xbf83bab0: (%eval a)
0xbf83bb20: (eval a)
0xbf83bdf8: (read-eval-print-one-command nil nil)
0xbf83c158: (read-eval-print-loop level 0)
0xbf83c1d0: (top-level-read-eval-print-loop1)
0xbf83c228: (top-level-read-eval-print-loop)
0xbf83c270: rs: stopped at "apply+311"
0xbf83c300: (start-interactive-top-level <stream: 0x71208f92> #'top-level-read-eval-print-loop nil)
0xbf83c5a0: (start-lisp-execution t)
0xbf83c5f0: rs: stopped at "apply+311"
0xbf83c6b8: (thread-reset-catcher)
0xbf83c7e8: (setup-required-thread-bindings)
0xbf83c8d8: (run-initial-thread t)
0xbf83c980: (lisp-thread-start #S(thread:0xa0001822 <bad object: 0x845972> <bad object: 0x845972> "Initial Lisp Listener" <standard-instance: 0x71a6a1e2> nil #'start-lisp-execution-0 (t) nil nil...) 1)
0xbf83c9d8: rs: stopped at "start_reborn_lisp+76"
0xbf83ca48: rs: stopped at "startup_lisp+11"
0xbf83ca68: stopped at "cont_setstack+371"
0xbf83cac0: rs: stopped at "first_lisp_thread_init+274"
0xbf83cb18: stopped at "setupstack_within_xhandler+16"
(gdb) 

We can move down a few frames:

(gdb) call lisp_dn(2)
0xbf83a8d8: (funcall #'(((method) . device-read) terminal-simple-stream t t t t) <stream: 0x71208f92> nil 0 nil peek)
(gdb) call lisp_dn(1)
0xbf83a958: (funcall #'(((internal) (((#))) . t) . 0) <stream: 0x71208f92> nil 0 nil peek)
(gdb) 

Here, we've selected a frame to print, and asked for two frames.
(gdb) call lisp_zo(0xbf83a958,2)
0xbf83a958: (funcall #'(((internal) (((#))) . t) . 0) <stream: 0x71208f92> nil 0 nil peek)
0xbf83a9a0: (erroring-device-read <stream: 0x71208f92> nil 0 nil peek)
(gdb) 

Note above that not all of the first frame's name is printed. If we grab the function object from the frame, print it, and then print it after increasing the lisp_print_level, then we see the whole name:

(gdb) x/16x 0xbf83a958-16
0xbf83a948: 0xfffffffa  0x719ed7f2  0x00000570  0x711d0a42
0xbf83a958: 0xbf83a9a0  0x71259d71  0x71208f92  0x71000685
0xbf83a968: 0x00000000  0x71000685  0x711c48ff  0x00000040
0xbf83a978: 0x71208f92  0xa0001822  0x71000685  0xa0001822
(gdb) call lisp_output_object(0x711d0a42)
#'(((internal) (((#))) . t) . 0)
(gdb) p lisp_print_level
$1 = 5
(gdb) set lisp_print_level=10
(gdb) call lisp_output_object(0x711d0a42)
#'(((internal) (((((effective-method) . 5)))) . t) . 0)
(gdb) 

Note that the name is of an "internal" fspec form. Note also that since the symbols don't print their packages, calling the internal function to externalize the name doesn't fix it:

cl-user(1): (excl::convert-to-external-fspec '(((internal) (((((effective-method) . 5)))) . t) . 0))
(((internal) (((#))) . t) . 0)
cl-user(2): 

However, adding package qualifiers does fix this (this nested fspec happens to have two function specs in the keyword package):

cl-user(2): (excl::convert-to-external-fspec '(((:internal) (((((:effective-method) . 5)))) . t) . 0))
(:internal (:effective-method 5 nil nil nil t) 0)
cl-user(3): 

Example 2

Here is the backtrace for Linux/amd64. Note that we use $rsp for the stack frame, and that there are a couple of anomalies at the end of the backtrace (the last frame is mistaken for a Lisp frame, which makes it appear to be incomplete as a lisp frame, and also that incompleteness causes a failure to find the next frame, which is hard in general to do on any architecture which does not store an explicit next-link into its stack frames).

(gdb) call lisp_zo($rsp,0)
0x7fff36d74968: stopped at "cl_select_read+300"
0x7fff36d769e0: (read-octets <stream: 0x100032dfe2> nil 0 nil peek)
0x7fff36d76e40: (funcall #'(((method) . device-read) terminal-simple-stream t t t t) <stream: 0x100032dfe2> nil 0 nil peek)
0x7fff36d76f30: (funcall #'(((internal) (((#))) . t) . 0) <stream: 0x100032dfe2> nil 0 nil peek)
0x7fff36d77040: (erroring-device-read <stream: 0x100032dfe2> nil 0 nil peek)
0x7fff36d770d0: #'(((efft) . dc-read-char) . latin1-base)
0x7fff36d771b0: (simple-stream-peek-char nil <stream: 0x100032dfe2> nil eof)
0x7fff36d77280: (peek-char-non-whitespace <stream: 0x100032dfe2>)
0x7fff36d77310: (read-top-level-command <stream: 0x100032dfe2>)
0x7fff36d77560: (read-eval-print-one-command nil nil)
0x7fff36d77ae0: (read-eval-print-loop continue-error-string nil condition <standard-instance: 0x1001136932>)
0x7fff36d78170: (internal-invoke-debugger "Error" <standard-instance: 0x1001136932> t)
0x7fff36d786e0: (error <standard-instance: 0x1001136932>)
0x7fff36d788a0: (unbound-variable-handler a)
0x7fff36d78d60: (general-error-handler 5 a t t)
0x7fff36d78e20: rs: stopped at "unbound+355"
0x7fff36d79240: (%eval a)
0x7fff36d79330: (eval a)
0x7fff36d79430: (read-eval-print-one-command nil nil)
0x7fff36d799b0: (read-eval-print-loop level 0)
0x7fff36d7a040: (top-level-read-eval-print-loop1)
0x7fff36d7a140: (top-level-read-eval-print-loop)
0x7fff36d7a200: rs: stopped at "apply+479"
0x7fff36d7a2b0: (start-interactive-top-level <stream: 0x100032dfe2> #'top-level-read-eval-print-loop nil)
0x7fff36d7a3d0: (start-lisp-execution t)
0x7fff36d7a8a0: rs: stopped at "apply+479"
0x7fff36d7a960: (thread-reset-catcher)
0x7fff36d7aae0: (setup-required-thread-bindings)
0x7fff36d7ad20: (run-initial-thread t)
0x7fff36d7aef0: (lisp-thread-start #S(thread:0x20000018b2 <bad object: 0x2aaaaad3b392> <bad object: 0x2aaaaad3b392> "Initial Lisp Listener" <standard-instance: 0x1001106462> nil #'start-lisp-execution-0 (t) nil nil...) 1)
0x7fff36d7b050: rs: stopped at "first_lisp_thread+584"
0x7fff36d7b110: rs: stopped at "start_reborn_lisp+140"
0x7fff36d7b1f0: 
Terminating frame 0x7fff36d7b1f0: can't find valid next frame
incomplete: stopped at "startup_lisp+14"

Terminating frame 0x7fff36d7b1f0: can't find valid next frame
(gdb) 

Copyright (c) 2023, Franz Inc. Lafayette, CA., USA. All rights reserved.

ToC DocOverview CGDoc RelNotes FAQ Index PermutedIndex
Allegro CL version 11.0