Elixir, Executing AST
Inspecting AST is a good way to learn about elixir macros. Executing AST can also be instructional, but it is a little more nuanced than directly executing elixir code. This post covers executing AST with Code.eval_quoted(). Note that outside of experimentation, executing AST with Code.eval_quoted() is probably the wrong thing to do.
Thanks to everyone in #elixir-lang on Freenode who answered my questions about executing AST.
Software Versions
$ date
February 1, 2016 at 04:49:00 AM JST
$ uname -vm
FreeBSD 11.0-CURRENT #0 r287598: Thu Sep 10 14:45:48 JST 2015 root@:/usr/obj/usr/src/sys/MIRAGE_KERNEL amd64
$ elixir --version
Erlang/OTP 18 [erts-7.2.1] [source] [64-bit] [async-threads:10] [hipe] [kernel-poll:false]
Elixir 1.2.2
Instructions
This is a single file example of a naive solution that does not work. Put the following in ast.exs.
#!/usr/bin/env elixir
defmodule MacroLibrary do
# a simple macro that delegates work to a complex function
defmacro process_macro(input) do
quote do
process_function(unquote(input))
end
end
# the complex function called by the macro
def process_function(input) do
input
|> Enum.each(&IO.puts/1)
end
end
defmodule Script do
import MacroLibrary # important
def main(args) do
# call the macro directly
process_macro(["Args:" | args])
|> IO.puts
# macro AST
ast = quote do
process_macro(["Args:" | args]) # BROKEN line 28
end
# display the AST
ast
|> Macro.to_string
|> IO.puts
# display the expanded AST
ast
|> Macro.expand(__ENV__)
|> Macro.to_string
|> IO.puts
# execute the AST
{result, _binding} = ast
|> Code.eval_quoted # BROKEN line 44
# display the result
result
|> IO.puts
end
end
# run the script
Script.main(System.argv)
Make the script executable.
$ chmod +x ast.exs
This is the expected outputed.
$ ./ast.exs a b c
Args:
a
b
c
ok
process_macro(["Args:" | args])
process_function(["Args:" | args])
Args:
a
b
c
ok
This is the actual output.
$ ./ast.exs a b c
Args:
a
b
c
ok
process_macro(["Args:" | args])
process_function(["Args:" | args])
** (CompileError) nofile:1: undefined function process_function/1
expanding macro: MacroLibrary.process_macro/1
nofile:1: (file)
What happened?
Directly invoking the macro works as expected. The naive expectation is that the AST should just execute because the code is the same as the direct invocation.
The problem is that context is missing. The AST evaluates as process_macro([“Args:” | args]) which expands to process_function([“Args:” | args]). process_function is defined in the MacroLibrary module. The macro library is included in the Script module. That information is stored in the current context. Without the context, Code.eval_quoted does not know where to find process_function.
Change line 44 to the following:
|> Code.eval_quoted([], __ENV__) # BROKEN line 44
There is another problem.
$ ./ast.exs a b c
Args:
a
b
c
ok
process_macro(["Args:" | args])
process_function(["Args:" | args])
** (CompileError) ast.exs:44: undefined function args/0
expanding macro: MacroLibrary.process_macro/1
ast.exs:44: Script (module)
When executing the macro directly, args resolves properly. Bindings are not part of the environment context and need to be passed in separately.
Change line 44 to the following:
|> Code.eval_quoted([args: args], __ENV__) # CORRECT line 44
This did not fix the problem. The macro can be directly invoked with process_macro([“Args:” | args]), but executed AST needs to use var!() to access variables. This means that the direct macro call and the executable AST need to be slightly different.
Change line 28 to the following:
process_macro(["Args:" | var!(args)]) # CORRECT line 28
The example finally works.
$ ./ast.exs a b c
Args:
a
b
c
ok
process_macro(["Args:" | var!(args)])
process_function(["Args:" | var!(args)])
Args:
a
b
c
ok
AST needs to be defined and executed as below.
# direct invocation
result = process_macro(["Args:" | args])
# indirect invocation
ast = quote do
process_macro(["Args:" | var!(args)])
end
{result, _binding} = ast
|> Code.eval_quoted([args: args], __ENV__)