How can I insert an AST as a function body?

I’m generating an AST that represents a function body (use case is converting CLDR plural rules and generating lookup functions). The principle looks like:

defmodule Fun do
  # AST representation of i
  function_body = {:i, [], Fun}
  
  def test(i), do: unquote(function_body)
end

But if I compile this I get “undefined function i/0”. Which suggests that there is a scope difference between the function body and the function head. Any ideas how to correctly insert a an AST as a function body?

2 Likes

defmacro’s are the only way to play with raw AST like that as far as I’ve seen.

2 Likes

You can totally manipulate an AST using functions. After all, an Abstract Syntax Tree in Elixir are just three-argument tuples*.

(* bar a few literals, see iex> h quote for details)

But if you want to grab something from an external context, or apply something to an external context, you need to use a Macro. A Macro will, instead of normal parameters, receive them in AST-format, so you can manipulate them before evaluating them (if at all).

Your example goes wrong for another reason than for using a function instead of a macro. Any kind of AST (using e.g. an AST-representation as in your example) is, by default, hygienic. This means that it does not have access to things defined in the outer scope.
The simplest way to bring an outer variable inside the scope, that is often most appropriate in these kinds of situations, is to use some unquote statement inside a quote This will first replace the variable named in the unquote with its value, and then the outer quote will use its value instead. An example would be:

def Foo
  defmacro test(x)
    quote do
      "%The result of #{Macro.to_string(x)} is #{unquote(x)}"
  end
end

If you actually want to grab the value of a variable in the external context in a macro, you can use var!. Do note that while this can be convenient in some cases, it does break the principle of least surprise and functional purity, because you are now writing code that depends on something outside of its own definition.

To answer your question:

  • There is no difference in scope between the parameters of a function and its body.
  • There is a difference in scope between quoted code inside a macro and code outside of this quote-block.
  • There is a difference in scope between the inside of a macro, and the location it is used in, but a macro can cheat by using var! to access things outside of itself.

As to why your example above doesn’t work:
The AST you’re presenting is not the AST of a local variable i. Instead, it is the AST describing an identifier i in the context of the module Fun (i.e. outside of the function definition of test) which might resolve to a variable or to a zero-arity function (In this case, it cannot find either, so it raises an error). If you want to refer to a local variable, you should use {:i, [], nil}. Or, even better would be to just write the code using quote do ... end, which would be a lot more readable.

2 Likes

Qqwy, thanks for the very considered response. I’m still not there yet so let me explain my use case. I’m working on a Cldr implementation and I’m parsing the plural rules and converting (leex/yecc) the plural rules syntax into an elixir AST. Hence why I have a function body that is already in AST format. I’m just trying to bolt an AST function body to a function head.

If I go the defmacro route then I can pass in the value of the variable to the macro using var! as you point out. But then when I unquote it to insert into the macro it inserts the AST literal, not the AST as code. For example:

defmodule Fun do
  defmacro funtest(var!(body)) do
    quote do
      def test(i), do: unquote(body)
    end
  end
end

defmodule Fun2 do
  import Fun
  funtest {:i, [], nil}
end

iex(22)> Fun2.test 123
{:i, [], nil}

Right now I am building the entire AST of the function (head and body) and then Code.eval_quoteing it. Which also doesn’t seem optimal (my actual code below)

  Enum.each configured_locales, fn (locale) ->
    function_body = cardinal_rules[locale] |> rules_to_condition_statement(__MODULE__)
    function = quote do
      defp do_cardinal(unquote(locale), n, i, v, w, f, t), do: unquote(function_body)
    end
    Code.eval_quoted(function, [], __ENV__)
  end

I’m very open to a better way to do this!

1 Like

I think you might find this useful. I use that within combine to generate multiple functions from a single definition. I don’t know that it’s a 1:1 match for what you are trying to achieve, but at the very least it will be educational on abusing macros to generate function definitions :wink:

1 Like

Bit walker, thanks - whole bunch of fun stuff there to play with :slight_smile:

1 Like

@kip your code is 99% correct. The part that you are missing is that variables defined outside of a quote have no context (i.e. nil).

Let’s prove this:

iex> defmodule Sample do
...>   defmacro sample(ast), do: IO.inspect ast
...> end
iex> import Sample
Sample
iex> sample a + b
{:+, [line: 4], [{:a, [line: 4], nil}, {:b, [line: 4], nil}]}

Therefore, if you use nil instead of Fun when defining your variable ({:i, [], nil}), your code should work since you want to match on the variable i which was defined outside of a quote.

