Macros

Macros in Onyx are very much like procedures, with a couple notable differences. When a macro is called, it is expanded at the call site, as though its body was copy and pasted there. This means that macros can access variables in the scope of their caller.

print_x :: macro () {
    // 'x' is not defined in this scope, but it can be used
    // from the enclosing scope.
    println(x);
}

{
    x := 1234;
    print_x();
}

{
    x := "Hello from a macro!";
    print_x();
}

Because macros are inlined at the call site and break traditional scoping rules, they cannot be used as a runtime known value.

There are two kinds of macros: block macros, and expression macros. The distinguishing factor between them is the return type. If a macro returns void, it is a block macro. If it returns anything else, it is an expression macro.

Block and expression macros behave different with respect to some of the language features. Expression macros behave exactly like an inlined procedure call with dynamic scoping.

add_x_and_y :: macro (x: $T) -> T {
    defer println("Deferred print statement.");
    return x + y;
}

{
    y := 20.0f;
    z := add_x_and_y(30.0f);
    printf("Z: {}\n", z);
}

// This prints:
// Deferred print statement.
// Z: 50.0000

This example shows that defer statements are cleared before the expression macro returns. Also, the return statement is used to return from the macro with a value.

Block macros behave a little differently. defer statements are not cleared, and return statements are used to return from the caller's procedure.

early_return :: macro () {
    return 10;
}

defer_a_statement :: macro () {
    defer println("Deferred a statement.");
}

foo :: () -> i32 {
    defer_a_statement();
    println("About to return.");
    early_return();
    println("Never printed.");
}

// foo() will print:
// About to return.
// Deferred a statement.

In foo, the call to defer_a_statement adds the deferred statement to foo. Then the first println is run. Then the early_return macro returns the value 10 from foo. Finally, the deferred print statement is run.

This distinction between block and expression macros allows for an automatic destruction pattern.

// These are not the actual procedures to use mutexes.
grab_mutex :: macro (mutex: Mutex) {
    mutex_lock(mutex);
    defer mutex_unlock(mutex);
}

critical_procedure :: () {
    grab_mutex(a_mutex);
}

grab_mutex will automatically release the mutex at the end of critical_procedure. This pattern of creating a resource, and then freeing it automatically using defer is very common.

Code Blocks

To make macros even more powerful, Onyx provides compile-time code blocks. Code blocks capture code and treat it as a compile-time object that can be passed around. Use [] {} to create a code block. Use #unquote to "paste" a code block.

say_hello :: [] {
    println("Hello!");
}

#unquote say_hello;

Code blocks are not type checked until they are unquoted, so they can contain references to references to variables not declared within them.

Code blocks have their syntax because they can optionally take parameters between their []. When unquoting a code block with parameters, you must pass an equal or greater number of arguments in parentheses after the variable name.

do_something :: ($do_: Code) {
    #unquote do_(1, 2);
    #unquote do_(2, 6);
}

do_something([a, b] {
    println(a + b);
});

Code blocks can be passed to procedures as compile-time values of type Code.

triple :: ($body: Code) {
    #unquote body;
    #unquote body;
    #unquote body;
}

triple([] {
    println("Hello!");
});

Code blocks can be passed to macros without being polymorphic variables, because all parameters to macros are compile-time known.

triple_macro :: macro (body: Code) {
    #unquote body;
    #unquote body;
    #unquote body;
}

triple_macro([] {
    println("Hello!");
});

A single statement/expression in a code block can be expressed as: [](expr)

[](println("Hello"))
// Is almost the same the as
[] { println("Hello"); }

The practical difference between []() and [] {} is that the latter produces a block of code, that has a void return type, while the former results in the type of the expression between it. The Array and Slice structures use this feature for creating a "lambda/capture-like" syntax for their procedures.

find_largest :: (x: [] $T) -> T {
    return Slice.fold(x, 0, [x, acc](x if x > acc else acc));
}

A code block can also be passed to a macro or procedure simply by placing a block immediately after a function call. This only works if the function call is a statement.

skip :: (arr: [] $T, $body: Code) {
    for n in 0 .. arr.count {
        if n % 2 == 1 do continue;
        it := arr[n];
        #unquote body;
    }
}

// This prints: 2, 5, 11
skip(.[2, 3, 5, 7, 11, 13]) {
    println(it);
}