Continuation passing style and compile-time computation

n AlgebraicJulia, we have a repeated problem that shows up in a number of places, where we want to pass values computed by one macro into another macro.

One classic example of this sort of problem is that we often want to generate struct definitions. But the “spec” for those structs might be generated compositionally, i.e. we don’t want to write down the entire spec in the body of the macro.

The problem is, macros only get access to the syntax that you pass in, so there’s no way to pass in values… right?

The way we currently do this is with eval. But calling eval on a struct definition seems icky.

I recently thought of another way. The only way to pass values into the “compile time context” (i.e. the context of the expander) is by defining macros. So… to output the result of a macro, we simply expand into a macro definition. But we can’t just pass in a macro as an argument to another macro, because the outer macro expands first. So we use a little trick based on continuation passing.

The idea is this.

We start out with a macro call @f(@a, 3). We this expands into a macro call @a(@f_internal(3)). Then this expands into a macro call @f_internal(4, 3). Basically, @f takes its first argument, and passes “what to do with its value” to it. Then @a takes in an expression, and inserts its value into the first argument. Then finally, @f_internal runs, and it has access to the value inserted in by @a.

This is kind of janky, because you have to trust that @a(@f_internal(3)) will actually just expand into @f_internal(4, 3) or whatever. But @a could be an arbitrary macro, so this could not do at all what you expected. So I don’t know how practical this is to actually use. But it does show how you can inject things into the compile time context, if all you’ve got is the ability to define macros.

I would much prefer to have proper compile-time variables, ala Zig, but I doubt those will be added to Julia any time soon…

Here’s a toy implementation of this pattern: Macrology.jl · GitHub

If @f knows about @a, it can just rewrite this to call the macro expansion in @a. It doesn’t need @eval.

For example:

a(expr) = ...transform expr...
macro a(expr)
    return a(expr)
end
macro f(expr)
    if Meta.isexpr(expr, :macrocall) && expr.args[1] == Symbol("@a")
        expr = a(expr.args[end]) # expand @a macro call
    end
    return ...some transformation of expr...
end
2 Likes

Ah, but the whole point is that @a is generated by a previous macro call. So f doesn’t know about @a until macro expansion time.

I’m not sure I completely understand your problem, but if you want the inner macro to expand first, nothing prevents you to manually trigger the macro-expansion. To expand on your “Macrology” gist:

julia> macro add(x,y)
         x+y
       end
@add (macro with 1 method)

julia> @macroexpand @add 1 2
3

julia> macro x()  9  end
@x (macro with 1 method)

# Does not work because @x is not expanded first
julia> @macroexpand @add (@x) 3
ERROR: MethodError: no method matching +(::Expr, ::Int64)
julia> macro add2(x, y)
          # Explicitly trigger the expansion of x first
          macroexpand(__module__, x) + y
       end
@add2 (macro with 1 method)

julia> @macroexpand @add2 (@x) 3
12

EDIT: here is a real-life example where the @timeit macro (from TimerOutputs.jl) explicitly macroexpands the expression on which it is called:

1 Like

Ohhh I didn’t know I was looking for this, but this is exactly what I was looking for! Thanks @ffevotte!