The code:
defmodule MyLib do
def get_line(meta) do
Keyword.get(meta, :line)
end
def annotate_parser_ast(ast, line, rule) do
# This is stupid because we assume the input is a list of pairs,
# while the output is a list of 3-tuples.
# In the real version, the input will also be a list of 3-tuples.
for {key, val} <- ast do
{key, %{line: line, rule: rule}, val}
end
end
# If this is a simple assignment, annotate the parser AST with location information
defp annotate_quoted_combinator({:=, _meta, [{rule_name, meta, nil} = lhs, rhs]}) do
line = get_line(meta)
quote do
unquote(lhs) = MyLib.annotate_parser_ast(unquote(rhs), unquote(line), unquote(rule_name))
end
end
# Else do nothing
defp annotate_quoted_combinator(something_else) do
something_else
end
defmacro combinators(do: {:__block__, _meta, exprs}) do
annotated_exprs = Enum.map(exprs, &annotate_quoted_combinator/1)
quote do
(unquote_splicing(annotated_exprs))
end
end
end
defmodule MyParser do
import MyLib
import NimbleParsec
combinators do
comb1 = string("abc")
comb2 = string("xyz")
comb3 =
choice([
comb1,
comb2
])
end
IO.inspect(comb1, label: "comb1")
IO.inspect(comb2, label: "comb2")
IO.inspect(comb3, label: "comb3")
end
When you compile this code, you’ll get the following output:
Compiling 1 file (.ex)
comb1: [{:string, %{line: 43, rule: :comb1}, "abc"}]
comb2: [{:string, %{line: 45, rule: :comb2}, "xyz"}]
comb3: [
{:choice, %{line: 47, rule: :comb3},
[
[{:string, %{line: 43, rule: :comb1}, "abc"}],
[{:string, %{line: 45, rule: :comb2}, "xyz"}]
]}
]
Basically this adds a metadata field to the combinators. That way, if there is a problem, the compiler used by defparsec
can give a nice error message containing the line number and the name of the “rule” (I’m stealing the name from ExSpirit). For this to work, the metadata field must be added to the combinators (I haven’t read enough of the nimble_parsec implementation to see if it this would be feasible, but it looks like it should be.
The idea is that the combinators
macro just fills the metada in the parser AST. It does so by cleverly mixing functions and macros to control which parts of the (elixir) AST are evaluated and each ones are retuned as is.