Search code examples
macrosjuliametaprogramminghygiene

Passing macro variable to function for interpolation


I'm trying to write a macro that evaluates an expression and then compares it with a few values. I have reduce the problem to a smaller example for this post.

macro small_bad(item)
    quote
        $(use_val(esc(item)))
    end
end

function use_val(val)
    quote
        if $val == 1
            1
        elseif $val == 2
            2
        else
            -1
        end
    end
end

Because I don't want to evaluate the expr more than once, I want to save it in a variable. So I tried this:

macro small_good(item)
    quote
        begin
            val = $(esc(item))
            $(begin
                  use_val(val)
              end)
        end
    end
end

But then I get that val is undefined in the interpolation in @small_good.

I also tried passing use_val(:val) but this also fails because the macro system will rename val to something else.

How can I achieve this?

EDIT: Given the first answer I tried this in my actual code

macro match(item, arms)
    var = gensym(:var)
    quote
        let $var = $(esc(item))
            $(begin
                code = :nothing
                for e in reverse(filter((e) -> e isa Expr, arms.args))
                    code = make_match(var, e, code)
                end
                code
            end)
        end
    end
end

and got UndefVarError: ##var#253 not defined

gist with the full code here

Disclaimer: I know the @match macro is already implemented in the Match.jl package, I'm reimplementing a subset of it as a learning exercise

EDIT 2:

I figured it out. After using François Févotte's suggestion I now had to change my real version of use_val which was actually doing $(esc(val)) instead of $val.

Mistake on my part for not including that detail. Will update the gist to reflect this


Solution

  • If I understand what you want, this should work:

    macro small(item)
        var = gensym(:var)
        quote
            let $var = $(esc(item))
                $(use_val(var))
            end
        end
    end
    
    function use_val(val)
        quote
            if $val == 1
                1
            elseif $val == 2
                2
            else
                -1
            end
        end
    end
    

    It expands to something like:

    julia> using MacroTools
    julia> MacroTools.@expand @small myexpr
    quote
        let octopus = myexpr
            begin
                if octopus == 1
                    1
                elseif octopus == 2
                    2
                else
                    -1
                end
            end
        end
    end
    

    and has issues with neither hygiene nor multiple evaluations:

    # Testing in a local scope introduced by `let`
    # is a good way to check for hygiene issues
    julia> let arg = [1]
               @small pop!(arg)
           end
    1
    




    Now I'm guessing a lot of the substance of your original problem has been lost during the MWE creation process, because all this is essentially equivalent to:

    julia> function small(val)
               if val == 1
                   1
               elseif val == 2
                   2
               else
                   -1
               end
           end
    small (generic function with 1 method)
    
    julia> let args = [1]
               small(pop!(args))
           end
    1