Hi fellow Haxers,
I'm working with new experimental web technology involving multithreading with shared memory. This means forgoing lots of the higher level language features such as objects in favor of simple primitives. Fortunately Haxe is awesome for this, since I can use Abstracts and Macros to create syntax sugar on top of these primitives.
However, there is one issue I keep running into when using project-wide macros in Haxe: The execution order of macros. I often need one macro to not execute before all other instances of another macro have safely executed. Enforcing this is tricky!
Imagine the following scenario: All across our project we have various macro calls defining letters of the alphabet. Once all of these have run, we want to run a macro that generates an alphabet lookup table using all the letters that have been defined.
// We have these shattered across various files
Alphabet.defineLetter('A'); // macro
Alphabet.defineLetter('B');
// macro
Alphabet.defineLetter('C'); // macro
// This macro should run only after all defineLetter calls have run
// It generates a runtime lookup table filled with all the letters that have been defined
Alphabet.generateAlphabet(); // macro
// And lastly we use the alphabet lookup table to do neat stuff
AlphabetTools.fetchRandomLetter(); // non-macro runtime function
Now, I actually managed to achieve the proper macro execution order with a neat trick, I simply imported every file that calls defineLetter() at the top of the Alphabet class file. This way,
every defineLetter() macro call is run before the generateAlphabet() macro is called. Problem solved!
...or so I thought. As I continued coding, I came across a situation where I needed to use the generated alphabet in one of the files that calls the defineLetter() macro. Something like this:
// file1.hx
Alphabet.defineLetter('B');
var foobar = AlphabetTools.fetchRandomLetter();
// file2.hx
Alphabet.defineLetter('C');
So what happens is, fetchRandomLetter() will obviously need to refer to the lookup table of all the letters. So, I have Alphabet.generateAlphabet() invoked inside of it, and thus it ruins my entire neat little arranged execution order. Now we miss out on all the defineLetter() calls that happens after 'B'. What appears to happen is that in file1.hx, when the compiler comes across the line containing AlphabetTools.fetchRandomLetter(), the compiler looks into the function and resolves the macro calls waiting inside it. This causes the macro Alphabet.generateAlphabet() to be called prematurely.
Again, I actually managed to find a workaround to this, but my workaround is horrible and ugly, and I'm hoping you guys might know of a better way.
What I do is this:
class AlphabetTools {
static dynamic public function fetchRandomLetter() {} //Empty function that does nothing
static public function unusedFetchRandomLetter() {
var lookuptable = Alphabet.generateAlphabet
(); // macro call that returns a static lookup table
return
lookuptable[
Std.random(
lookuptable.length)]
;
}
static var temp:Int = tempRun();
static public function tempRun() {
AlphabetTools.fetchRandomLetter = AlphabetTools.unusedFetchRandomLetter;
return 0;
}
}
What happens here is that I leave a empty dummy function fetchRandomLetter() for the compiler to find, which does nothing. But I mark it as "dynamic" so that it can be changed during runtime. During the startup of runtime, the tempRun() function swaps in the unusedFetchRandomLetter() function on top of the fetchRandomLetter, so that all the calls to fetchRandomLetter() ends up running unusedFetchRandomLetter() instead. But, in the eyes of the compiler, no file actually invokes unusedFetchRandomLetter() directly, and thus there is no need to resolve it's inner macro prematurely. The macro still gets resolved in the end but it doesn't happen the instant some other file refers to fetchRandomLetter();
So now the macro execution order is correct, but this solution is very ugly, and uses features in way they aren't really meant to be used. Is there a "clean" way to do it instead?
(And note that I'm not actually building the alphabet with a macro, it's just a metaphor for something else. The real data involves lots of non-primitives such as objects and function callbacks, and thus cannot be stored as class @metadata)