1
votes

To help my debugging (and also in order to better understand how Julia macros work), I'm trying to define a simple macro that sourrounds blocks of code with "Entering" and "Leaving" notifications. Here is what I've come up with so far:

macro dbg(block_title, expr)
    quote
        title = $block_title
        println("Entering $title")
        $expr
        println("Leaving  $title")
    end
end

At first glance, it seems to do what I want:

julia> @dbg "first test" begin
           println("does it work?")
       end
Entering first test
does it work?
Leaving  first test

however, as soon as variables are involved, nothing works anymore and I get UndefVarError for all variables accesses. It looks like the scope inside and outside the macro are distinct:

julia> @dbg "initialization" begin
           foo = rand(10)
           println("foo = ", foo)
       end
Entering initialization
foo = [0.9178016919066918, 0.6004694971609528, 0.5294790810682284, 0.04208146400653634, 0.09271603217172952, 0.2809448815925, 0.68236281020963, 0.8313876607106496, 0.07484095574744898, 0.14099531301938573]
Leaving  initialization

julia> foo
ERROR: UndefVarError: foo not defined

What am I doing wrong?

1

1 Answers

5
votes

In short, you're missing the notion of macro hygiene and in particular the esc function.

Although this part of the documentation is a good read for anyone wanting to develop their own macros, let's try to expand a little on what macro hygiene does in this particular example, and how you can fix things.

A useful way to debug macros is provided by @macroexpand:

julia> @macroexpand @dbg "initialization" begin
           foo = rand(10)
           println("foo = ", foo)
       end
quote
    #= REPL[1]:3 =#
    var"#32#title" = "initialization"
    #= REPL[1]:4 =#
    Main.println("Entering $(var"#32#title")")
    #= REPL[1]:5 =#
    begin
        #= REPL[5]:2 =#
        var"#33#foo" = Main.rand(10)
        #= REPL[5]:3 =#
        Main.println("foo = ", var"#33#foo")
    end
    #= REPL[1]:6 =#
    Main.println("Leaving  $(var"#32#title")")
end

Leaving aside all comments within #= ... =# markers, we see almost the code we wanted to get: the user code block has been surrounded with statements printing the "Entering" and "Leaving" notifications. However, there is one notable difference: variable names like foo or title have been replaced with weirdly looking names like var"#33#foo" or var"#32#title". This is what is called "macro hygiene", and it helps avoiding clashes that could occur between the variables used in the macro itself (like title in this example), and the variables used in the code block provided as an argument to the macro (like foo here). (Think for example what would happen if you used you @dbg on a code block that defines a title variable.)

Julia errs on the side of caution, and protects in this way all variables appearing in the macro. If you want to disable this for selected parts of the expression produced by the macro, you can wrap these subexpressions inside the esc function. In your example, you should for example escape the user-provided expression:

macro dbg(block_title, expr)
    quote
        title = $block_title
        println("Entering $title")
        $(esc(expr))
        println("Leaving  $title")
    end
end

Now things should work like want them to:

julia> @dbg "initialization" begin
           foo = rand(10)
           println("foo = ", foo)
       end
Entering initialization
foo = [0.2955287439482881, 0.8989053281359838, 0.27751430906108343, 0.4920810199867245, 0.7633806735297282, 0.34535540650110597, 0.7099231627594489, 0.39978144801175564, 0.9104888704503833, 0.1983996781283539]
Leaving  initialization

julia> @dbg "computation" begin
           foo .+= 1
       end
Entering computation
Leaving  computation

julia> foo
10-element Array{Float64,1}:
 1.295528743948288 
 1.8989053281359838
 1.2775143090610834
 1.4920810199867245
 1.7633806735297282
 1.345355406501106 
 1.709923162759449 
 1.3997814480117556
 1.9104888704503833
 1.198399678128354