Help writing macro which returns an eval expression

I have a macro which is expected to return a Core.eval expression, in the line of:

macro foreach(f, arr)
  quote
    Core.eval(__module__,
    quote
      isempty($(esc(arr))) && return ""

      mapreduce(*, $(esc(arr))) do _s
        @show @__MODULE__
        $f(_s) * "\n"
      end
    end)
  end
end

However, I don’t know how to escape the arr argument all the way into the nested quote, resulting in ERROR: UndefVarError: arr not defined.

Thanks!

Maybe something like this, where the quoted expression is built first, then spliced into the macro result (I also had to replace __module__ with @__MODULE__ and to escape f):

macro foreach(f, arr)
    e = quote
        isempty($(esc(arr))) && return ""
        
        mapreduce(*, $(esc(arr))) do _s
            @show @__MODULE__
            $(esc(f))(_s) * "\n"
        end
    end
    
    quote
        Core.eval(@__MODULE__, $e)
    end
end

This expands to:

julia> let
           fun(x) = "$x"
           x = [1,2,3]
           @macroexpand @foreach fun x
       end
quote
    #= REPL[1]:12 =#
    (Main.Core).eval(Main, begin
            #= REPL[1]:3 =#
            Main.isempty(x) && return ""
            #= REPL[1]:5 =#
            Main.mapreduce(Main.:*, x) do #1#_s
                #= REPL[1]:6 =#
                begin
                    Base.println("#= REPL[1]:6 =# @__MODULE__() = ", Base.repr(begin
                                #= show.jl:576 =#
                                #2#value = Main
                            end))
                    #2#value
                end
                #= REPL[1]:7 =#
                fun(#1#_s) * "\n"
            end
        end)
end

and seems to work at runtime too:

julia> let
           fun(x) = "$x"
           x = [1,2,3]
           @foreach fun x
       end
#= REPL[1]:6 =# @__MODULE__() = Main
#= REPL[1]:6 =# @__MODULE__() = Main
#= REPL[1]:6 =# @__MODULE__() = Main
"1\n2\n3\n"

(but I’m not sure this is what the macro was meant to do…)

1 Like

But I have to ask: why the eval stage, and not simply:

macro foreach(f, arr)
    quote
        isempty($(esc(arr))) && return ""

        mapreduce(*, $(esc(arr))) do _s
            @show @__MODULE__
            $(esc(f))(_s) * "\n"
        end
    end
end
1 Like

Thanks!

I actually meant __module__ – as that’s the point of the macro, to obtain a reference to the module where it’s invoked.

As for the reason, it’s quite complex. The simple code you propose at the end is what I have been using for a while, but doesn’t work entirely. It’s for an HTML based templating language.

There’s a module called Flax which is part of an external package (Genie) which can read an HTML template file with embedded Julia code. Flax knows how to parse the HTML code and dynamically execute the Julia code in order to include parts of other views, add logic based of conditionals, loops, etc. Ultimately, it “executes” this code to generate the resulting HTML which is sent to the browser.

But these view files are defined and loaded into the user app, usually in a Controller module (or in some external Julia file). And Flax needs access to the variables from the view file, which are defined in the user file (in the Controller). So Flax evals the template in the Controller’s context, so the template code has access to the variables. It all works great with the exception of this @foreach macro, which because is a macro, does not get evaled within the Controller’s scope but within Flax itself.

Hence my need to grab __module__ (which is the Controller) and have the expression evaling in __module__ scope.

I hope that makes sense.


Example of a Flax template:

<table class="table table-striped">
<% @foreach(@vars(:translations)) do t %>
    <tr>
      <td style="width: 45%" id="original_$(t.id)" data-action="original">$(t.original |> escapeHTML)</td>
      <td><button class="btn btn-outline-secondary btn-sm" title="Copy" data-action="copy">↪</button></td>
      <td style="width: 45%" data-action="editor">
        <textarea name="translation_$(t.id)" class="form-control">$( getfield(t, Symbol(@vars(:locale))) )</textarea>
      </td>
      <td><button class="btn btn-outline-secondary btn-sm" data-action="save">Save</button></td>
    </tr>
<% end %>
</table>

This works well, but the problem is, if there is a variable x defined outside the @foreach inner block, it’s not accessible inside the block, as the code runs in a different scope.


I will give the nested code a try, thank you!

Thanks, I did not know about __module__… Is it documented somewhere ? Is it specific to Flax or Genie?

In any case, if you’re evaling the code you produce in another module, how is that module going to access the data you provide to the macro (e.g. arr)?

It’s Julia - the __module__ variable is automatically made available by Julia within the macro:
https://docs.julialang.org/en/v1/manual/metaprogramming/index.html#Macro-invocation-1


If the view needs to access variables from the module, I just pass the module explicitly.

Ex:

julia> x = 100
julia> view = raw"<div>$x</div>"
julia> Genie.Render.html(view, context = @__MODULE__)

However, the preferred approach is to pass the variables explicitly, ie:

julia> view = raw"<div>$x</div>"
julia> Genie.Render.html(view, x = 100)

Both invocations work.


Update 1 - sorry, I meant __module__ not __macro__ - edited!

1 Like

@ffevotte Yes, victory, thank you!

All I had to do is introduce __module__ instead of @__MODULE__:

macro foreach(f, arr)
  e = quote
    isempty($(esc(arr))) && return ""

    mapreduce(*, $(esc(arr))) do _s
      @show @__MODULE__
      $(esc(f))(_s)
    end
  end

  quote
    Core.eval($__module__, $e)
  end
end

Here is the code being used :slight_smile:


julia> using Genie.Renderer

julia> y = 100
100 

julia> view = raw"""
       <ol>
       <% @foreach(["a", "b", "c"]) do letter %>
       <li>$(letter) $(y)</li>
       <% end %>
       </ol>"""
"<ol>\n<% @foreach([\"a\", \"b\", \"c\"]) do letter %>\n<li>\$(letter) \$(y)</li>\n<% end %>\n</ol>"

julia> html(view)
#= /Users/adrian/.julia/dev/Genie/src/Flax.jl:318 =# @__MODULE__() = Genie.Renderer
ERROR: UndefVarError: y not defined

julia> html(view, context = @__MODULE__)
#= /Users/adrian/.julia/dev/Genie/src/Flax.jl:318 =# @__MODULE__() = Main
HTTP.Messages.Response:
"""
HTTP/1.1 200 OK
Content-Type: text/html; charset=utf-8

<html><head></head><body><ol><li>a 100</li><li>b 100</li><li>c 100</li></ol></body></html>"""

Great stuff, thanks so much!

2 Likes

Thanks for the explanations about __module__. And glad I could be of help!

2 Likes