The Jai Programming Language implements hygenic macros. Hygenic macros do not cause any accidental captures of identifiers. Hygenic macros modify variables only when explicitly allowed. Unlike the C programming language in which a macro is completely arbitrary, hygenic macros are more controlled, better supported by the compiler, and come with much better typechecking.
Macros can be created by adding the #expand
keyword to the end of the function declaration before the curly brackets.
macro :: () #expand {
// This is a macro
}
Macros are similar to inline functions in that the compiler inlines the code with the macro functionality. Anything with a backtick is something the macro refers to in the outer scope. In this language, macros work like hygenic macros like Lisp: local variables are available locally, and if the macro refers to something unknown in the outer scope, mark the variable with a backtick “`”.
a := 0;
macro(); //call the macro
macro :: () #expand {
`a += 10; // add 10 to the "a" variable found in the outer scope.
}
Macros can take in Code
as an argument and #insert
directives can be used inside the macros to insert the code into the body of the macro. This can be useful if you want a macro that’s close to the way C macros work.
macro :: (c: Code) #expand {
#insert c;
#insert c;
#insert c; // In this macro, we insert the code "c" into the macro three times
}
You can also use Code to strip out things from your program in release builds, expr
will not get evaluated when ENABLE_ASSERTIONS
is false, which is better than directly passing a bool when expr
takes a long time to evaluate.
assert :: (expr : Code) #expand
{
#if ENABLE_ASSERTIONS
{
value : bool = #insert expr;
always_assert (value);
}
}
Just like regular functions, you can return values from macros.
max :: (a: int, b: int) -> int #expand {
if a > b then return a;
return b;
}
Variables are not the only piece of code that can be backticked “`”. You can also backtick return values and defer statements. You cannot backtick continue
, break
, or remove
statements.
macro :: () -> int #expand {
if `a < `b {
`defer print("Defer inside macro\n");
`return "Backtick return macro";
}
return 1;
}
Passing Registers through Macro Arguments
Registers can be passed through macro arguments, giving you the power of macros while using inline assembly.
add_regs :: (c: __reg, d: __reg) #expand {
#asm {
add c, d;
}
}
main :: () {
#asm {
mov a:, 10;
mov b:, 7;
}
add_regs(b, a);
}
Nested Macros
Macros can be nested. You call a macro within another macro. There is a macro limit, meaning there is a limit to how many macro calls you can generate. If you call a macro recursively (e.g. creating a fibonacci macro to call fibonacci recursively), this results in a compiler error that you hit a macro limit. The macro limit is by default 1000.
macro :: () #expand {
print("This is a macro\n");
nested_macro();
nested_macro :: () #expand {
print("This is a nested macro\n");
}
}
The following recursive fibonacci macro calls results in a compiler error, saying you hit the macro limit.
fibonacci :: () #expand {
fibonacci();
}
fibonacci();
Here’s the error generated:
Error: Too many nested macro expansions. (The limit is 1000.)
If you want to make a recursive macro, compute the if
at compile-time with a compile-time #if
.
/*
// This version of the macro fails to compile since the 'if' is a runtime 'if'
factorial :: (n: int) -> int #expand {
if n <= 1 return 1;
else {
return n * factorial(n-1);
}
}
*/
// This code works and compiles.
factorial :: (n: int) -> int #expand {
#if n <= 1 return 1;
else {
return n * factorial(n-1);
}
}
x := factorial(5);
print("factorial of 5 = %\n", x);