API Stability Notice

Macroforge is under active development. The API is not yet stable and may change between versions. Some documentation sections may be outdated.

Repetitions & Backtracking

Syntax

A repetition is $( … ) followed by an optional separator and a required count suffix:

$( … )*        zero or more, no separator
$( … )+        one or more, no separator
$( … )?        zero or one
$( … ),*       zero or more, comma-separated
$( … ),+       one or more, comma-separated
$( … );+       one or more, semicolon-separated

Only , and ; are valid separators. Anything else is a parse error.

The same syntax works on both sides of the arrow. On the pattern side it captures a sequence; on the body side it replays one:

const $vec = macroRules`
  ($($x:Expr),*) => {
    const __v = [];
    $( __v.push($x); )*
    __v
  }
`;

Body-side separators use a lookahead

On the body side, a , or ; immediately after $( … ) is only treated as a separator when a *, +, or ? follows it. Otherwise it stays literal text. That is what makes this work as written:

$($rest),+     // comma is the separator
$( stmt; )+    // semicolon is inside the group, emitted each time

Length inference

The number of iterations is inferred from the sequence-bound metavariables mentioned inside the group:

  • Mention noneUnanchoredRepetition error. The expander has no way to know how many times to repeat.
  • Mention two of different lengthsInconsistentSequenceLength error.
// Error: nothing inside the repetition is sequence-bound.
($($x:Expr),*) => { $( console.log("hi"); )* }

Nested repetitions are supported in bodies, and each level infers its own length from the variables it mentions.

Bounded backtracking (value position)

Repetitions are greedy, but the matcher will backtrack to let a trailing metavariable match. This pattern works:

const $last = macroRules`
  ($($init:Expr),* $last:Expr) => $last
`;

$last(1, 2, 3); // → 3

The matcher first lets $($init),* consume everything, fails to bind $last, then gives back one element and succeeds. Backtracking is bounded — it gives back one element at a time rather than exploring every split — so it’s predictable and cheap.

No backtracking in type position

This is the important limitation: type-position matching is greedy with no retry. The identical pattern in a kind: "type" macro cannot match, because the repetition consumes every type argument and never gives one back:

// Works in value position, never matches in type position:
($($t:Type),* $last:Type) =>

Restructure type-position patterns so the variable-length group is last, or match a fixed arity per arm.