Is it possible to detect if code is executing at compile time?

Can Elixir code detect whether it is executing at compile time? I would like to write something like the following macro:

defmacro foo() do
  quote do
    if is_compile_time? do
      one_thing()
    else
      another_thing()
    end
  end
end

It’s presumably obvious how the code that the macro expands to can execute at runtime. It can execute at compile time in a scenario such as the following:

defmodule Bar do
  def bar() do
    foo() # invoke the macro
  end
end

defmodule Amp do
  @attr Bar.bar() # code that 'foo' macro expands to is executed at compile time
end

I have tried examining __ENV__, but unless I’m missing something, it offers no reliable diagnostic for compile time vs. runtime. I wonder if there is any other way. For anyone familiar with Common Lisp, I suppose I am looking for something that works a bit like eval-when.

By the way, I am asking largely out of curiosity. I’m aware that in general one should not need to perform such a check :slight_smile:

The problem is that it is pretty hard to differentiate between “compile time” and “run time” as “compile time” is “run time with saving created modules to .beam files”.

You can check __CALLER__.function to detect whether macro was called within function body or outside of it.

4 Likes

compiling? = is_list(Process.get(:elixir_module_binaries))
But as @hauleth pointed out, difference between runtime and compile time is ephemeral. Compilation can happen in runtime, and anything can be called during compilation

3 Likes

Yes, sorry, I did not make this clear in my question. I’m interested to know if the code is being called during compilation, not necessarily at some mythical ‘compile time’.

Given that, I think your suggestion should work! Thank you.

That distinction doesn’t change anything he said though, you can still generate a module at runtime and compile it and for it what is happening will be “compile time”. You really should revise what you really want to achieve with this and consider other ways, this can very easily bite you in the rear in the most inopportune moment in the future.

5 Likes

Yes, I’m aware of that and clarified what I meant in the comment that you’re replying to: I’m interested to know if the code is being called during compilation. I assume hissssst’s expression would evaluate to true in the scenario you describe, which is exactly what I want.

To be precise, expression evaluates to true, only if the process is compiling the Elixir module. Some processes can be present during the compilation, but this expression would return false in them, since these processes are not compiling anything.

1 Like

I see, yes, thanks for the clarification, hissssst.

We’re talking past each other. What I described is also “during compilation”. :smiley:

What I described is also “during compilation”

Yes, I know. And I want the expression to evaluate to true in that context, so I don’t see the problem.

I use the following code in ex_cldr to detect if Elixir is compiling. The mechanism changed from Elixir 1.11, hence the conditional check:

  def compiling? do
    # TODO: When we depend on Elixir v1.11+ only, remove function_exported and elixir_compiler_pid
    process_alive?(:can_await_module_compilation?) ||
      process_alive?(:elixir_compiler_pid)
  end

  defp process_alive?(:can_await_module_compilation?) do
    Code.ensure_loaded?(Code) &&
      function_exported?(Code, :can_await_module_compilation?, 0) &&
      apply(Code, :can_await_module_compilation?, [])
  end

  defp process_alive?(name) do
    case Process.get(name) do
      nil -> false
      pid when is_pid(pid) -> true
    end
  end
14 Likes

Very helpful – thank you!

Please do not do this:

This is relying on internal compiler behaviour. Use the suggestions from @kip instead, which uses public APIs. :slight_smile:

8 Likes

Just out of curiosity: would it be correct to say that in this case BEAM never creates a corresponding .beam file?

BEAM does not create files on its own, Elixir and Erlang compiler do if run under some special conditions (e. g. from the command line.)

If you are compiling files manually, you are responsible for persisting them, if you wish. Here I do it, because Common Test needs tests to be available as beams.

Basically, it’s

{module, code} = Code.compile_file(file)
beam = Path.join(Path.dirname(file), to_string(module) <> ".beam")
:ok = File.write!(Path.join(Path.dirname(file), to_string(module) <> ".beam"), code)
4 Likes

I have to admit I don’t remember if the files are created.

But yes, the usual order of executing code, including during compile-time, still applies, regardless if the .beam file is persisted or not.