Obtaining Stack-trace Analog for Macros in Common Lisp

Viewed 170

I may be asking for the impossible, but am wondering nonetheless.

Is it possible to obtain an analog of the stack-trace for macros? That is, if one set a break-point inside a certain function, the macro-stack-trace would list all macros (perhaps with their inputs) that were macroexpanded to get to that level in the code.

From what I understand, this is currently impossible, but it may be due to my shallow understanding. Does Allegro or SBCL allow or track this kind of information? It appears that this would be really useful for debugging macros.

Any help or advice is appreciated.

2 Answers

As SBCL is a compiler-only implementation meaning all code is automatically compiled (in contrast to being "interpreted"). Calls to macros are expanded as part of compilation, so the fact that something was a macro call is lost.

(defmacro m (n)
   `(/ 10 ,n))

(defun foo (x) (m x))

SBCL:

* (foo 0)

debugger invoked on a DIVISION-BY-ZERO in thread
#<THREAD "main thread" RUNNING {1001E06493}>:
  arithmetic error DIVISION-BY-ZERO signalled
Operation was /, operands (10 0).

Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.

restarts (invokable by number or by possibly-abbreviated name):
  0: [ABORT] Exit debugger, returning to top level.

(SB-KERNEL::INTEGER-/-INTEGER 10 0)
0] backtrace

Backtrace for: #<SB-THREAD:THREAD "main thread" RUNNING {1001E06493}>
0: (SB-KERNEL::INTEGER-/-INTEGER 10 0)
1: (FOO 0)
2: (SB-INT:SIMPLE-EVAL-IN-LEXENV (FOO 0) #<NULL-LEXENV>)
3: (EVAL (FOO 0))
4: (INTERACTIVE-EVAL (FOO 0) :EVAL NIL)
[...]

Some implementations, e.g. Allegro CL, support both interpreted as well as compiled code, the first being helpful in debugging, the second giving better performance. (I show here the command-line interactions. Allegro also offers a GUI to set breakpoints that I'm not familiar with.)

cl-user(4): (foo 0)
Error: Attempt to divide 10 by zero.
  [condition type: division-by-zero]

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

[1] cl-user(5): :zoom
Evaluation stack:

   (error division-by-zero :operation ...)
 ->(/ 10 0)
   (foo 0)
   (eval (foo 0))
   [...]

The zoom command takes many options to be more verbose, this shows the form (block foo (m x)):

[1] cl-user(6): :zoom :all t
Evaluation stack:

... 4 more newer frames ...

   ((:runsys "lisp_apply"))
   [... sys::funcall-tramp ]
   (excl::error-from-code 17 nil ...)
   (sys::..runtime-operation "integer_divide" :unknown-args)
   (excl::/_2op 10 0)
 ->(/ 10 0)
   [... excl::eval-as-progn ]
   (block foo (m x))
   (foo 0)
   (sys::..runtime-operation "comp_to_interp" 0)
   [... excl::%eval ]
   (eval (foo 0))

When you (compile 'foo) the macro calls will be expanded away (like for SBCL) and not show up in backtraces anymore (but Allegro's source-level debugging could help).

In general when it comes to defining macros, to help debugging try to expand into function calls and not big bodies of code. E.g. instead of:

(defmacro with-foo ((var-x var-y thing) &body body)
   `(let ((,var-x (..derive from ,thing ..))
          (,var-y (..derive from ,thing ..)))
       ,@body))

I would write it like:

(defmacro with-foo ((var-x var-y thing) &body body)
   `(call-with-foo (lambda (,var-x ,var-y) ,@body) ,thing))

(defun call-with-foo (func thing)
  (let ((x (..derive from thing ..)
        (y (..derive from thing ..))
   (funcall func x y)))

so it ends up in the stack trace and is easy to redefine. See this great post by Kent Pitman:

Incidentally, too, back to CL, you should know that when I write these WITH-xxx macros, I almost always accompany them with a CALL-WITH-xxx so that I can do either kind of call. But I find I almost never use the CALL-WITH-xxx even when I was the one to provide it as an option. The main reason I write them is not to use them but to make redefinition easier, since I can redefine the CALL-WITH-xxx without redefining the macro, and so I don't have to recompile the callers if the definition changes.

Yes, AllegroCl supports tracing and in general debugging of macros. Quite an effort for not sure how much benefit, but Franz tends to do good things to make CL more viable. Pro tip: there is a an option to turn off what I think they call source-level debugging of macros, and you will want to do that if your code makes heavy use of macros or compilation times can get crazy. Just turn it back on when you think you need the source debugging.

Related