Mighty macros — v0.4 spec¶
Status: shipped in v0.4.
Supersedes the placeholder discussion in spec §20.3 (v0.1).
Normative companions: spec §3.3 (macro keyword), spec §30.2 (deferred
feature index).
Goals¶
- Predictable. A v0.4 macro is a token template parameterised by
named parameters. Expansion is deterministic and visible in
mty dump --hir. - Hygienic. Macro-introduced bindings cannot capture or be captured by the caller's bindings.
- Sandboxed by shape. A macro body is data; the macro system executes no user code at build time. No I/O, no FFI, no environment access.
- Bounded. Recursive expansion is capped at 32 levels; the cap is part of the spec, not an implementation detail.
Non-goals (deferred to v0.5+)¶
- Procedural macros (token-tree → token-tree functions).
- Hygienic set-of-scopes name resolution. v0.4 uses expansion-time mangling — sound for the v0.4 surface, deliberately simpler than a full set-of-scopes implementation.
mac!name(...)syntactic marker. v0.4 reuses ordinary function-call syntax; the marker (and the matching MT6001 check) lands in v0.5.- Compile-time function evaluation.
Grammar¶
MACRO_DECL = "macro" NAME "(" PARAM_LIST? ")" "=>" "{" BODY_TOKENS "}"
PARAM_LIST = NAME ("," NAME)*
BODY_TOKENS = ... brace-balanced opaque tokens ...
A macro decl is an item. It is visible from the point of declaration to the end of the enclosing file (no forward references across files in v0.4; cross-file macro export is v0.5 territory).
Call sites¶
v0.4 has no syntactic marker for macro calls. A macro is invoked using ordinary function-call syntax:
macro assert_eq(a, b) => {
if a != b { panic("assert_eq failed") }
}
fn main() {
assert_eq(1 + 1, 2)
}
The HIR lowering pre-pass replaces every CALL_EXPR whose callee
resolves to a registered macro with the expanded body, then re-parses
the resulting source. After this pre-pass, downstream stages see only
the expansion.
Expansion semantics¶
For an expansion of name(arg_1, ..., arg_n) against a definition
macro name(p_1, ..., p_n) => { body }:
- Arity check. If
ndoes not equal the declared parameter count, diagnostic MT6002 is raised and the call is replaced with the integer literal0to keep the surrounding parse well-formed. - Parameter substitution. Each
IDENTtoken inbodywhose text matchesp_iis replaced by( arg_i_tokens ). The surrounding parens preserve precedence at the call site. - Hygiene mangling. Any identifier introduced by a
letbinding inside the body — and any subsequent reference to that name within the same body — is renamed to__mac_<ctx>_<orig>, where<ctx>is a fresh per-expansion counter and<orig>is the source name. Parameters and free names are not renamed. - Splice + re-parse. The expansion is spliced into the source at the call site's byte range. The full file is re-parsed.
- Iterate. If the re-parsed source contains further macro calls (because the expansion itself called another macro, or because an outer macro produced a call after substitution), repeat from step 1 for that next layer. Stop when no macro calls remain or after 32 iterations.
MAX_EXPANSION_DEPTH = 32 is part of this spec. Hitting it raises
MT6004 for every call still present and aborts further expansion.
Hygiene scope (v0.4 subset)¶
Hygiene applies to the simple binding shape let IDENT [: TYPE] = ...
and let mut IDENT [: TYPE] = .... Tuple, struct, and reference
patterns in let inside macro bodies are explicitly out of scope:
their bindings will not be mangled in v0.4, and any collision with a
caller binding is a real shadowing situation that name resolution will
report normally.
This restriction is acceptable because v0.4 declarative macros are intended for small templates (assertions, guards, simple sugar); authors needing complex local-binding patterns should hand-write the code or wait for v0.5's set-of-scopes hygiene.
Diagnostic codes¶
- MT6001
unknown_macro— reserved; not raised in v0.4 (see Non-goals). - MT6002
macro_arity_mismatch— wrong arg count. - MT6003
macro_body_parse_failed— expanded body did not parse. - MT6004
recursive_macro_too_deep— depth cap exceeded.
Worked acceptance example¶
macro assert_eq(a, b) => {
if a != b { panic("assert_eq failed") }
}
fn main() {
assert_eq(1 + 1, 2)
}
After expansion, fn main lowers to a HirExpr::If over
(1 + 1) != (2) with a panic("assert_eq failed") consequent — the
same HIR as if the user had typed the if by hand. Verifiable via:
Compatibility¶
v0.4 declarative macros are forward-compatible with v0.5's planned additions:
- Adding
mac!name(...)syntax does not change the meaning of any existing v0.4 program (which never uses the marker). - Switching the hygiene engine from mangling to set-of-scopes
preserves the v0.4 behaviour for programs whose
letbindings follow the v0.4 supported shapes. - Introducing procedural macros adds a new MacroDef variant; the registry and the HIR-lowering hook stay shape-compatible.