Macros

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);
1 Like