TL;DR - Only variables defined inside a quote have the context set.

3 Likes

José, thanks. Makes sense. But I’m still stuck trying to work out how to insert an AST as a function body in a macro. I think (but open to correction) that the general case of parsing (leex/yecc) into an elixir AST seems a reasonable strategy. And for the CLDR implementation I’m working on there are several grammars that seem open to this approach (plural rules, decimal formats, rbnf, …) After parsing I want to insert that generated AST as a function body.

defmodule Fun do
  defmacro test(body) do
    def test, do: body
  end
end

Doesn’t work of course since def needs to be quoteed.

defmodule Fun do
  defmacro test(body) do
    quote do
      def test, do: unquote(body)
    end
  end
end

Doesn’t work because the unquoted body inserts the AST literal rather than the AST as code.

So think I’m still at the point where i need to build the entire AST (function head + function body) and Code.eval_quoted/1.

I feel like I’m missing something simple?

2 Likes

I might be misunderstanding what you’re trying to do, but this works, using your second code snippet:

defmodule Fun do
  defmacro test(body) do
    quote do
      def test, do: unquote(body)
    end
  end
end


defmodule Bar do
  Fun.test(IO.puts("This will be printed during execution"))
end

iex> Bar.test
This will be printed during execution
:ok

Is this not the format you want?

The only other thing I can think of, is that you want to return the AST of the body passed to the macro, when test is invoked. If you want that, the function you need is Macro.escape:

defmodule Fun do
  defmacro test(body) do
    quote do
      def test, do: unquote(Macro.escape(body))
    end
  end
end


defmodule Bar do
  Fun.test(IO.puts("This will be printed during execution"))
end
iex> Bar.test
{{:., [line: 67], [{:__aliases__, [counter: 0, line: 67], [:IO]}, :puts]},
 [line: 67], ["This will be printed during execution"]}

1 Like

As for your earlier , real-life example where you are using Code.eval_quoted. I think you can do something similar to this (code has been simplified as I do not know your implementations of some of the functions you’re referring to, and they do not matter to explain what is going on):

defmodule LocaleTest do
  Enum.each ~w{nl de en fr}, fn (locale) ->
    function_body = quote do IO.puts(unquote(locale)) end # Replace this with any other quote-block.
    def cardinal(unquote(locale)), do: unquote(function_body)
  end
end

iex> LocaleTest.cardinal("nl")
nl
iex> LocaleTest.cardinal("en")
en

I don’t believe Code.eval_quoted is necessary here.

1 Like

Qqwy, I really appreciate your patience and help. What I’m after is sort of the inverse of what you’ve described.

I already have the AST crafted (as output from a yecc parser). The AST is the representation of a function body. I want to inject the AST into the Elixir compilation flow. Therefore i needs to be part of a function.

In essence what I’m after would look like this:

defmodule AstFun do
  defmacro AstAsFunctionBody(body_which_is_an_ast) do
    quote do
      def some_function, do: Macro.insert_ast(body_which_is_an_ast)
    end
  end
end

Basically I want to insert an already prepared AST into the compilation flow. Of course there is no such function as Macro.insert_ast/1, but thats the basic idea. Did I explain it better?

1 Like

If you’re wanting to just make a macro that will insert some AST into a function, then I believe the approach you were suggesting earlier is the way to go.

defmodule AstFun do
  defmacro make_function(ast) do
    quote do
      def some_function, do: unquote(:erlang.element(1, Code.eval_quoted(ast)))
    end
  end
end

#example
AstFun.make_function quote do
    4 + 6
end

AstFun.make_function { :+, [context: Elixir, import: Kernel], [4, 6] }

As for the performance of having to use Code.eval_quoted/1, I don’t think it’s too much of a concern as I assume you’re doing this at compile time? You can take a look at it anyway to determine if it’s too complex for your situation https://github.com/elixir-lang/elixir/blob/v1.3.2/lib/elixir/src/elixir.erl#L189

If it is too complex I’d suggest instead just manipulating the AST yourself (outside of a macro). And then you could pass that along to Module.create/3 or use the @before_compile callback and return the completed AST there.

2 Likes

I already have the AST crafted (as output from a yecc parser). The AST is the representation of a function body.

defmacro astAsFunctionBody(body_which_is_an_ast) do

The body in this case is always an AST. So there is something definitely missing here. In the previous example, with defmacro test, the body was also an AST. So maybe you don’t have an AST or you have an AST and you want to transform it into something else.

1 Like