Continuation passing style and compile-time computation

In 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?

Well, it turns out there is a way around this. 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…