Macro Plan
No Lisp is complete without a macro system. If only it were so easy to implement.
Having never written a language before, let alone a macro system, I’m going to try to break it down into manageable pieces. I have no idea if this is the right approach, but we’ll start with it and see where it takes us.
The steps I’m envisioning are
- Parse the code.
- Find instances of
defmacro
. - Find invocations of each of those macros.
- Convert the output of the parser into an Erie abstract syntax tree.
- Execute each macro with the AST as the argument.
- Support “unquoting” somehow.
- Replace the macros in the AST with their results.
- Repeat until all macros have been expanded.
The simplest macro I can think of is comment
: replace the inner forms with a single nil
.
(defmacro comment [ast]
nil)
(def loop []
(comment
(Enum.map [1 2 3]
(lambda [x]
(+ 1 x)))))
Step One
After parsing, we have this.
[
[
{:atom, 1, :defmacro},
{:atom, 1, :comment},
{:list, 1, [{:atom, 1, :ast}]},
{:atom, 2, nil}
],
[
{:atom, 4, :def},
{:atom, 4, :loop},
{:list, 4, []},
[
{:atom, 5, :comment},
[
{:symbol, 6, :"Enum.map"},
{:list, 6, [{:integer, 6, 1}, {:integer, 6, 2}, {:integer, 6, 3}]},
[
{:atom, 7, :lambda},
{:list, 7, [{:atom, 7, :x}]},
[{:atom, 8, :+}, {:integer, 8, 1}, {:atom, 8, :x}]
]
]
]
]
]
Step Two
We can find instances of defmacro
just like we find instances of def
. In this case we only find one: comment
.
Step Three
We find inovcations of comment
by looking through the results of the parser for anything that looks like a function call to comment
.
Step Four
Converting the output of the parser into an Erie abstract syntax tree will look something like this.
[
[:defmacro, :comment, [:ast], nil],
[:def, :loop, [],
[:comment, [:"Enum.map", [1, 2, 3], [:lambda, [:x], [:+, 1, :x]]]]
]
]
Step Five
To execute the comment
macro, we need to complie it as if it were a normal function. That is possible with :compile.forms/1
, but as far as I know, it needs to be contained in a module.
Current strategy is to define all macros as functions in their “macro” module which will be prefixed with MACRO
. E.g. the module Erie.Core
’s macros will be defined as functions in MACRO.Erie.Core
.
Step Six
TBD.
Step Seven
The result of the call to comment
is nil
, so we can replace that list in the parser output with nil
. We’ll also remove the macro definition which should leave us with this.
[
[
{:atom, 4, :def},
{:atom, 4, :loop},
{:list, 4, []},
{:atom, 5, nil}
]
]
Step Eight
In this case nothing. comment
doesn’t need any further expansion, so we can now continue the pipeline and pass this to the translator stage which will produce Erlang Abstract Form for final compilation.