Macros: @before_compile

According to Metaprogramming Elixir p.39, it says:

Elixir allows us to set a special module attribute, @before_compile, to notify the compiler that an extra step is required just before compilation is finished.

Then why isn’t it named @after_compile?

According to the Elixir Getting Started Guide:

Finally, callbacks such as @before_compile allow us to inject code into the module when its definition is complete.

Is the end of “module definition” the same thing as the “end of compilation”?

Finally, the Module docs think @before_compile works the the opposite of what Metaprogramming Elixir says:

A hook that will be invoked before the module is compiled.

…which makes more sense to me given @before_compile’s name.

However, the Module docs also say:

@before_compile


Note : unlike @after_compile , the callback function/macro must be placed in a separate module (because when the callback is invoked, the current module does not yet exist).

Yet, the example in Metaprogramming Elixir puts the callback macro inside the same module that specifies @before_compile.

My survey of the literature reveals that @before_compile is not well understood.

My question is why the example on p.39 cannot be modified like this:

  defmacro __using__(_options) do
    quote do
      import unquote(__MODULE__)
      Module.register_attribute(__MODULE__, :tests, accumulate: true)
      @before_compile unquote(__MODULE__) 
    end
  end

defmacro __before_compile__(_env) do
    IO.inspect @tests   #*******  WARNING *********
    quote do
      def run do
          IO.puts "Running the tests: (#{inspect @tests})"
      end
    end
end

I get a warning:

warning: undefined module attribute @tests, please remove access to @tests or explicitly set it before access
my.ex:15: Assertion (module)

If @tests is replaced at compile time inside the quote block, why isn’t @tests replaced at compile time outside the quote block?

2 Likes

Because it happens before compilation of the module. Compilation of the whole project has started. You wouldn’t be able to call macros if some code hadn’t already compiled. Compilation of the module has not. The books wording does seem awkward, but it conveys the important part, that it is executed before compilation.

No. @before_compile happens before compilation of a module, but after definition. The point of @before_compile is allow you to generate code by introspecting everything already defined in the module.

If that’s true, it’s wrong. I’d be surprised if it was that way though. It’s likely in the same module that defines the __using__/1 macro, but not the module that use is called in.

It’s because the __before_compile__ macro body is in the context of the module it’s defined in. In the __using__ macro, you set the tests module attribute on the module where use is called. You’d need to put IO.inspect @tests inside the quote block.

1 Like

Nope!

There is an @after_compile, and it happens after compilation, unlike @before_compile which happens before.

@before_compile lets you effectively inject code at the bottom of the module. The module is still not actually compiled into BEAM byte code at this point in time. Modules are not compiled one line at a time or anything like that. The contents of a macro execute and build a data structure / shove stuff into :ets, and then after all the full module body including @before_compile hooks are run, the module is compiled, and then there is an @after_compile which can run. Notably, @after_compile can’t change anything about the module, since it is compiled now.

Not exactly. The code on Page 39 has a module called Assertions that contains a __using__ macro and a __before_compile__ module. It contains the line @before_compile unquote(__MODULE__) but that @before_compile Assertions is not executed in the Assertions module, it’s executed in whatever module calls use Assertions. Thus, the book is correct, the module that actually invokes @before_compile Assertions is not itself the Assertions module containing the callback.

2 Likes

Elixir compilation has two phases. The first phase is macro expansion, the second being actually compiling the fully expanded AST. You can think of the expansion phase as a pre-compilation step, which makes the @before_compile name makes sense. But since the both of them happen together every time I think it’s just been used in both ways historically. Elixir fires the before compile hook after expansion but before compilation.

After compile is rarely used in my experience.

1 Like

Ah, okay. And that answers my question at the end of my post. The line I added executes in the Assertions module while the @tests module attribute is set in the module that calls use Assertions.

Elixir compilation has two phases. The first phase is macro expansion, the second being actually compiling the fully expanded AST. You can think of the expansion phase as a pre-compilation step, which makes the @before_compile name makes sense.

That is the mental model I came up. I assumed Metaprogramming Elixir made a mistake(even though there was no errata for that passage), and the passage should read:

Elixir allows us to set a special module attribute, @before_compile, to notify the compiler that an extra step is required just before macro expansion is finished. The @before_compile attribute accepts a module argument where a __before_compile__/1 macro must be defined. This macro is invoked just before compilation in order to perform a final bit of code generation.

2 Likes

This seems like an odd assumption. The language of “just before compilation is finished” is imprecise, but can be understood when thinking about compilation as the overall process of macro expansion and byte code generation. The paragraph gets more precise as it continues: “This macro is invoked just before compilation in order to perform a final bit of code generation”.

2 Likes

This seems like an odd assumption.

My assumption is based on on my belief that the following two statements conflict because they describe different points in time:

Elixir allows us to set a special module attribute, @before_compile, to notify the compiler that an extra step is required just before compilations is finished.

and:

This macro is invoked just before compilation in order to perform a final bit of code generation.

You can substitute any noun you want instead of “compilation”. For instance:

Meet me outside the stadium just before the soccer match is finished.

v.

Meet me outside the stadium before the soccer match.

====

when thinking about compilation as the overall process of macro expansion and byte code generation.

I may have missed it, but I went back and re-read the beginning of the book, and I could not find where that was explained. In any case, the fact that compilation is a two step process does not in any way make the conflict in those two statements disappear for me. But, then again I’m dense.

1 Like

The conflict arises from the fact that in those two statements “compilation” means different things. In the case of:

Elixir allows us to set a special module attribute, @before_compile, to notify the compiler that an extra step is required just before compilations is finished.

“Compilation” means the whole process when you type in mix compile in the console or use elixirc. It’s a complex process that includes many steps - lexing, parsing, evaluating the modules and finally producing the bytecode.

In the case of:

This macro is invoked just before compilation in order to perform a final bit of code generation.

“Compilation” means just one part of that whole process - that last part of actual conversion to the final bytecode.

Is it confusing? Yes, it is. Unfortunately such is the language when the same term can have many meanings depending on the context.

3 Likes