fix(semantic): reject a macro rule pattern that reuses a placeholder name - #10302
fix(semantic): reject a macro rule pattern that reuses a placeholder name#10302orizi wants to merge 6 commits into
Conversation
e3f372f to
8cf3e0b
Compare
08026fe to
b654579
Compare
8cf3e0b to
acb286b
Compare
PR SummaryHigh Risk Overview Declaration-time checks add E2204 when a pattern binds the same placeholder twice (expansion validation is skipped for that rule so depth errors are not mis-attributed). E2203 flags expansion Item macro calls skip expansion when the call syntax has missing/parser errors, so only parse diagnostics surface. Extensive golden tests cover nested repetition, flatten/broadcast, Reviewed by Cursor Bugbot for commit a6418d1. Bugbot is set up for automated code reviews on this repo. Configure here. |
b654579 to
e8f1e18
Compare
1cf10bb to
d009269
Compare
e8f1e18 to
7a54e5b
Compare
7a54e5b to
5596948
Compare
4353179 to
ba1d59f
Compare
5596948 to
80b2e30
Compare
80b2e30 to
75c3eb4
Compare
ba1d59f to
2cd7a83
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, have a team admin enable autofix in the Cursor dashboard.
Reviewed by Cursor Bugbot for commit 75c3eb4. Configure here.
| let mut diagnostics = SemanticDiagnostics::new(callsite_module_id); | ||
| // Skipping the expansion of a macro call that had a parser error, as the reported parser errors | ||
| // already describe the problem. | ||
| if macro_call_syntax.as_syntax_node().descendants(db).any(|node| node.kind(db).is_missing()) { |
There was a problem hiding this comment.
Redundant missing-node walk
Low Severity
The new parse-error skip walks descendants and checks is_missing by hand, duplicating contains_missing, which the expression-position path and this PR’s own pattern check already use. That helper walks the green tree on purpose so this check does not materialize a red SyntaxNode per descendant.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 75c3eb4. Configure here.
There was a problem hiding this comment.
Fixed at the source - the guard is #10297's; it now uses contains_missing, and this PR inherits it.
orizi
left a comment
There was a problem hiding this comment.
@orizi+AGNT made 1 comment and resolved 1 discussion.
Reviewable status: 0 of 7 files reviewed, all discussions resolved (waiting on eytan-starkware and TomerStarkware).
| let mut diagnostics = SemanticDiagnostics::new(callsite_module_id); | ||
| // Skipping the expansion of a macro call that had a parser error, as the reported parser errors | ||
| // already describe the problem. | ||
| if macro_call_syntax.as_syntax_node().descendants(db).any(|node| node.kind(db).is_missing()) { |
There was a problem hiding this comment.
Fixed at the source - the guard is #10297's; it now uses contains_missing, and this PR inherits it.
eytan-starkware
left a comment
There was a problem hiding this comment.
@eytan-starkware+AGNT made 3 comments.
Reviewable status: 0 of 7 files reviewed, 2 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/items/macro_declaration.rs line 377 at r1 (raw file):
reused_names: OrderedHashMap<SmolStrId<'db>, SyntaxStablePtrId<'db>>, /// The path of the pattern elements being traversed. current_path: Vec<usize>,
current_path and next_rep_id are recursion scratch, not "placeholders the pattern defines" - after collect one is empty and the other meaningless, and a second collect on the same value keeps numbering from the old counter. keep them as params like the free function did and hand back the result:
impl<'db> PatternPlaceholders<'db> {
fn collect(
db: &'db dyn Database,
elements: impl IntoIterator<Item = ast::MacroElement<'db>>,
) -> Self {
let mut res = Self::default();
res.collect_elements(db, elements, &mut vec![], &mut 0);
res
}
}caller becomes let placeholders = PatternPlaceholders::collect(db, pattern_elements.elements(db));.
crates/cairo-lang-semantic/src/diagnostic.rs line 1218 at r1 (raw file):
SemanticDiagnosticKind::DuplicateMacroPlaceholder(name) => { format!( "Macro placeholder '{}' is already captured by this rule's pattern. A \
second sentence restates the first, and it's the only two-sentence message among its neighbours (Undefined macro placeholder: 'x'.).
format!(
"Macro placeholder '{}' is already captured by this rule's pattern.",
name.long(db)
)
`priv_macro_call_data` called `expand_macro_rule(..).unwrap()`, so any expansion
failure at item position was an ICE. At expression position the same failures
went through `skip_diagnostic()` and produced no diagnostic at all. Both
`Err` paths inside the expansion (a missing capture in the `MacroParam` arm, and
`find_first_repetition_param` returning `None` in the `MacroRepetition` arm) are
now a typed `MacroExpansionError` carrying the offending node in the rule's
expansion, reported by both callers as the new E2202
`SemanticDiagnosticKind::MacroExpansionFailed`. No `skip_diagnostic` remains on
that path, and item position can no longer panic.
Also ports the expression path's missing-descendant guard to
`priv_macro_call_data`: an item-position macro call whose syntax contains a
parse error is now skipped before path resolution and rule matching, so the
parse errors are no longer joined by a spurious E2158/E2156.
rustc ground truth (rustc 1.96.0 (ac68faa20 2026-05-25)), reproduced in this
change. rustc's transcriber-side definition errors are lazy, so the probe has to
invoke the macro - rustc reports the definition-site error upon invocation:
$ cat probe_invoked.rs
macro_rules! m {
() => { $(foo)* };
}
fn main() {
m!();
}
$ rustc --edition 2021 --emit=metadata probe_invoked.rs
error: attempted to repeat an expression containing no syntax variables matched as repeating at this depth
--> probe_invoked.rs:2:14
|
2 | () => { $(foo)* };
| ^^^^^
The definition-only counterpart compiles clean, which is why the citation is
worded "upon invocation":
$ cat probe_def_only.rs
macro_rules! m {
() => { $(foo)* };
}
fn main() {}
$ rustc --edition 2021 --crate-type lib -A unused probe_def_only.rs; echo $?
0
Bless-then-revert evidence. With the goldens in place and only the four source
hunks reverted, `cargo test --profile=ci-dev -p cairo-lang-semantic
expr_diagnostics::inline_macros` gives:
* item position - a panic, not a diagnostic:
thread 'expr::test::expr_diagnostics::inline_macros' panicked at
crates/cairo-lang-semantic/src/items/macro_call.rs:154:71:
called `Result::unwrap()` on an `Err` value: DiagnosticAdded
* expression position - silent failure (with the item-position section removed
so the run gets past the panic):
Test "Test expression-position macro call whose expansion repeats a
placeholder-free block." failed.
`expect_diagnostics` is true, but no diagnostics were generated.
* D9, new golden - a spurious semantic error on top of the parse error:
error[E1001]: Missing token ']'.
--> lib.cairo:5:6
m!([1);
^
<
<error[E2158]: No matching rule found in inline macro `m`.
< --> lib.cairo:5:1
<m!([1);
<^^^^^^^
* D9 also removes the same spurious diagnostic from two pre-existing goldens:
`error[E2158]` from "Regression for #9938: item-scope macro invocation without
arg brackets does not ICE." and `error[E2156]: Inline macro `MyEnum::A` not
found.` from items/tests/enum. In both, the parse error and (for the enum
case) the load-bearing `error[E0006]: Type not found.` survive.
The new goldens also cover a passing control (a genuine repetition with two
captures, no new diagnostic) and a macro declared in a different module than the
call, confirming the caret still renders in the declaring module.
Gates: cargo test --profile=ci-dev -p cairo-lang-semantic; cargo test
--profile=ci-dev --workspace; ./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 -- tests/bug_samples --starknet.
…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.
…cro captures Introduces `CaptureTree` (`Leaf`/`Seq`), a per-placeholder tree mirroring the repetition nesting of the macro pattern that matched it, and has the matcher build one per placeholder name in `MatcherContext` alongside the existing flat `captures`, which stays authoritative. Group `k` of a pattern repetition is element `k` of its `Seq`, so a repetition that matched zero times leaves an empty `Seq` in the position its groups would have taken - something the flat representation cannot express. Expansion is untouched. A debug assertion at the end of a successful match verifies the invariant tying the two representations together: the in-order leaves of a name's tree are exactly the name's flat capture list, every captured name has a tree, and a tree's leaves are all at one nesting level. Pattern param names are not verified to be unique, so a name may be used at conflicting nesting positions, which a single tree cannot mirror. Such a name is recorded as poisoned - its tree is not authoritative and consumers must fall back to the flat captures - rather than tripping the assertion on legal input. Pure refactor with zero behavior change: `git diff -- '*test_data*'` is empty, `cargo test -p cairo-lang-semantic` is green, as are the `cairo-lang-lowering`, `cairo-lang-plugins`, `cairo-lang-doc` and `cairo-lang-compiler` suites, `cairo-test tests/bug_samples --starknet` and the corelib macro tests. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tor behaviors
Tests only - no production code changes. These goldens pass unchanged on this
branch; their value is pinning behaviors that the upcoming per-group expansion
rewrite must preserve, so that its review shows them as zero-churn.
Repetition operator constraints on a nested, NON-final group
------------------------------------------------------------
Cairo enforces `+`/`?` in validate_repetition_operator_constraints(), after the
whole pattern matched, over every repetition occurrence - including the inner
repetition's occurrence in each group of an outer repetition. The new pins cover
the case where the violating group is not the last one, each with a `*`
counterpart proving the rejection comes from the operator constraint and not from
a plain match failure:
* `($([$($x:ident),+]),*)` on `m!([], [a, b])` -> E2158 no-matching-rule.
* `($([$($x:ident),*]),*)` on `m!([], [a, b])` -> accepted.
* `($([$($x:ident)?]),*)` on `m!([a b], [c])` -> E2158 no-matching-rule.
* `($([$($x:ident)*]),*)` on `m!([a b], [c])` -> accepted.
Cross-checked on rustc 1.96.0 (ac68faa20 2026-05-25), every program run with an
invocation because macro_rules! transcriber errors are lazy:
macro_rules! m { ($([$($x:ident),+]),*) => { 0 }; }
fn main() { let _ = m!([], [a, b]); }
error: no rules expected `]`
note: while trying to match meta-variable `$x:ident`
macro_rules! m { ($([$($x:ident),*]),*) => { 0 }; }
fn main() { let _ = m!([], [a, b]); }
compiles, rustc exit 0
macro_rules! m { ($([$($x:ident)?]),*) => { 0 }; }
fn main() { let _ = m!([a b], [c]); }
error: no rules expected `b`
note: while trying to match `]`
macro_rules! m { ($([$($x:ident)*]),*) => { 0 }; }
fn main() { let _ = m!([a b], [c]); }
compiles, rustc exit 0
The `?` cases carry no separator because rustc rejects one at definition time:
`($([$($x:ident),?]),*)` gives "error: the `?` macro repetition operator does not
take a separator". Cairo accepts `,?`, so the pins use the rustc-legal form.
The outer level holds 2 groups in every case, and the non-violating group holds 2
captures wherever the operator allows it - `?` caps its own group at 1 by
construction. The `+` invocation is `m!([], [a, b])` rather than `m!([], [a])` to
keep >= 2 captures per repetition level.
Trailing separator - deliberate divergence from rustc
-----------------------------------------------------
`($($x:ident),*)` matches `m!(a, b,)`: the matcher consumes a separator after
every match and only then tries the next one, so a trailing separator is absorbed
and does not add an empty match. The pinned tuple type `(felt252, felt252)` fixes
the match count at 2, so the golden also fails if the count changes.
rustc rejects the equivalent call:
macro_rules! m { ($($x:expr),*) => { [$($x),*] }; }
fn main() { let _arr: [i32; 2] = m!(1, 2,); }
error: unexpected end of macro invocation
note: while trying to match meta-variable `$x:expr`
The same program with `m!(1, 2)` compiles, rustc exit 0.
This pins the current behavior, not the desired one. Aligning with rustc - a
trailing separator no longer matching a `$(...),*` repetition - is a separate,
already scheduled change (graph node separator-grammar.sep-trailing-f13), which
will re-bless this golden into a rejection.
The call is written over several lines because the Cairo formatter deletes a
trailing separator from a single-line macro call and the golden framework formats
`function_code`; the multi-line form is the only one the formatter keeps - it even
emits the trailing separator itself when breaking a macro call, so the divergence
is reachable from formatted source.
Perturbations used to verify each pin fails when the pinned behavior changes. All
were run locally and reverted before the gates; none is committed:
1. validate_repetition_operator_constraints(): `if rep_id != RepetitionId(0) {
continue; }`, so only the outermost repetition is validated. Reddens exactly
the `+` and `?` pins; the 92 other tests in the file, including the
pre-existing flat `+`/`?` ones, still pass.
2. validate_repetition_operator_constraints(): a nested `*` must match exactly
once, `ZeroOrMore if rep_id != RepetitionId(0) && count != 1 => return false`.
Reddens both `*` pins, plus the pre-existing "expansion nesting matching the
pattern nesting" test, which pins the same property.
3. is_macro_rule_match_ex()'s repetition loop: reject a consumed separator that is
not followed by a match, i.e. rustc's rule. Reddens exactly the
trailing-separator pin - no other test in the file depends on a trailing
separator being absorbed.
Gates: cargo test --profile=ci-dev -p cairo-lang-semantic; ./scripts/rust_fmt.sh;
./scripts/clippy.sh --profile=ci-dev; cargo run --profile=ci-dev --bin cairo-test
-- tests/bug_samples --starknet.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Macro expansion read the flat capture list, so a `$()` block was iterated over
*every* value a placeholder ever captured instead of over the values of the group
being expanded. With `($([$($x:expr),*]),*) => { ($([$($x),*]),*) }`,
`m!([1, 2], [3, 4])` expanded to `([1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4])`; it
now expands to `([1,2],[3,4])`. Expansion now walks the nested `CaptureTree` the
matcher builds (previous node), and the flat machinery is deleted.
The algorithm
-------------
`ExpansionContext` (replacing the `matcher_ctx`-threading `expand_macro_rule_ex`)
keeps `group_indices`: the index of the group being expanded, one per `$()`
expansion block currently entered, outermost first.
* `MacroParam`: `CaptureTree::at(&group_indices)` descends one `Seq` level per
index. Reaching a `Leaf` before the indices run out is a broadcast - the
placeholder repeats less deeply than the block, so its single value is used in
every group. Reaching a `Seq` after the last index means the placeholder is
nested deeper than the blocks it is used in, which E2198 rejects at declaration
time; if it happens anyway the E2202 `MissingCapture` error is returned rather
than panicking, likewise for an out of range index, which E2199 rejects.
* `MacroRepetition`: the number of groups is the length of the `Seq` reached by
descending with the current index prefix, for each placeholder used anywhere in
the block whose captures still repeat at its depth. E2203 (previous node)
guarantees at least one such placeholder exists and E2199 that they all come
from one pattern repetition, hence agree; both are verified rather than assumed,
and a violation returns E2202 instead of expanding to something arbitrary.
Separator emission and its `i + 1 < len` condition are unchanged.
Deleted: `Captures` and `MatcherContext::captures`, `placeholder_to_rep_id` (its
two per-capture inserts and the blanket `for placeholder_name in
ctx.captures.keys()` rebind loop), `repetition_indices` (and the dead
`for i in 0..match_count` fill loop), `find_first_repetition_param`, the previous
node's `capture_trees_are_valid` assertion with `CaptureTree::leaves` /
`is_uniformly_nested`, and `poisoned_capture_trees`. `current_repetition_stack:
Vec<RepetitionId>` became `repetition_depth: usize`, its only remaining use being
the nesting depth; `open_repetition_groups` still runs strictly before the
increment. `is_macro_rule_match` returns `CaptureTrees`, so `MatcherContext` and
`RepetitionId` are private again and both callers (expr and item position) just
pass the trees.
`repetition_match_counts` / `repetition_operators` keep per-ENCOUNTER
`RepetitionId`s exactly as on main - each group's inner repetition gets a fresh
id, which is what validates `+`/`?` per group - and were deliberately not
stabilized across groups. The trailing-separator acceptance and the
separator-consumption loop in the matcher are untouched.
One behavior change on a pattern no check covers yet: with duplicate placeholder
names at conflicting depths (legal today - see the `TODO(Dean): Verify uniqueness
of param names.`) the conflicting values are dropped from the tree instead of
being emitted in an arbitrary order. Where the surviving tree cannot drive the
expansion block, as in `($y:ident, $($y:ident),*) => { $($y),* }`, that is now
reported as E2202; where it is still well formed, as in
`($($p:expr),* ; $($($p:expr),*),*)`, the dropped values simply do not appear and
no diagnostic is produced - E2202 is not a guaranteed backstop for duplicate
names. E2202's `RepetitionWithoutPlaceholder` reason, dead text since E2203 moved
that defect to declaration time, is replaced by `UndeterminedRepetitionCount`, of
which the first shape is the reachable case.
Goldens - expansion (src/expr/expansion_test_data/inline_macros)
---------------------------------------------------------------
Six new cases. Every expected text was derived by running the equivalent
`macro_rules!` on rustc 1.96.0 (ac68faa20 2026-05-25), transcribing through
`stringify!` so the transcriber output itself is observed. The Cairo expansions
are wrapped in a tuple so that they are single expressions. Verbatim rustc runs:
macro_rules! nested { ($([$($x:expr),*]),*) => { stringify!($([$($x),*]),*) }; }
macro_rules! bcast_tok { ($p:ident, $($x:ident),*) => { stringify!($($p $x),*) }; }
macro_rules! bcast_add { ($p:expr, $($x:expr),*) => { stringify!($($p + $x),*) }; }
macro_rules! sums { ($([$($x:expr),*]),*) => { stringify!($((0 $(+ $x)*)),*) }; }
macro_rules! flatten { ($([$($x:expr),*]),*) => { stringify!($($($x),*),*) }; }
nested!([1, 2], [3, 4]) -> [1, 2], [3, 4]
bcast_tok!(f, a, b) -> f a, f b
bcast_add!(10, 1, 2) -> 10 + 1, 10 + 2
sums!([1, 2], []) -> (0 + 1 + 2), (0)
sums!([], []) -> (0), (0)
sums!() -> (empty)
flatten!([1, 2], [3, 4]) -> 1, 2, 3, 4
(a) nested groups, `nested!([1, 2], [3, 4])` -> `([1,2],[3,4])`.
(b) broadcast, `broadcast!(10, 1, 2)` -> `(10+ 1,10+ 2)`, the `$p + $x` form of the
rustc-verified `f a, f b`.
(c) zero match: `sums!([1, 2], [])` -> `((0 + 1+ 2),(0 ))` and `sums!([], [])` ->
`((0 ),(0 ))` - an empty inner group expands to nothing while its outer group
is still expanded. `sums!()` -> `()` is a PRESERVATION pin, not fix evidence:
with no captures at all the old code also emitted nothing, so it passes on the
pre-fix branch too.
(d) depth flattening, `flatten!([1, 2], [3, 4])` -> `(1,2,3,4)`, an inner-depth
placeholder emitted by two nested expansion blocks over a nested pattern.
Every level of every case holds >= 2 captures, and the varying group is not the
first one in (c).
Bless-then-revert: with only this node's four production hunks reverted
(diagnostic.rs, expr/compute.rs, items/macro_call.rs, items/macro_declaration.rs
checked out from the previous node, all three earlier foundation nodes applied),
the five fix-evidence cases fail as `expanded_code` output-tag mismatches -
(a) `([1,2,3,4],[1,2,3,4],[1,2,3,4],[1,2,3,4])`, (b) `10+ 1`, (c)
`((0 + 1+ 2),(0 + 1+ 2))` and `()`, (d) `(1,2,3,4,1,2,3,4,1,2,3,4,1,2,3,4)`.
Goldens - diagnostics (src/expr/test_data/inline_macros)
-------------------------------------------------------
Two new cases, both failing on the reverted branch:
(g) item position, `($([$($name:ident),*]),*) => { $($(fn $name() {})*)* }` with
`make_fns!([first, second], [third, fourth]);` - no diagnostics. Pre-fix each
of the four functions is generated once per group, giving four E2118 "The name
`first` is defined multiple times." errors. Items generated by an item-position
call are not resolvable from the calling module, so this collision, rather than
calls to the generated functions, is what makes the case depend on per-group
expansion.
(h) `($y:ident, $($y:ident),*) => { $($y),* }` -> the new E2202 message, the
duplicate-name case described above. Pre-fix it emitted `base,one,two` and
produced E2117 "Parser error in macro-expanded code".
The behavior-pins goldens from the preceding node have ZERO diffs, as does every
other pre-existing case in both files. Exactly one pre-existing golden is churned:
* "Test depth-0 placeholder used inside a repetition driven by a depth-1 var
(valid)." - its expansion `$($x + $y),*` is now correctly expanded once per
capture of `y`, so `m!(1 + 2, 3, 4);` yields a two element comma list, which is
not a single expression. What the case asserts - that the declaration-time
checks accept a depth-0 placeholder broadcast into a depth-1 block - is
unchanged, so the expansion was wrapped in a tuple, as its neighbour
`array![$($base + $x),*]` already is, instead of flipping a "(valid)" case into
an error. The old single-iteration output was the bug.
Gates: cargo test --profile=ci-dev -p cairo-lang-semantic (106 passed);
./scripts/rust_fmt.sh; ./scripts/clippy.sh --profile=ci-dev;
scripts/check_comment_punctuation.py crates;
cargo run --profile=ci-dev --bin cairo-test -- tests/bug_samples --starknet (68
passed). Fallout beyond the listed gates: cargo check --workspace --all-targets,
./scripts/validate_error_codes.sh, cairo-test corelib (737 passed) and cargo test
over cairo-lang-compiler / -doc / -plugins / -lowering / -starknet / -defs.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
75c3eb4 to
33e5dcd
Compare
…name
A pattern using one name for two placeholders leaves the nesting depth of the
values it stands for ambiguous, so nothing can decide which of the two the
expansion means. It is now a declaration-time error, E2204, reported once per
reused name at that name's second use in the pattern. The rule's `err` is set,
so it is not expanded: neither the expression-position path (`rule.err?`) nor
the item-position one emits a second diagnostic on top of it. This resolves the
`TODO(Dean): Verify uniqueness of param names.`.
The expansion check is skipped for such a rule. Its placeholder paths keep the
*last* occurrence of a reused name, so E2198/E2199 would measure the expansion
against an arbitrary depth and report the pattern's ambiguity as a defect of the
expansion. `([$([$($p:ident),*]),*] [$($p:ident),*]) => { fn $p() {} }` reported
`E2198: ... 'p' requires 1 repetition level(s) ...` before this change - `p` is
at depth 2 in its first occurrence, and the "1" came from the second. That
misfire is replaced, not doubled up on.
Base re-verification. The witness `($($x:expr),* ; $x:expr) => { $x }` on
`m!(1, 2; 3)` no longer expands to `1`: driving an expansion block by the
repetition at its own depth already turned it into an expansion-time E2202
("has no captured value in this repetition") with no expansion. The
declaration-time defect is untouched, and other shapes still generate silently
wrong code - `([$($x:ident),*] [$($x:ident),*]) => { 0 $(+ $x)* }` on
`dup!([a, b][c, d])` expanded to `0 + a + b + c + d`, concatenating both sibling
groups, and `($p:ident, $p:ident, $p:ident) => { $p }` on `dup!(a, b, c)`
expanded to `a`. Both are E2204 now.
A hard error rather than a softer rollout: no Cairo macro rule in `corelib/`,
`tests/`, `crates/` or `examples/` reuses a placeholder name within one pattern -
the only hit was the golden re-blessed below. `cairo-test -- tests/bug_samples
--starknet` (68 passed) and `cairo-test -- corelib --filter macro` (49 passed)
confirm it against the real corelib.
rustc's `macro_rules!` agrees, rejecting all four shapes at definition time with
`error: duplicate matcher binding`, its caret on the whole duplicate matcher
(`$x:expr`) - the same anchor E2204 uses. `($($a:ident),* ; $b:expr)` is
accepted there as it is here.
The re-blessed golden, "a repetition whose placeholder shares its name with a
non-repeating one", was the repro of the E2202 added when the expansion failure
paths stopped panicking, and its trigger was a duplicate name
(`($y:ident, $($y:ident),*)`). It now reports E2204 at the pattern's second
`$y:ident` instead of E2202 at the expansion's `$($y),*` block, so it was
retitled and its comment - which asserted that names are not verified to be
unique - rewritten. It doubles as the different-nesting-depths case. E2202 is
left without a golden as a result; no non-duplicate-name trigger for it is
known, and its arms are kept because a rule with a reused name is still
*matched* - every rule is matched before its `err` is honored - so the
value-dropping paths in the matcher stay reachable and stay defensive.
Names are unique per pattern, not per macro: a later rule of the same macro may
reuse them, which the negative-control golden pins.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
33e5dcd to
a6418d1
Compare



A pattern using one name for two placeholders leaves the nesting depth of the
values it stands for ambiguous, so nothing can decide which of the two the
expansion means. It is now a declaration-time error, E2204, reported once per
reused name at that name's second use in the pattern. The rule's
erris set,so it is not expanded: neither the expression-position path (
rule.err?) northe item-position one emits a second diagnostic on top of it. This resolves the
TODO(Dean): Verify uniqueness of param names..The expansion check is skipped for such a rule. Its placeholder paths keep the
last occurrence of a reused name, so E2198/E2199 would measure the expansion
against an arbitrary depth and report the pattern's ambiguity as a defect of the
expansion.
([$([$($p:ident),*]),*] [$($p:ident),*]) => { fn $p() {} }reportedE2198: ... 'p' requires 1 repetition level(s) ...before this change -pisat depth 2 in its first occurrence, and the "1" came from the second. That
misfire is replaced, not doubled up on.
Base re-verification. The witness
($($x:expr),* ; $x:expr) => { $x }onm!(1, 2; 3)no longer expands to1: driving an expansion block by therepetition at its own depth already turned it into an expansion-time E2202
("has no captured value in this repetition") with no expansion. The
declaration-time defect is untouched, and other shapes still generate silently
wrong code -
([$($x:ident),*] [$($x:ident),*]) => { 0 $(+ $x)* }ondup!([a, b][c, d])expanded to0 + a + b + c + d, concatenating both siblinggroups, and
($p:ident, $p:ident, $p:ident) => { $p }ondup!(a, b, c)expanded to
a. Both are E2204 now.A hard error rather than a softer rollout: no Cairo macro rule in
corelib/,tests/,crates/orexamples/reuses a placeholder name within one pattern -the only hit was the golden re-blessed below.
cairo-test -- tests/bug_samples --starknet(68 passed) andcairo-test -- corelib --filter macro(49 passed)confirm it against the real corelib.
rustc's
macro_rules!agrees, rejecting all four shapes at definition time witherror: duplicate matcher binding, its caret on the whole duplicate matcher(
$x:expr) - the same anchor E2204 uses.($($a:ident),* ; $b:expr)isaccepted there as it is here.
The re-blessed golden, "a repetition whose placeholder shares its name with a
non-repeating one", was the repro of the E2202 added when the expansion failure
paths stopped panicking, and its trigger was a duplicate name
(
($y:ident, $($y:ident),*)). It now reports E2204 at the pattern's second$y:identinstead of E2202 at the expansion's$($y),*block, so it wasretitled and its comment - which asserted that names are not verified to be
unique - rewritten. It doubles as the different-nesting-depths case. E2202 is
left without a golden as a result; no non-duplicate-name trigger for it is
known, and its arms are kept because a rule with a reused name is still
matched - every rule is matched before its
erris honored - so thevalue-dropping paths in the matcher stay reachable and stay defensive.
Names are unique per pattern, not per macro: a later rule of the same macro may
reuse them, which the negative-control golden pins.
Co-Authored-By: Claude Fable 5 noreply@anthropic.com