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.