Macro expansion happens at the compile time, which makes it difficult to debug. The best way to avoid headache of macro debugging is not to write macros unless they’re absolutely necessary, and keep them as simple as possible if you need to write ones.
However, if you find yourself in an unfortunate situation that you have to untangle hairy macros, Gauche has some tools to help.
• Tracing macro expansion: | ||
• Expanding macros manually: |
Macro tracing shows the input to the macro expander and the result of
its expansion on selected macros. Suppose you have the following
macro definition. It’s essentially the same as shown in the definition
of letrec
in R7RS section 7.3:
(define-syntax my-letrec (syntax-rules () [(_ ((var init) ...) body ...) (my-letrec "tmps" (var ...) () ((var init) ...) body ...)] [(_ "tmps" () (tmp ...) ((var init) ...) body ...) (let ((var 'undefined) ...) (let ((tmp init) ...) (set! var tmp) ... body ...))] [(_ "tmps" (x y ...) (tmp ...) binds body ...) (my-letrec "tmps" (y ...) (newtmp tmp ...) binds body ...)]))
The my-letrec
macro uses an idiom to generate temporary
variables by looping with "tmps"
tag. You can see how
the macro is expanded step by step, by tracing my-letrec
:
gosh> (trace-macro 'my-letrec) (my-letrec) gosh> (my-letrec [(ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))] (ev? 3)) Macro input>>> (my-letrec ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (my-letrec "tmps" (ev? od?) () ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro input>>> (my-letrec "tmps" (ev? od?) () ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (my-letrec "tmps" (od?) (newtmp.0) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro input>>> (my-letrec "tmps" (od?) (newtmp.0) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (my-letrec "tmps" () (newtmp.1 newtmp.0) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro input>>> (my-letrec "tmps" () (newtmp.0 newtmp.1) ((ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))) (ev? 3)) Macro output<<< (let ((ev? (quote undefined)) (od? (quote undefined))) (let ((newtmp.0 (^n (if (= n 0) #t (od? (- n 1))))) (newtmp.1 (^n (if (= n 0) #f (ev? (- n 1)))))) (set! ev? newtmp.0) (set! od? newtmp.1) (ev? 3))) #f
In the above example, the S-expressions
after gosh>
prompt is what you type; all other things are
Gauche’s answer, including Macro input
and Macro output
S-expressions.
The S-expression shown with Macro input
is the input of
the macro expander, and the one with Macro output
is the
expanded result. Actual macro output has syntactic information
attached, but the tracer strips them off for the legibility.
Note that the loop introduces new temporary variables with the same
name (newtemp
), but they are treated as different identifiers
in the macro expansion.
Once you’re done debugging,
don’t forget to call untrace-macro
with no arguments
to remove macro traces. If there’s a macro trace set, all macro expansions
get some overhead, so don’t leave macro traces.
gosh> (untrace-macro) #f
Get/set current macro trace setting. Macro trace setting can be one of the following values:
#f
Macro tracing is off. This is the default setting.
#t
All macro expansions are traced.
(name-or-pattern …)
Trace macros that match any one of name-or-pattern, which is either a symbol or a regexp. If it’s a symbol, a macro whose name is the same as the symbol is traced. If it’s a regexp, macros whose name match the regexp are traced.
When called without arguments, trace-macro
doesn’t change
the setting; it returns the current setting.
When called with single boolean value, it sets the current setting to that value. Returns the updated setting.
When called with one or more name-or-pattern, it adds them
to the current setting. Note that if the current setting is #t
,
it remains #t
, for all macros are already traced.
Returns the updated setting.
If macro trace settings is not #f
, it incurs overhead for
every macro expansion. Be careful not to leave macro trace set.
The trace information is output to the current trace port. (see Common port operations).
When called without arguments, it turns macro trace off.
When called with one or more name-or-pattern, which is either
a symbol or a regexp, untrace-macro
removes them from
the currently traced macros. Note that if the current macro trace setting
is #t
(trace all macros), you can’t remove traced macro individually.
It returns the updated macro trace setting.
If form is a list and its first element is a variable
globally bound to a macro, macroexpand-1
invokes its macro transformer and
returns the expanded form. Otherwise, returns form as is.
macroexpand
repeats macroexpand-1
until the
outermost expression of form can’t be expanded.
(It doesn’t expand macros other than outermost one. If you want
to expand all the macros within form, use macroexpand-all
).
These procedures can be used to expand globally defined macros.
Internally, hygienic macro expansion wraps symbols in form with syntactic information to keep hygiene. However, such information is hard to read, and not suitable when you just want to expand a macro in REPL to check its result. So, by default, these procedures strips syntactic information. For the identifiers introduced in the macro, it renames them to avoid name conflicts.
The following example expands my-letrec
macro
(see Tracing macro expansion, for the definition) and
results shows temporary variable introduced by the macro (newtemp)
to be renamed.
(macroexpand '(my-letrec [(ev? (^n (if (= n 0) #t (od? (- n 1))))) (od? (^n (if (= n 0) #f (ev? (- n 1)))))] (ev? 3))) ⇒ (let ((ev? (quote undefined)) (od? (quote undefined))) (let ((newtmp.0 (^n (if (= n 0) #t (od? (- n 1))))) (newtmp.1 (^n (if (= n 0) #f (ev? (- n 1)))))) (set! ev? newtmp.0) (set! od? newtmp.1) (ev? 3)))
If you pass a module to the env argument, it is used as the
macro use environment. You can also pass #t
to let it use
the current runtime environment as the macro use environment.
In those cases, syntactic information in the output won’t be
stripped.
If you want to use the output of macroexpand
as a program,
e.g. embed it into another macro expansion, you need syntactic
information preserved.
Fully expand macros inside form. The result only contains function calls and Gauche’s built-in syntax.
By default, or #t
is passed to env,
the form is assumed to be a toplevel form within the current runtime
module.
You can also pass a module to env to specify
the alternative toplevel environment.
Any local variables introduced in form is renamed to avoid collision.
Since each local variable has unique name, all let
forms become
letrec
forms (we can safely replace let
with letrec
if no bindings introduced by let
shadows outer bindings.)
NB: If a macro in form inserts a reference to a global variable
which belongs to other module, the information is lost in the current
implementation. There are a few ways to address this issue; we may
leave such reference as an identifier object, convert it to
with-module
form, or introduce a special syntax to represent
such case. It’s undecided currently, so do not rely too much on
the current behavior. For the time being, it’s best to use this
feature only for interactive macro testing.
(macroexpand-all '(letrec-syntax [(when-not (syntax-rules () [(_ test . body) (if test #f (begin . body))]))] (let ([if list]) (define x (expt foo)) (let1 x 3 (when-not (bar) (if x)))))) ⇒ (letrec ((if.0 list)) (letrec ((x.1 (expt foo))) (letrec ((x.2 '3)) (if (bar) '#f (if.0 x.2)))))