Macros: when and why is #if !macro / #if macro required?

Hello,
I am new to haxe and learning macros.

  1. I want to ask why if #if !macro / #if macro required? Because, on a quick glance of other languages with similar macro systems, none of them seem to be requiring any such thing. They automatically run the correct code in the macro context and non-macro context. Why can’t haxe implement such a feature?

  2. Since, haxe does not automatically determine what code should be in macro context and what should not be, so my question is where all should I be using #if macro / #if !macro? i.e. under which conditions does the code is required to be put under #if.

I ask these questions because the error messages are incomprehensible for a beginner.
And I tried other languages, they did not require #if !macro equivalent and they generally have better error messages.
So, please answer the questions -

why does haxe require #if !macro when other languages don’t
can the error messages for haxe macro code be improved
and in what conditions is #if !macro/ #if macro required. (for e.g. i have seen plenty of code using it with import haxe.macro.Foo, and I have seen plenty of code which does not do it yet it still works, so when is it required, when is it not required.)

2 Likes

why does haxe require #if !macro when other languages don’t

Because we are special :wink:No its because macro’s run in different context and might or might not have same available fields/functions (see Macro Context - Haxe - The Cross-platform Toolkit).

and in what conditions is #if !macro / #if macro required. (for e.g. i have seen plenty of code using it with import haxe.macro.Foo , and I have seen plenty of code which does not do it yet it still works, so when is it required, when is it not required.)

I think general rule is; is it a macro? Isolate macros in #if !macro

There is a alternative which is maybe nicer. Lets say you have a Util.hx file, then you can also create a Util.macro.hx file where you put the macros in, then you dont have to use #if !macro. Its still the same Util class but its separated by context. See Target-Specific Files - Haxe - The Cross-platform Toolkit

It has to do with the way Haxe compiler processes the code. It always loads and “types” types the whole module, which means that if you have mixed macro/non-macro code in the same module it can get messy, since the compiler will try to load the module and all its dependencies in the macro context (also true for the other way - using macro APIs in non-macro code can bring unnecessary dependencies into the compiled code).

Ideally, try to always separate macro and non-macro code by placing it in different modules, it’s much cleaner than the #if approach.

I’m also curious about the “other languages” you keep mentioning, please elaborate.

AFAICT, Nim and Rust have macros comparable to haxe. (AST macros, quasi-quoting,can be used as decorator/metadata)

In nim, I personally have never wrote any macros, but -

they do not seem to require any conditional compilation to separate the context. The compiler determines what code to execute in which context automatically.

Same for rust AFAIK. proc macros (ast macros) can (i think) only be used as decorators (or whatever is the right terminology), so the compiler knows that it is to be performed at compile time context. The normal macros (not ast macros? idk), require an exclamation mark see code here Formatted print - Rust By Example , the macro is called by print!(…) so the compiler again knows to run it at compile time.
Again, my knowledge of these languages is not much (I come from mainstream languages C/Java/python), so I may be wrong about how the compiler detects which code to execute at compile time, but that is what those compilers do, I looked at many examples for both these languages, and I have never seen haxe like #if macro used anywhere.

@nadako
So, did you check if/why/how those other languages do it and if it is possible to do that in haxe?

For starters: you can entirely avoid using #if macro fencing, if you make sure that all compile time code lives in separate modules (which is preferable).

The question is what to do when runtime/compile time code gets mixed. Consider the following code:

import haxe.macro.*;

class Log {
  #if macro
    static function output(t:Type) // this creates reflection-free code required to output a value of type t in a variable called "x" into a StringBuf called "out"
      return switch Context.follow(t) {
        case TInst(_.get() => { pack: [], name: 'Array' }, [entry]):
          var outputEntry = output(entry);
          macro {
            out.addChar('['.code);
            for (x in x) $outputEntry;
            out.addChar(']'.code)
          }
        case ... // implement all kinds of other cases
      }
  #else
    static public function printString(s:String)
      js.Browser.console.log(s);
  #end

  static public macro function print(e:Expr) {
    var body = output(Context.typeof(e));
    return macro @:pos(e.pos) Log.printString({
      var x = $e,
        out = new StringBuf();
      $body;
      out.toString();
    });
  }
}

The example is a bit contrived, but it should get the point across. When Log.print runs at compile time it calls Log.output, and that’s just fine. It cannot call Log.printString though, because js.Browser.console is not available at compile time. Likewise, Log.output cannot be called at runtime, because it relies on compiler APIs that are not available.

In principle, it would be possible to let that code compile without conditional compilation, because Log.print does not call Log.printString and Log.output is not used from any code that runs at runtime. That would come down to completely ignoring unused fields is during typing. Haxe instead ignores unused modules, because this proved more practical (IIRC in the very early days of dead code elimination it did actually skip type errors in unused fields, so when you started calling methods in modules that previously compiled just fine, you suddenly got errors … it was pretty confusing to say the least).

As mentioned in the beginning, you want to separate macro and non-macro code anyway. The example I gave could be split into runtime/compiletime modules like so:

// - - - Log.hx

class Log {
  static public function printString(s:String)
    js.Browser.console.log(s);

  static public macro function print(e)
}

// - - - Log.macro.hx

import haxe.macro.*;

class Log {
  static function output(t:Type) // this creates reflection-free code required to output a value of type t in a variable called "x" into a StringBuf called "out"
    return switch Context.follow(t) {
      case TInst(_.get() => { pack: [], name: 'Array' }, [entry]):
        var outputEntry = output(entry);
        macro {
          out.addChar('['.code);
          for (x in x) $outputEntry;
          out.addChar(']'.code)
        }
      case ... // implement all kinds of other cases
    }

  static public macro function print(e:Expr) {
    var body = output(Context.typeof(e));
    return macro @:pos(e.pos) Log.printString({
      var x = $e,
          out = new StringBuf();
      $body;
      out.toString();
    });
  }
}

No #if \o/

All that aside, Nim and Rust macros don’t quite compare with Haxe’s, where it’s not uncommon to use 3rd party libs, networking, file system, databases, the operating system (invoking other CLI tools) and what not. It’s also common to call into the compiler to perform typing and what not (which as far as Google tells me is not possible in Nim or Rust). Properly separating code is therefore vital in the long run.

3 Likes

Not really, I didn’t have time for this, but I can assume that they either completely separate macro and non-macro methods (forbidding usage of non-macro functions from macro functions), or skip typing of the things that are unused in the macro code. Anyway, @back2dos explained the Haxe situation pretty well and I personally use this approach and very rarely need #if fencing for macros.

in Nim you decorate a proc with {.compileTime.} which is more or less the same as #if macro …