Skip to main content

Macros

A macro binds a name to one or more pattern/template pairs. Invoking it splices the matching template into your program before name resolution runs, so a macro is a way to write code that writes code, not a runtime construct. Nothing about a macro survives into the compiled module.

Macros pair with compile-time parameters, which expand structurally (comptime for over a known range). A macro instead expands at a call site, with arguments.

macro square {
    ($x:expr) => { $x * $x };
}

func check() -> i32 {
    // A typed binding is needed because this language cannot infer an operand type
    // for a comparison between two literal-only expressions.
    let grouped: i32 = square!(2 + 3);
    if (grouped != 25) { return 1; }
    return 0;
}

export func check as "check";

check() returns 0, and the 25 there is the interesting part. See grouping below.

Defining and invoking

macro name {
(pattern) => { template };
(pattern) => { template };
}

Arms are tried top to bottom and the first one that matches wins. An invocation is name!(...), valid in two places:

  • At module scope, as a declaration: name!(args); — the ; is required, like every other declaration.
  • In an expression: let x: i32 = name!(args);

A macro must be snake_case, like a function. It is also the one declaration kind that is order-dependent: a macro must be defined before the code that uses it, because expansion is a single forward pass.

// Rejected: "macro 'later' is not defined here -- a macro must be
// defined before it is used".
func check() -> i32 { return later!(1); }

macro later {
    ($x:expr) => { $x };
}

Captures

A pattern is a mix of captures and literal tokens that must match exactly. A capture is $name:kind, where kind is one of five fragment specifiers:

KindMatches
exprA whole expression, up to the pattern's next literal token
identExactly one identifier
literalOne number, string, true, or false, with an optional leading -
tyA type: optional &/?, then a name
ttOne token, or one whole (...)/[...]/{...} group

Literal tokens in the pattern are what separate the captures:

macro add_pair {
    ($a:expr, $b:expr) => { $a + $b };
}

func check() -> i32 {
    let sum: i32 = add_pair!(2, 3);
    if (sum != 5) { return 1; }

    // `expr` respects nesting, so this inner comma belongs to the call, not to the
    // pattern -- `$a` captures `min(9, 4)` whole.
    let nested: i32 = add_pair!(min(9, 4), 1);
    if (nested != 5) { return 2; }

    return 0;
}

func min(a: i32, b: i32) -> i32 {
    if (a < b) { return a; }
    return b;
}

export func check as "check";

check() returns 0.

Two captures need something between them

($a:expr $b:expr) is rejected when the macro is defined, not when it is called:

// Rejected: "'$a:expr' is immediately followed by '$b:expr' with no separator
// token between them; add one (e.g. ',')".
macro unsplittable {
    ($a:expr $b:expr) => { $a + $b };
}

An expr capture scans forward until it hits the next token the pattern expects. With another capture immediately after it, there is nothing to stop at, so the split would be arbitrary. Rejecting it up front beats guessing.

Arity is not checked by the matcher

This surprises people, and it matches Rust. A trailing expr capture with nothing after it consumes everything left, including top-level commas:

macro one_arg {
    ($x:expr) => { $x };
}

// `$x` captures `3 , 4` -- both of them. This does NOT fail to match.
one_arg!(3, 4)

Because a multi-token expr is parenthesized when spliced (below), that capture becomes the tuple (3, 4). So if you want two arguments, write the separator into the pattern (($x:expr, $y:expr)); the matcher will not reject a mis-arity call for you.

Grouping is automatic

Substitution works on tokens, which is exactly how C macros became infamous. Given:

macro square {
    ($x:expr) => { $x * $x };
}

a naive token splice of square!(2 + 3) would produce 2 + 3 * 2 + 3, which reparses with * binding tighter and evaluates to 11. Reed parenthesizes a multi-token expr fragment when it splices, so you get (2 + 3) * (2 + 3) and 25.

Only expr is parenthesized. A ty fragment is not an expression ((&Counter) is not a valid type), a tt may be a {...} block, and ident/literal are single tokens already.

Generating declarations

A template at module scope can produce whole declarations:

macro make_constant {
    ($name:ident, $value:literal) => {
        func $name() -> i32 { return $value; }
    };
}

make_constant!(answer, 42);
make_constant!(zero, 0);

func check() -> i32 {
    if (answer() != 42) { return 1; }
    if (zero() != 0) { return 2; }
    return 0;
}

export func check as "check";

check() returns 0.

Composing with comptime for

A template may contain comptime for, and this is the payoff: the macro takes the name, the loop generates the repetition.

param N: i32 = 3;

