Testing a string macro

The code

using Test

xx = Dict{String,String}()

macro x_str(str,suf)
    return str * xx[suf]
end

@testset "a" begin
    xx["a"] = "apple"
    @test x"tasty "a == "tasty apple"
end

Throws an error:

ERROR: LoadError: KeyError: key "a" not found

and so far I have corrected it by replacing the second last line with:

    @test eval(Meta.parse("""x"tasty "a""")) == "tasty apple"

Is there a better way to do this?

This is basically a confusion about what macros do and when they run, and what they are supposed to return.

The problem is that your string macro body executes when shortly after the code is parsed (technically, when the code is “lowered”), which happens before your @testset code runs. So, the line xx["a"] = "apple" hasn’t executed yet, and the dictionary lookup fails.

A solution is to change your macro definition to return an expression that constructs the desired string, so that it doesn’t actually execute the xx[suf] lookup when the macro is lowered (when the macro body runs), but only when the corresponding line is executed:

macro x_str(str,suf)
    return :($str * $xx[$suf])
end
1 Like

There are several level of problems here.

A macro, including string macros, are supposed to return expressions, not do the evaluation themselves. Any evaluation done in a macro is done at parse time.

The code you wrote proceeds similar the following order. Because you try to evaluate xx["a"] before you set it, you get the error that you get.

xx = Dict{String,String}()
temp = "tasty " * xx["a"]
ex = :(@testset "a" begin
    xx["a"] = "apple"
    @test $temp == "tasty apple"
end)
eval(ex)

Rather your macro should return an expression that will be evaluated in the normal order.

julia> macro x_str(str, suf)
           return :($str * xx[$suf])
       end
@x_str (macro with 1 method)

julia> @testset "a" begin
           xx["a"] = "apple"
           println(xx["a"])
       end
apple
Test Summary: |Time
a             | None  0.0s
Test.DefaultTestSet("a", Any[], 0, false, false, true, 1.711649824769256e9, 1.711649824780527e9, false, "REPL[11]")

julia> @macroexpand x"tasty "a
:("tasty " * Main.xx["a"])

julia> macro x_str(str, suf)
           return :($str * xx[$suf])
       end
@x_str (macro with 1 method)

julia> let xx = Dict("a" => "orange")
           x"tasty "a
       end
"tasty apple"

Also note that with the definition I just gave, xx is Main.xx.

If I want xx to refer to something in the context of the local scope, then I would need to the following.

julia> macro x_str(str, suf)
           return esc(:($str * xx[$suf]))
       end
@x_str (macro with 1 method)

julia> let xx = Dict("a" => "orange")
           x"tasty "a
       end
"tasty orange"

julia> @macroexpand x"tasty "a
:("tasty " * xx["a"])

Edit: Added the esc in the last example.

2 Likes

Thank you @stevengj and @mkitti , I learnt some very valuable lessons, especially:

Macros often do the evaluation and return the “final” value instead of an expression. Even in base Julia, see eg r"..." and dateformat"...".

1 Like

I think this is one of those “Do as I say, not as I do” moments. Base can bend the rules, but generally you shouldn’t be using eval in a macro.

It’s not about using eval in the macro, but computing a concrete value instead of an expression that would do so later. This is also used in regular macros when literal information is available at compile time, e.g., @printf will compile the format string and does not allow it to depend on runtime information:

@printf "%d" 10  # works
let s = "%d"; @printf s 10 end  # does not work

Guess that using literal compile time information in this fashion is even more widespread in string macros.

My statement holds here. In the examples you gave, we are creating literal expressions from literal expressions. Importantly, these are usually not dependent on outside variables.

This cam have some really distinct effects that you cannot otherwise accomplish.

julia> big"5" === big"5"
false

julia> macro big5()
           return big"5"
       end
@big5 (macro with 1 method)

julia> @big5() === @big5()
true

Did you mean to insert esc here? Like this:

julia> macro x_str(str, suf)
           return esc(:($str * xx[$suf]))
       end
@x_str (macro with 1 method)

julia> let xx = Dict("a" => "orange")
           x"tasty "a
       end
"tasty orange"

julia> @macroexpand x"tasty "a
:("tasty " * xx["a"])
1 Like

Yes. I corrected the post.

1 Like

Except that esc is a hammer that should be used with care:

macro x_str(str, suf)
    return esc(:($str * xx[$suf]))
end

macro y_str(str, suf)
    return :($str * $(esc(:xx))[$suf])
end
julia> let xx = Dict("a" => "orange"), * = tuple
           @show x"tasty "a
           @show y"tasty "a
       end;
x"tasty "a = ("tasty ", "orange")
y"tasty "a = "tasty orange"
2 Likes