Macro passes
Our Big Friendly Macro is implemented in a series of passes. Each pass looks for items in the module that are transformed into different syntax, like structs, mixins and constructors. These passes are made composable so that they run in a single loop.
We use the prewalk function (from MacroTools.jl) to transform expressions and collect information into searchable data structures. We make a little abstraction over the prewalk function, so we can compose multiple transformations in a single tree walk.
An implementation of the pass function should take a Pass object and an expression (or symbol), and return no_match if the expression did not match the pattern.
Types that derive from Pass can be added into a composite CompositePass using the + operator.
#| file: src/Passes.jl
module Passes
using MacroTools: prewalk
export Pass, pass, no_match, walk
abstract type Pass end
struct NoMatch end
const no_match = NoMatch()
"""
pass(x::Pass, expr)
Interface. An implementation of the `pass` function should take a `Pass` object
and an expression (or symbol), and return `no_match` if the expression did not
match the pattern.
You can use the given `Pass` object to store information about this pass, return
syntax that should replace the current expression, or `nothing` if it should be
removed.
"""
function pass(x::Pass, expr)
error("Can't call `pass` on abstract `Pass`.")
end
struct CompositePass <: Pass
parts::Vector{Pass}
end
Base.:+(a::CompositePass...) = CompositePass(splat(vcat)(getfield.(a, :parts)))
Base.convert(::Type{CompositePass}, a::Pass) = CompositePass([a])
Base.:+(a::Pass...) = splat(+)(convert.(CompositePass, a))
"""
pass(x::CompositePass, expr)
Tries all passes in a composite pass in order, and returns with the first
that succeeds (i.e. doesn't return `no_match`). You may create a `CompositePass`
by adding passes with the `+` operator.
"""
function pass(cp::CompositePass, expr)
for p in cp.parts
result = pass(p, expr)
if result !== no_match
return result
end
end
return no_match
end
"""
walk(x::Pass, expr_list)
Calls `MacroTools.prewalk` with the given `Pass`. If `no_match` is returned,
the expression stays untouched.
"""
function walk(x::Pass, expr_list)
function patch(expr)
result = pass(x, expr)
result === no_match ? expr : result
end
prewalk.(patch, expr_list)
end
endA composite pass tries all of its parts in order, returning the value of the first pass that doesn't return no_match.
Tests
test/PassesSpec.jl
#| file: test/PassesSpec.jl
@testset "ModuleMixins.Passes" begin
using ModuleMixins.Passes: Passes, Pass, no_match, pass, walk
<<test-passes>>
endWe define a small pass that replaces some symbol with blip!.
#| id: test-passes
struct BlipPass <: Pass
tag::Symbol
end
Passes.pass(p::BlipPass, expr) = expr == p.tag ? :blip! : no_matchWe can test that this works on small tuple expression.
#| id: test-passes
@testset "pass replacement" begin
@test walk(BlipPass(:a), [:(a, b)])[1] == :(blip!, b)
@test walk(BlipPass(:b), [:(a, b)])[1] == :(a, blip!)
endAnd then that it composes to replace both elements in the tuple.
#| id: test-passes
@testset "pass composition" begin
a = BlipPass(:a) + BlipPass(:b)
@test walk(a, [:(a, b)])[1] == :(blip!, blip!)
end