macro declare_counters {
    ($base:ident) => {
        comptime for i in 0..N {
            global [<$base $i>]: mut i32 = 0;
        }
    };
}

declare_counters!(Counter);

func check() -> i32 {
    Counter0 = 1;
    Counter1 = 2;
    Counter2 = 4;
    if (Counter0 + Counter1 + Counter2 != 7) { return 1; }
    return 0;
}

export func check as "check";

check() returns 0. Note [<$base $i>] pasting a macro capture and a loop variable into one name. A template can also read a param it never captured, since $name resolves against the macro's own captures first and the compile-time environment second.

A macro may also expand into another macro invocation, which is expanded on a later pass.

Repetition

A pattern element can repeat. $( ... )sep* matches zero or more occurrences, $( ... )sep+ one or more, and the separator (, or ;) goes between consecutive occurrences:

macro make_adders {
    ($($name:ident = $val:literal),*) => {
        $(
            func $name(x: i32) -> i32 { return x + $val; }
        )*
    };
}

make_adders!(add_one = 1, add_two = 2, add_three = 3);

func check() -> i32 {
    if (add_one(10) != 11) { return 1; }
    if (add_two(10) != 12) { return 2; }
    if (add_three(10) != 13) { return 3; }
    return 0;
}

export func check as "check";

check() returns 0. One arm handled three pairs, and the template's own $( ... )* group is what emitted one function per pair.

The separator is optional. Without one, occurrences simply run together:

macro declare_slots {
    ($($n:ident)*) => {
        $( global $n: mut i32 = 0; )*
    };
}

declare_slots!(Slot0 Slot1 Slot2);

func check() -> i32 {
    Slot0 = 1;
    Slot1 = 2;
    Slot2 = 4;
    if (Slot0 + Slot1 + Slot2 != 7) { return 1; }
    return 0;
}

export func check as "check";

check() returns 0.

Rules worth knowing

  • Zero occurrences is fine for *, and generates nothing. $(...)+ requires at least one and reports a '$(...)+' repetition needs at least 1 occurrence(s) otherwise.
  • The body must contain a capture. $(,)* is rejected when the macro is defined: with no metavariable inside, there is nothing to drive how many times the template's group should run.
  • A template group must mention a repeated name, for the same reason.
  • Two repeated names of different lengths cannot be zipped. The diagnostic names both and their counts, since either could be the one you got wrong.
  • A repeated capture is only usable inside a $(...) group. Outside one — or in a [<...>] paste or str!(...) — it has no single value, and you get a message saying so rather than a confusing one about a missing name.
  • A body that could match zero tokens is rejected. This sounds pedantic and is not: a nested repetition matching nothing, with no separator on the outer one, would loop forever. The compiler reports a '$(...)' repetition body consumed no tokens instead.

What macros deliberately do not do

  • No hygiene. A name a template introduces is an ordinary name, so it can collide with one at the call site. Prefix generated names, or take the name as an ident capture.
  • One repetition depth per template group. $(...)* works and nests, but a single template group iterates one level: a group mentioning two repeated names of different lengths is an error naming both, rather than zipping or truncating.
  • No statement-position invocation. Declaration and expression position only, so a template producing bare statements has nowhere to go.
  • No expansion preview. There is no flag that prints what a macro expanded to. What you can rely on is position: a diagnostic caused by template text is reported at the invocation, and one caused by an argument you passed is reported at that argument.

Both limits, plus the reasoning, are in limitations.

Safeguards

Expansion is bounded, because a macro can invoke itself:

  • Depth: more than 64 nested expansions is an error.
  • Total: more than 10,000 expansions in one module is an error.
// Rejected: "macro expansion nested past the maximum depth of 64".
macro forever {
    ($x:expr) => { forever!($x) };
}

func check() -> i32 { return forever!(1); }

Both numbers are this compiler's choice, not a language requirement, exactly like comptime for's own budgets.

In an editor

A template is code you edit, so the language server treats it as code. An arm's captures are offered by completion at their declaration and at every $name use in that arm's template, and whatever the template itself declares — a function's parameters and locals, the fields of a struct one of them points at — resolves the same way it would outside a macro. Module-scope names stay reachable too, since a template still sits in the module.

Each arm is its own scope. A sibling arm's captures are never offered here, because arms are alternatives and a template naming a capture its own pattern does not bind will not expand.

Hover, go-to-definition, document highlight, and rename follow the same reasoning and work in the same places: hovering $name says which fragment kind it captures, and renaming a capture, a template local, or a template parameter rewrites the pattern and every splice together, so the macro still expands to the same program afterwards.

A macro whose closing } you have not typed yet keeps all of this, rather than going silent until the brace is closed.