fix(semantic): reject a macro expansion block with nothing repeating at its depth - #10298
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
| /// | ||
| /// Two kinds of errors are reported: | ||
| /// Three kinds of errors are reported: | ||
| /// * Depth mismatch (E2198): placeholder used at fewer expansion levels than its pattern depth. |
There was a problem hiding this comment.
do not write error codes in comments - these are just for exernal consumption.
There was a problem hiding this comment.
Removed across the stack — including the preexisting E2198/E2199 mentions this doc block builds on, and the E2206 one in the pr4a follow-up. Also renamed the juxt test macro to juxtaposed to fix the typos CI.
54de8ab to
e2df0dd
Compare
99a511f to
ea21fb3
Compare
e2df0dd to
0f4a094
Compare
PR SummaryMedium Risk Overview
Existing placeholder-free repetition tests move from expansion-time E2202 to E2203 with an updated message; new goldens cover non-first offenders, Reviewed by Cursor Bugbot for commit f2b7370. Bugbot is set up for automated code reviews on this repo. Configure here. |
orizi
left a comment
There was a problem hiding this comment.
@orizi resolved 1 discussion.
Reviewable status: 0 of 3 files reviewed, all discussions resolved.
ea21fb3 to
df6b1f5
Compare
0f4a094 to
86a5bfc
Compare
df6b1f5 to
dfca0ac
Compare
af3c023 to
ff9e3a9
Compare
0fe495c to
ca2c79a
Compare
e3c19f7 to
ddc4b10
Compare
ca2c79a to
641715c
Compare
eytan-starkware
left a comment
There was a problem hiding this comment.
@eytan-starkware+AGNT made 5 comments.
Reviewable status: 0 of 3 files reviewed, 4 unresolved discussions (waiting on eytan-starkware, orizi, and TomerStarkware).
a discussion (no related file):
Note: the comments below are from an automatic orizi-review run (Claude agents reviewing in Ori's style, findings adversarially verified before posting). Treat with the usual bot skepticism.
crates/cairo-lang-semantic/src/expr/test_data/inline_macros line 2853 at r3 (raw file):
//! > ========================================================================== //! > Test broadcasting a non-repeating placeholder over a repeating one (valid).
this is the same case as Test depth-0 placeholder used inside a repetition driven by a depth-1 var (valid). at line 2410, just wrapped in array!. drop it.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 246 at r3 (raw file):
/// /// A placeholder undefined in the pattern counts as driving: it is reported on its own, and /// reporting the block too would double up on a single defect.
this branch (is_none_or) is documented but untested. add a case pinning that $($undefined)* reports only the undefined-placeholder error and not E2203 on top of it.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 253 at r3 (raw file):
enclosing_depth: usize, ) -> bool { block
you re-walk every repetition subtree here that check_node is about to walk anyway - O(depth * size) on nested blocks. also node is passed when repetition is already in hand; repetition.elements(db).as_syntax_node() is the precise subtree (the separator/operator can't hold params). if you want it in one pass, have check_node return the deepest placeholder path length it saw and compare on the way out - that also drops in_non_repeating_block, at the cost of reporting the innermost block instead of the outermost.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 279 at r3 (raw file):
/// Whether an enclosing `$()` block was already reported as non-repeating. A block /// nested in such a block fails for the same reason, so only the outermost one is reported. in_non_repeating_block: bool,
the whole point of this field is suppressing the cascade, and nothing tests it. add a case with nested placeholder-free blocks pinning that only the outer one is reported:
//! > Test nested placeholder-free repetitions in an expansion.
//! > test_runner_name
test_function_diagnostics(expect_diagnostics: true)
//! > cairo_code
macro m {
() => { $($(foo)*)* };
}
fn foo() {
m!();
}
…at its depth
`ExpansionCheckCtx` already tracks, for every placeholder, its pattern repetition depth
(`placeholder_paths[name].len()`) and the expansion depth it is used at (`curr_rep_depth`).
It now also validates each `$()` block of the expansion itself: a block nested in
`enclosing_depth` other blocks is legal only if it holds a placeholder whose pattern depth
exceeds `enclosing_depth`, i.e. something that actually repeats at this level and can drive
the block. Otherwise the number of repetitions is undetermined, and the new
declaration-time error E2203 is reported on the offending `MacroRepetition` and sets
`rule.err`, so the rule never expands.
Placeholders nested deeper inside the block count as drivers: in `$($($x),*),*` with `$x` at
pattern depth 1, `$x` sits in the inner block but is consumed by the outer repetition too, so
the outer block is legal and only the inner one is rejected.
E2203 was unallocated (main's general band ended at E2201, the parent commit took E2202;
E2300-E2315 is the `InferenceError` sub-band and is not part of this band). The next free
general semantic code is E2204.
Probed with rustc 1.96.0 (ac68faa20 2026-05-25) in this change. rustc's transcriber-side
definition errors are LAZY, so every probe below invokes the macro; rustc then reports the
definition-site error upon invocation. Probe sources are transcribed inline.
REJECTED - all four report
`attempted to repeat an expression containing no syntax variables matched as repeating at this
depth`:
* no metavariable: `macro_rules! m1 { () => { $(foo)* }; }` + `m1!();`
-> caret on `(foo)`.
* depth-0-only metavariable: `macro_rules! m2 { ($a:ident) => { $($a)* }; }` + `m2!(x);`
-> caret on `($a)`, plus `this similarly named macro metavariable is unrepeatable`.
* over-deep inner block: `macro_rules! m3 { ($($x:ident),*) => { $($($x),*),* }; }`
+ `m3!(a, b)` -> caret on the INNER `($x)`, not the outer block.
* offender not first: `macro_rules! m9 { ($($x:ident),*) => { stringify!($($x),* ; $(bar)*) }; }`
+ `m9!(a, b)` -> caret on `(bar)`; the leading well-formed block is silent.
ACCEPTED:
* broadcast: `macro_rules! m4 { ($p:ident, $($x:ident),*) => { stringify!($($p $x),*) }; }`
+ `m4!(f, a, b)` prints `f a, f b`.
* properly nested:
`macro_rules! m5 { ($([$($x:ident),*]),*) => { stringify!($([$($x),*]),*) }; }`
+ `m5!([a, b], [c, d])` prints `[a, b], [c, d]`.
* zero-match calls of those same two rules: `m4!(f,)` and `m5!()` print nothing, `m5!([], [])`
prints `[], []`.
Two rustc behaviours copied deliberately:
* `macro_rules! m7 { ($($x:ident),*) => { stringify!($($(foo),*),*) }; }` + `m7!(a, b)` reports
exactly ONE error, anchored at the OUTER block. A block nested in a failing block always
fails for the same reason (`inner_max <= outer_max <= d < d+1`), so `in_non_repeating_block`
suppresses nested E2203s. E2193/E2198/E2199 are still reported inside a suppressed block.
* `macro_rules! m10 { ($($x:ident),*) => { stringify!($($q)*) }; }` + `m10!(a, b)` reports one
error, not a separate "unbound metavariable" one. So an undefined placeholder counts as a
driver here and `$($undef)*` keeps reporting E2193 alone rather than E2193 + E2203.
STRICTER THAN RUSTC, deliberately: Cairo reports E2203 from `priv_macro_declaration_data`, so a
bad `macro` is rejected with no call anywhere, unlike rustc's lazy transcriber check. This
matches the neighbouring declaration-time checks E2193/E2198/E2199.
`$defsite`/`$callsite` are not placeholders (`extract_placeholder` filters them), so a block
holding only those is now E2203 too; it previously expanded to nothing silently. Grepping every
`$(` under corelib/ and all crate `test_data` found no such block - corelib's only
expansion-side repetitions (macro_test.cairo:239/241/281/321) are all driven by a depth-1
placeholder.
crates/cairo-lang-semantic/src/expr/test_data/inline_macros, re-blessed with a narrow
`CAIRO_FIX_TESTS=1` + `--lib expr::test::expr_diagnostics::inline_macros`. Hunk by hunk:
1. `:3067` title `Test item-position macro call whose expansion repeats a placeholder-free
block.` -> `Test placeholder-free repetition in an expansion, with an item-position call.`
The diagnostic is no longer produced by the call.
2. `:3086` `E2202` -> `E2203` with the new message. The caret is byte-identical: both the old
expansion-time error and the new check anchor on the same `MacroRepetition` node in the
declaration, and nothing else appears or disappears - `m!();` in item position adds no
fallout.
3. `:3093` title renamed for the same reason as hunk 1.
4. `:3112` `E2202` -> `E2203`, same caret. The expression-position call stays silent because
`compute.rs` propagates `rule.err` without reporting again.
5. `:3188` (cross-module `mod helpers`) `E2202` -> `E2203`, same caret in the declaring module,
title unchanged. The location survives for a new reason: the diagnostic used to live on the
root module's `MacroCallData` with a stable ptr into `helpers`; it now lives on `helpers`'s
own macro-declaration diagnostics, which the runner collects via
`get_recursive_module_semantic_diagnostics` (inline submodules).
6. seven new sections appended - three error shapes and three legal controls, plus an
offender-not-first variant modelled on rustc probe m9. Every repetition level with a
repetition carries >= 2 captures (`m!(first, second)`, `m!(base, one, two)`, `m!(1, 2)`,
`m!(10, 1, 2)`, `m!([1, 2], [3, 4])`).
Bless-then-revert: with only this commit's report disabled on this branch (`if false && ...`),
all six error goldens fail as output-tag mismatches, no panics -
* the three re-homed goldens and the offender-not-first golden fall back to E2202 (the
expansion-time path the parent commit added),
* `$($base)*` falls back to `error[E0006]: Identifier not found.` - the block silently
expanded to nothing,
* `$($($x),*),*` falls back to
`error[E2117]: Parser error in macro-expanded code: Skipped tokens. Expected: statement.`
The three legal controls pass with and without the check; a golden that emits no diagnostic
cannot change when the check is removed, so revert-mode evidence does not apply to them.
Those last two reverted outputs are the evidence for `rule.err`: E0006 and E2117 are produced
*by the expansion*, and they are gone once E2203 sets `rule.err`. The rule genuinely stops
expanding rather than expanding to nothing.
`MacroExpansionFailure::RepetitionWithoutPlaceholder` needs `find_first_repetition_param` to
return `None`, i.e. an expansion `$()` block with zero `MacroParam` descendants. Such a block
can never have a driving placeholder, so E2203 now rejects the rule at declaration time and
that arm is unreachable through Cairo source. `MacroExpansionFailure::MissingCapture` is
untouched; per the plan it should still be reachable via duplicate placeholder names in a
pattern (see the `TODO(Dean): Verify uniqueness of param names.`), which a later change rejects
at declaration time. E2202 stays either way as defensive hardening - neither arm may panic.
Gates: `cargo test --profile=ci-dev -p cairo-lang-semantic` (106 passed),
`./scripts/rust_fmt.sh`, `./scripts/clippy.sh --profile=ci-dev`,
`./scripts/validate_error_codes.sh`, `scripts/check_comment_punctuation.py crates`,
`cargo run --profile=ci-dev --bin cairo-test -- corelib/` (737 passed),
`cargo run --profile=ci-dev --bin cairo-test -- tests/bug_samples --starknet` (68 passed), plus
`cargo test --profile=ci-dev` over lowering / doc / starknet / sierra-generator / defs / plugins
as a declaration-time-fallout check.
641715c to
d2d530a
Compare
ddc4b10 to
f2b7370
Compare
orizi
left a comment
There was a problem hiding this comment.
@orizi+AGNT made 4 comments and resolved 4 discussions.
Reviewable status: 0 of 3 files reviewed, all discussions resolved (waiting on TomerStarkware).
crates/cairo-lang-semantic/src/expr/test_data/inline_macros line 2853 at r3 (raw file):
Previously, eytan-starkware+AGNT (Agent AGNT for eytan-starkware) wrote…
this is the same case as
Test depth-0 placeholder used inside a repetition driven by a depth-1 var (valid).at line 2410, just wrapped inarray!. drop it.
Dropped.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 246 at r3 (raw file):
Previously, eytan-starkware+AGNT (Agent AGNT for eytan-starkware) wrote…
this branch (
is_none_or) is documented but untested. add a case pinning that$($undefined)*reports only the undefined-placeholder error and not E2203 on top of it.
Added - Test an undefined placeholder in a repetition not reported as non-repeating too.: only the undefined-placeholder error, no E2203 on top.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 253 at r3 (raw file):
Previously, eytan-starkware+AGNT (Agent AGNT for eytan-starkware) wrote…
you re-walk every repetition subtree here that
check_nodeis about to walk anyway - O(depth * size) on nested blocks. alsonodeis passed whenrepetitionis already in hand;repetition.elements(db).as_syntax_node()is the precise subtree (the separator/operator can't hold params). if you want it in one pass, havecheck_nodereturn the deepest placeholder path length it saw and compare on the way out - that also dropsin_non_repeating_block, at the cost of reporting the innermost block instead of the outermost.
Restructured: a single prepass (collect_block_driving_depths) walks the expansion once and records, per $() block, the deepest pattern depth of any placeholder in it; check_node reads the map. One O(size) walk total, and the redundant node/repetition double argument is gone with the deleted helper.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 279 at r3 (raw file):
Previously, eytan-starkware+AGNT (Agent AGNT for eytan-starkware) wrote…
the whole point of this field is suppressing the cascade, and nothing tests it. add a case with nested placeholder-free blocks pinning that only the outer one is reported:
//! > Test nested placeholder-free repetitions in an expansion. //! > test_runner_name test_function_diagnostics(expect_diagnostics: true) //! > cairo_code macro m { () => { $($(foo)*)* }; } fn foo() { m!(); }
Added - Test nested placeholder-free blocks reporting only the outer one.: one E2203 on the outer block only.
eytan-starkware
left a comment
There was a problem hiding this comment.
@eytan-starkware reviewed 2 files and all commit messages, and made 2 comments.
Reviewable status: 2 of 3 files reviewed, 2 unresolved discussions (waiting on orizi and TomerStarkware).
crates/cairo-lang-semantic/src/expr/test_data/inline_macros line 2721 at r4 (raw file):
error[E2203]: Macro expansion block holds no placeholder that repeats at its depth, so the number of repetitions cannot be determined. --> lib.cairo:2:13 () => { $(foo)* };
Wouldnt this be 2202 (the old one) or have the old message change?
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 263 at r4 (raw file):
if let Some(param) = MacroParam::cast(db, node) { let Some(name) = extract_placeholder(db, ¶m) else { return 0 }; return placeholder_paths.get(&name).map_or(usize::MAX, |path| path.len());
Why usize::MAX? For optional returns use option not coded usize info.

ExpansionCheckCtxalready tracks, for every placeholder, its pattern repetition depth(
placeholder_paths[name].len()) and the expansion depth it is used at (curr_rep_depth).It now also validates each
$()block of the expansion itself: a block nested inenclosing_depthother blocks is legal only if it holds a placeholder whose pattern depthexceeds
enclosing_depth, i.e. something that actually repeats at this level and can drivethe block. Otherwise the number of repetitions is undetermined, and the new
declaration-time error E2203 is reported on the offending
MacroRepetitionand setsrule.err, so the rule never expands.Placeholders nested deeper inside the block count as drivers: in
$($($x),*),*with$xatpattern depth 1,
$xsits in the inner block but is consumed by the outer repetition too, sothe outer block is legal and only the inner one is rejected.
E2203 was unallocated (main's general band ended at E2201, the parent commit took E2202;
E2300-E2315 is the
InferenceErrorsub-band and is not part of this band). The next freegeneral semantic code is E2204.
Semantics mirrored from rustc
Probed with rustc 1.96.0 (ac68faa20 2026-05-25) in this change. rustc's transcriber-side
definition errors are LAZY, so every probe below invokes the macro; rustc then reports the
definition-site error upon invocation. Probe sources are transcribed inline.
REJECTED - all four report
attempted to repeat an expression containing no syntax variables matched as repeating at this depth:macro_rules! m1 { () => { $(foo)* }; }+m1!();-> caret on
(foo).macro_rules! m2 { ($a:ident) => { $($a)* }; }+m2!(x);-> caret on
($a), plusthis similarly named macro metavariable is unrepeatable.macro_rules! m3 { ($($x:ident),*) => { $($($x),*),* }; }m3!(a, b)-> caret on the INNER($x), not the outer block.macro_rules! m9 { ($($x:ident),*) => { stringify!($($x),* ; $(bar)*) }; }m9!(a, b)-> caret on(bar); the leading well-formed block is silent.ACCEPTED:
macro_rules! m4 { ($p:ident, $($x:ident),*) => { stringify!($($p $x),*) }; }m4!(f, a, b)printsf a, f b.macro_rules! m5 { ($([$($x:ident),*]),*) => { stringify!($([$($x),*]),*) }; }m5!([a, b], [c, d])prints[a, b], [c, d].m4!(f,)andm5!()print nothing,m5!([], [])prints
[], [].Two rustc behaviours copied deliberately:
macro_rules! m7 { ($($x:ident),*) => { stringify!($($(foo),*),*) }; }+m7!(a, b)reportsexactly ONE error, anchored at the OUTER block. A block nested in a failing block always
fails for the same reason (
inner_max <= outer_max <= d < d+1), soin_non_repeating_blocksuppresses nested E2203s. E2193/E2198/E2199 are still reported inside a suppressed block.
macro_rules! m10 { ($($x:ident),*) => { stringify!($($q)*) }; }+m10!(a, b)reports oneerror, not a separate "unbound metavariable" one. So an undefined placeholder counts as a
driver here and
$($undef)*keeps reporting E2193 alone rather than E2193 + E2203.STRICTER THAN RUSTC, deliberately: Cairo reports E2203 from
priv_macro_declaration_data, so abad
macrois rejected with no call anywhere, unlike rustc's lazy transcriber check. Thismatches the neighbouring declaration-time checks E2193/E2198/E2199.
$defsite/$callsiteare not placeholders (extract_placeholderfilters them), so a blockholding only those is now E2203 too; it previously expanded to nothing silently. Grepping every
$(under corelib/ and all cratetest_datafound no such block - corelib's onlyexpansion-side repetitions (macro_test.cairo:239/241/281/321) are all driven by a depth-1
placeholder.
Goldens
crates/cairo-lang-semantic/src/expr/test_data/inline_macros, re-blessed with a narrow
CAIRO_FIX_TESTS=1+--lib expr::test::expr_diagnostics::inline_macros. Hunk by hunk::3067titleTest item-position macro call whose expansion repeats a placeholder-free block.->Test placeholder-free repetition in an expansion, with an item-position call.The diagnostic is no longer produced by the call.
:3086E2202->E2203with the new message. The caret is byte-identical: both the oldexpansion-time error and the new check anchor on the same
MacroRepetitionnode in thedeclaration, and nothing else appears or disappears -
m!();in item position adds nofallout.
:3093title renamed for the same reason as hunk 1.:3112E2202->E2203, same caret. The expression-position call stays silent becausecompute.rspropagatesrule.errwithout reporting again.:3188(cross-modulemod helpers)E2202->E2203, same caret in the declaring module,title unchanged. The location survives for a new reason: the diagnostic used to live on the
root module's
MacroCallDatawith a stable ptr intohelpers; it now lives onhelpers'sown macro-declaration diagnostics, which the runner collects via
get_recursive_module_semantic_diagnostics(inline submodules).offender-not-first variant modelled on rustc probe m9. Every repetition level with a
repetition carries >= 2 captures (
m!(first, second),m!(base, one, two),m!(1, 2),m!(10, 1, 2),m!([1, 2], [3, 4])).Bless-then-revert: with only this commit's report disabled on this branch (
if false && ...),all six error goldens fail as output-tag mismatches, no panics -
expansion-time path the parent commit added),
$($base)*falls back toerror[E0006]: Identifier not found.- the block silentlyexpanded to nothing,
$($($x),*),*falls back toerror[E2117]: Parser error in macro-expanded code: Skipped tokens. Expected: statement.The three legal controls pass with and without the check; a golden that emits no diagnostic
cannot change when the check is removed, so revert-mode evidence does not apply to them.
Those last two reverted outputs are the evidence for
rule.err: E0006 and E2117 are producedby the expansion, and they are gone once E2203 sets
rule.err. The rule genuinely stopsexpanding rather than expanding to nothing.
E2202 reachability
MacroExpansionFailure::RepetitionWithoutPlaceholderneedsfind_first_repetition_paramtoreturn
None, i.e. an expansion$()block with zeroMacroParamdescendants. Such a blockcan never have a driving placeholder, so E2203 now rejects the rule at declaration time and
that arm is unreachable through Cairo source.
MacroExpansionFailure::MissingCaptureisuntouched; per the plan it should still be reachable via duplicate placeholder names in a
pattern (see the
TODO(Dean): Verify uniqueness of param names.), which a later change rejectsat declaration time. E2202 stays either way as defensive hardening - neither arm may panic.
Gates:
cargo test --profile=ci-dev -p cairo-lang-semantic(106 passed),./scripts/rust_fmt.sh,./scripts/clippy.sh --profile=ci-dev,./scripts/validate_error_codes.sh,scripts/check_comment_punctuation.py crates,cargo run --profile=ci-dev --bin cairo-test -- corelib/(737 passed),cargo run --profile=ci-dev --bin cairo-test -- tests/bug_samples --starknet(68 passed), pluscargo test --profile=ci-devover lowering / doc / starknet / sierra-generator / defs / pluginsas a declaration-time-fallout check.