DSL help with nested macros

Hi all. I’m learning how to implement DSL using macros and some help would be appreciated. In particular, I would like to create a DSL for L-System.

The purpose of DSL is clean syntax. So here’s what I’m thinking in regards to the algae example:

@lsys begin
    @start A
    @rule A = AB
    @rule B = A
end

and I would like to transform it into the following Julia code:

model = LModel("A")
add_rule(model, "A", split("AB", ""))
add_rule(model, "B", split("A", ""))

I can create the @start and @rule macros but I’m not sure how to pass the model variable around. The @start macro generates a random variable name for model and likewise for @rule macro.

See LSystem.jl repo for the current implementation. I am open to suggestions including tweaking the DSL itself to make it easier. Thanks

Do I understand correctly, that the main questions are:

  1. How to capture the name of the model variable created by var = LModel(name) expression?
  2. How to pass this name to add_rule() calls?

If so, here’s one simple way to implement @lsys which does the trick:

# let's say, @start and `rule` generated the following expression which is passed to @lsys
ex = quote 
       # say, @start generated name `model_123`
       model_123 = LModel("A")
       # initially using dummy name __model_var__
       add_rule(__model_var__, "A", split("AB", "")) 
       add_rule(__model_var__, "B", split("A", ""))
end

using Espresso

# Espresso.findex() traverses AST and finds all subexpressions 
# matching the pattern "_var = LModel(_)"
# (any symbol starting with underscore is considered a placeholder)
model_subexs = findex(:(_var = LModel(_)), ex)
# ==> 1-element Array{Any,1}:
# ==>  :(model_123 = LModel("A"))

# model variable is in 
model_var = model_subexs[1].args[1]
# ==> :model_123

# now we can substitute all occurrences of `__model_var__` with actual model var name
subs(ex, Dict(:__model_var__ => model_var))
# ==> quote
# ==>     model_123 = LModel("A")
# ==>     add_rule(model_123, "A", split("AB", ""))
# ==>     add_rule(model_123, "B", split("A", ""))
# ==> end
1 Like

That’s a great idea! Thanks!

By the way, since I’m already using MacroTools, I took your idea and implemented using @capture and postwalk.

1 Like

Good to hear there will be an L-systems package, what’s your use case?

Not a real use case but I’m planning to use it as an example in my upcoming book Hands On Design Patterns for Julia :slight_smile:

1 Like

Another approach would be to mimic how it’s done in @sync and @async (i.e., create a binding with special gensym'ed variable sync_varname in @sync using let).

It would be something like

const model_varname = gensym(:model)

macro lsys(block)
    var = esc(sync_varname)
    quote
        let $var
            $(esc(block))
        end
    end
end

macro start(name)
    var = esc(model_varname)
    quote
        $var = LModel($(esc(name)))
    end
end

and so on (untested). You can also use @isdefined macro in @start to throw a user-friendly error when it’s used outside @lsys.