fix(semantic): reject a macro rule pattern that reuses a placeholder name - #10302
Conversation
e3f372f to
8cf3e0b
Compare
08026fe to
b654579
Compare
8cf3e0b to
acb286b
Compare
PR SummaryMedium Risk Overview Declaration checking uses a new Tests are updated/added for sibling repetitions, post-repetition reuse, triple reuse (single diagnostic), and the case where distinct names across rules in one macro remain valid. Reviewed by Cursor Bugbot for commit 99e75ec. Bugbot is set up for automated code reviews on this repo. Configure here. |
b654579 to
e8f1e18
Compare
1cf10bb to
d009269
Compare
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)
)
33e5dcd to
a6418d1
Compare
orizi
left a comment
There was a problem hiding this comment.
@orizi+AGNT made 2 comments and resolved 2 discussions.
Reviewable status: 0 of 7 files reviewed, all discussions resolved (waiting on TomerStarkware).
crates/cairo-lang-semantic/src/diagnostic.rs line 1218 at r1 (raw file):
Previously, eytan-starkware+AGNT (Agent AGNT for eytan-starkware) wrote…
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) )
Trimmed to the one sentence.
crates/cairo-lang-semantic/src/items/macro_declaration.rs line 377 at r1 (raw file):
Previously, eytan-starkware+AGNT (Agent AGNT for eytan-starkware) wrote…
current_pathandnext_rep_idare recursion scratch, not "placeholders the pattern defines" - aftercollectone is empty and the other meaningless, and a secondcollecton 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));.
Done, as suggested - collect builds and returns, collect_elements carries the scratch as params.
a6418d1 to
0c878f6
Compare
302fe00 to
c8a4835
Compare
0c878f6 to
37a120f
Compare
c8a4835 to
e0c6b94
Compare
37a120f to
14ed2f7
Compare
e0c6b94 to
baae183
Compare
14ed2f7 to
c3c400b
Compare
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 2 total unresolved issues (including 1 from previous review).
❌ 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 c3c400b. Configure here.
c3c400b to
e8a781b
Compare
630f42b to
6d44048
Compare
e8a781b to
50f49dc
Compare
6d44048 to
e892b4a
Compare
7758d81 to
5bd7273
Compare
e892b4a to
2515ca7
Compare
5bd7273 to
5739b7d
Compare
2515ca7 to
5563631
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>
5563631 to
e7dc075
Compare
5739b7d to
99e75ec
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