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-separatedOnly , 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 timeLength inference
The number of iterations is inferred from the sequence-bound metavariables mentioned inside the group:
- Mention none →
UnanchoredRepetitionerror. The expander has no way to know how many times to repeat. - Mention two of different lengths →
InconsistentSequenceLengtherror.
// 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); // → 3The 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.