How to deal with code blocks in macros?

I found myself struggling with the idea of __block__. Let’s say I have a macro

defmodule Foo do
  defmacro foo([do: block]) do
    IO.inspect(block)
    nil
  end
end

When I call this macro like

Foo.foo do
  1
  2
end

it prints

{:__block__, [], [1, 2]}

But when I call the same macro this way

Foo.foo do
  1
end

it prints

1

Is there a graceful way to handle both situations (single statement versus zero or more than one statements) without writing case-when or multiple macro clauses?

This happens because you get the AST (abstract syntax tree) in the block variable in your macro. Macro expansion happens at compile time so you don’t know the runtime value of block yet, only the AST. It just so happens that the AST of 1 is 1. But the AST of 1; 2 (which you were doing) is {:__block__, [], [1, 2]}.

You can see what AST some code produces by doing quote do <code-here> end in IEx.

What is it that you want your macro to actually do?

I want to decompose a user-defined block and build another AST according to the pieces in the original block (just for fun).

For now, I defined a function

defp normalize_block({:__block__, _, terms}), do: terms
defp normalize_block(term), do: [term]

But I feel that defining such functions whenever I want to manipulate block AST’s is a mild pain. Is there a built-in function or something that eases the pain?

I’m not aware of such functions but I’m not an expert on AST modifications so maybe there is something. If there is, the docs will have it.

Perhaps Macro.prewalk/2 might help?

1 Like