Repository navigation
fix(#1290): a parameter that shadows a file-scope name moves like a local - #1993
Conversation
…ocal ADR-057: 1.4 settles a shadowing parameter's C name as <function>__<name>, and the .c signature, the .h prototype and every use in the body read that one answer, so global.X no longer binds the parameter. ScopeFunctions.adopt shares DeferredTypes' in-place scope.functions rewrite so the registry's scope holds the settled function. TEMPLATE.md: the matrix axes cannot see declaration kind. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
|
I'll fix CI failures and address comments from users with write access. I'll skip comments containing "(aside)".
|
process(u32 process) shadows the function name, a file-scope name, so the parameter moves to process__process as a local of that name already does. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
jlaustill
left a comment
There was a problem hiding this comment.
Review Summary
PR: #1993: fix(#1290): a parameter that shadows a file-scope name moves like a local
Reviewed by: Claude (automated review)
Head: f785d4ae. CI: All Checks Passed success at 2026-10-11T13:28:06Z (the earlier failure was the cancelled superseded run). Sonar on PR 1993: 0 open issues, gate OK, new coverage 96.8%.
Overview
The decision is in the right place. 1.4 settles a parameter's C name once on its frame and stamps it on IParameterInfo, so the .h prototype and the .c signature read one answer. The ADR-057 rule now covers parameters, which the ADR requires.
The problem is downstream. Before this PR, a parameter's emitted name always equalled its source spelling, so every render path that emits or compares the source spelling was right by coincidence. This PR breaks that coincidence and finds only three of those paths. I found three more by probing struct, string, bitmap and pass-on parameters. Each one reproduces on this head and is correct on main when global. is not used. So this PR regresses programs that main compiles correctly: any parameter that shadows a global, even with no global. anywhere.
Findings
| Severity | Count |
|---|---|
| Critical | 2 |
| Important | 1 |
| Moderate | 2 |
| Nit | 0 |
Key Issues
- [critical] Silent miscompile: a shadowing parameter passed to another C-Next function is passed as the global.
ArgumentGenerator.handleIdentifierArgreturnsid, notemitted. Execution repros below: a string returns the wrong length with no warning, C++ writes the global through a reference, C segfaults. - [critical] Non-compiling C: a shadowing struct parameter gets
.for member read and write. A shadowing bitmap parameter's field read shifts the pointer. Cause:PostfixExpressionGeneratordecides "is this the root" byctx.result === ctx.rootIdentifier, which compares the emitted name with the source spelling. The same comparison already breaks pointer-held locals onmain. I filed that as #1994 with a minimal repro and a control. This PR touches the same check, so it owns that fix: the structural fix covers both, and the PR can sayFixes #1994. - [important] One decision, several derivations: the PR adds three call sites that each work out a parameter's C name with
emittedLocalNameAt, and the argument path needs a fourth.TypeValidator.resolveBoundIdentifiermakes the same binding→C-name decision for locals and records ADR-057 provenance.emittedLocalNameAtrecords nothing. As a result, both new fixtures are listed indocs/scope-context-matrix.mdunder "linked fixtures with no derivable context": they occupy no ADR-057 cell, so the cross-file fixture's mutation check reddens no transitive cell. - [moderate] The fixture tests only
u32scalars and au32array. Struct, string, bitmap and pass-on parameters each render through their own ADR-006 path, and all three regressions above are in those paths. - [moderate]
ScopeFunctions.adoptwas extracted but has no unit test of its own.
What Looks Good
- The C name is decided in 1.4, on the frame, and both the
.hand the.cread it, so the prototype and the definition cannot disagree. ScopeFunctions.adoptremoves the copy of the in-placescope.functionsrewrite thatwithSettledParameterswould otherwise have needed.- The
plainCase(u32 fresh)negative control, and a mutation check that reverts the exemption and reddens both fixtures.
🤖 Automated review by Claude Code
…mber by position (Fixes #1994) - ArgumentGenerator passes a parameter on under its settled C name: a moved parameter passed as written was the global it shadows (C++ wrote it). - TranspileState.localCName is the one place a bound local's or parameter's body C name is decided, and records ADR-057 provenance when it moves; parameterBindingAt carries the parameter with that name to every site. - PostfixExpressionGenerator decides 'member of the root' by position, as the write path's isFirstOp does, not by comparing the C name with the name as written (struct/bitmap parameters, #1994's pointer-held local). - The write path's root holding is bound by the name as written, so a moved struct parameter's member write takes '->' and global.pt.x does not. - Unit test for ScopeFunctions.adopt; fixtures for struct, string, bitmap, by-reference pass-on parameters and #1994's pointer-held local. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
…eads Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
jlaustill
left a comment
There was a problem hiding this comment.
Review Summary
PR: #1993: fix(#1290): a parameter that shadows a file-scope name moves like a local
Reviewed by: Claude (automated review)
Overview
This moves a parameter that shadows a file-scope name to <function>__<name>, as #1289 does for locals. 1.4 decides the name once (LexicalFrames). The .c signature, the .h prototype and the body all read that decision. The approach is sound and I found no correctness bug in the changed code. The notes are about one decision still made in two places, one positional pairing worth asserting, and two places where the PR's claims run ahead of the code.
Verification I ran (head a61ed85a5, base 0cd96994b)
- CI: 16 of 17 check-runs on
a61ed85a517dconcludedsuccess, andDeploy Coveragewasskipped(gh api .../commits/<sha>/check-runs). Sonar:total 0open issues forpullRequest=1993, gateOK, new coverage 95.1%. - Mutations: reverting the struct root check to text fails
parameter-kindsandpointer-local-shadow. Reverting the bitmap check failsparameter-kinds. Both match the PR's claims. Removing the newScopeFunctions.adoptcall inwithSettledParametersfailsProgram.test.ts > the copy a scope holds > is the SAME object the file's symbol list holds, which passes 52/52 without the mutation. So that call is guarded too. - Extra probes, all correct: a callback parameter that shadows a global function, called both bare and with
global.. Astring<N>parameter written withstrncpyand measured. Bit-range and compound writes through a by-reference parameter. A shadowing parameter as an array index, aswitchsubject and a ternary operand. A private scope function (no stray prototype). A duplicate function, which still reaches E0425 as claimed.
Findings
| Severity | Count |
|---|---|
| Critical | 0 |
| Important | 1 |
| Moderate | 1 |
| Nit | 4 |
Key Issues
- [important]
Fixes #1994would close #1994 with its first box unmet. The read path decides "is this the root" by position, and the write path still decides it in its own loop (PostfixChainBuilder.isFirstOp). That is decided twice, not once. - [moderate] Frame and symbol parameters are paired by index, but the invariant checks only the count.
PR description accuracy
- The body says the ADR-057 edit was "approved in the issue thread". #1290's only comment (
gh api --paginate repos/jlaustill/c-next/issues/1290/comments: 1 comment,2026-10-11T11:37:18Z) is the bot's "Waiting on plan approval before changing the transpiler." There is no approval on the thread. ADR-057 already promised thatglobal.sees past a shadow, so under CLAUDE.md this edit documents a codegen fix and may not have needed approval. Even so, the description shouldn't cite an approval the thread doesn't hold. Link where it was given, or reword. - "Every place a parameter is written into C now reads its settled name": the callback typedef doesn't (inline nit).
Not caused by this PR, found while probing
u32 total <- total + 1;in a block insidef(u32 total)emitsuint32_t f__total = cnx_clamp_add_u32(f__total, 1U);, a read of uninitialized memory (exit 1).mainemits the same self-read under the old parameter name, and also foru32 x <- x + 1;with no global involved. Already tracked by #1643.- MISRA C:2012 Rule 5.3 is not checked at all. The cppcheck 2.13
misra.pythatvalidate:cruns has nomisra_5_3(it definesmisra_5_1,5_2,5_4…5_9), and an obvious inner-hides-outer sanity case reports nothing. So a block-local that re-shadows a moved parameter (both becomef__total, as nested locals already do) gets through ungated. I found no issue tracking this. Per the "fix it or file it" rule it needs its own issue.
What Looks Good
- One decision, three readers.
LexicalFramessettles the name once, and the.hprototype now matches the.cdefinition, as MISRA 8.3 requires of declarations. The PR also removes a whole class of Rule 5.3 violations (a parameter hiding a global) from generated C. - Fixtures that can't pass by coincidence: distinct values, execution tests in both C and C++, a negative control (
plainCase(u32 fresh)), and a cross-file case. IBoundParametermakes it structurally hard for a call site to hold a parameter without its C name, which is the right fix for theid/emittedconfusion.ScopeFunctionsextracts theDeferredTypesspecial case into a tested helper.
🤖 Automated review by Claude Code
… names a parameter as its prototype does - IChainHead.rootOp is the one decision; the read path's planned member ops and the write path's IPostfixOperation both carry it as appliesToRoot, so neither re-counts positions (#1994 box 1) - callback typedefs write the settled parameter C name - Program.ts: frames invariant and per-pair source-name invariant - ParameterDereferenceResolver: emittedName alias, JSDoc - test-adr: 057 on the #1290 and #1994 fixtures; ADR-057 references #1994 Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
….4, which settles parameter C names Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
jlaustill
left a comment
There was a problem hiding this comment.
Review Summary
PR: #1993: fix(#1290): a parameter that shadows a file-scope name moves like a local
Reviewed by: Claude (automated review), round 3, head d9809012
Overview
Both earlier rounds' findings are resolved on this head. "Applies to the root" is decided once, on IChainHead.rootOp. The argument path passes the C name. The callback typedef writes the settled name. The frame/symbol pairing asserts names per index. I re-checked each one and found no remaining issue in the changed render code. One defect remains in #1290's own construct, in a pass this PR does not touch: 1.3 Declare's modification analysis still binds global.<name> <- to the shadowing parameter by spelling (inline).
Verification I ran (head d9809012, base 0cd96994b, local clone)
npx tsc --noEmitclean.npm run unit: 428 files / 8496 tests pass.npm run test:q: 1620/1620, and the working tree was clean afterwards (no snapshot drift).- CI (
gh api …/commits/d9809012…/check-runs, read 15:58Z): 15 runssuccess,Deploy Coverageskipped, andC Static Analysis (misra)stillin_progress. The previous headee2b8205failedUnit Testswith1.4 settled the C name of aPoke's parameter d, which is whatd9809012addresses. Sonar analysedd9809012(SonarCloud Code Analysissuccess at 15:55:29Z):total 0open issues forpullRequest=1993, gateOK, new coverage 96.4%. - Probes on this head, all correct:
.char_countread twice on a shadowingstring<16>parameter: the length cache keys by the rendered C name.bump(global.total)in a shadowing function: the collector's call-edge arm already excludesglobal..- An ADR-029 callback-type function whose parameter shadows a global: body rename and
global.write. global.pt.yread/write beside a struct parameterpt:rootIdentifierisundefinedfor aglobal.root, sorootParameteris too.
- Cache: only C/C++ symbols are written to the cache (
CachedSymbolReaderrejects C-Next ones), so a staleIParameterInfowithoutemittedNamecannot reach render.
Findings
| Severity | Count |
|---|---|
| Critical | 0 |
| Important | 1 |
| Moderate | 0 |
| Nit | 1 |
Key Issues
- [important]
global.total <- …(andthis.count <- …) marks the shadowing parameter as modified. It becomesuint32_t*/uint32_t&andPoint*instead of by-value /const Point*. In C++,f(5)then does not compile. Cause and a measured one-line fix are inline. - [nit] PR description: the Changes list says
docs/scope-context-matrix.md"is regenerated", but that file is not in the diff. It came back tomain's content once the fixtures landed in already-occupied cells (youree2b8205reply says so). The Testing section's "355 files / 7426 tests pass" is also stale: this head runs 428 / 8496. Please carry the SHA a count came from, or drop the count.
Filed while probing (not this PR's code; each reproduces on main with no shadowing)
- #1999: an ADR-029 callback-type function that writes its own primitive parameter is emitted as
uint32_t*againsttypedef void (*handler_fp)(uint32_t). C segfaults at exit 0, and C++ rejects it. ADR-029 promises a matching typedef; which body-independent shape it should take is an open question for the owner. - #2000: a write to a block-local that shadows a parameter is recorded against the parameter. This is the lexical-scope arm of the inline finding; it needs block-declaration tracking, not the grammar alternative.
What Looks Good
- One root decision:
IChainHead.rootOp, carried asappliesToRootby identity on both the planned read ops andIPostfixOperation. Neither path counts positions any more. Aglobal./this.chain's root op isops[1], and its root binds no parameter. IBoundParameter: the C name travels with the parameter, soParameterDereferenceResolverandSimpleIdentifierResolvercannot write the spelling.localCNameis the single site that records ADR-057 provenance, which is why the fixtures now occupy matrix cells.- One 1.4 answer for the
.c,.hand_fptypedef: the signature, prototype and typedef all read the parameter name 1.4 copied from its frame, asserted per pair by name. Thed9809012test change makes the opaque-callback case go through 1.4 rather than relaxing the invariant.
🤖 Automated review by Claude Code
…rameter modified A shadowing parameter whose global the body writes through `global.` was counted as modified by spelling, so it went by pointer/reference and lost auto-const; in C++ a literal argument then did not compile. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
The frames lookup reuses framesOf's one assertion instead of a second copy. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
A qualified assignment target names a file-scope variable or a scope member, never a local. The scope-member pre-scan likewise skips global. targets and bare writes a local or parameter binds. Fixtures in C and C++, each arm mutation-checked. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
jlaustill
left a comment
There was a problem hiding this comment.
Review Summary
PR: #1993: fix(#1290): a parameter that shadows a file-scope name moves like a local
Reviewed by: Claude (automated review), round 4, head bd4f034d
Overview
Round 3's finding is fixed: a global./this. assignment target no longer marks a parameter as modified. The new global-write-past-parameter fixture can fail. Its C++ cell compiles only if the parameter is by value. This round reviews the three commits since d9809012. The new #2001 fix in InitializationAnalyzer introduces a regression: valid code with a this. write inside critical/forever is now rejected. It also leaves one binding question answered two ways in a single analyzer. Both point to pre-existing defects, filed as #2003 and #2004. Each belongs to this PR for the reason given inline.
Verification I ran (local clone at bd4f034d, base 0cd96994b)
- CI (
gh api …/commits/bd4f034d…/check-runs, read 18:19Z): every check's latest run issuccessandDeploy Coverageisskipped. The onefailureis the superseded run'sAll Checks Passed(17:20:51Z); that check's rerun succeeded at 17:29:04Z. Sonar:total 0open issues forpullRequest=1993, gateOK. - Probes on this head vs
main(npx tsx src/cli/index.ts … --target host):this.xwrites insidecritical/forever:mainaccepts, this head reports E0381. This is a regression (inline).for-variable write shadowing a member: the result depends on declaration order (inline).- Parameter shadowing a scope member:
mainmisses the uninitialized member, this head catches it, but no fixture covers it (inline). - Bare write inside
critical: correct on both.
- Prototype of the walker fix (inline, +19 −123):
npm run unit428 files / 8505 tests,npm run test:q1626/1626, no snapshot change.
Findings
| Severity | Count |
|---|---|
| Critical | 0 |
| Important | 2 |
| Moderate | 1 |
| Nit | 0 |
Key Issues
- [important] Regression: a
this.xwrite insidecritical { }orforever { }is now invisible to E0381. The listener's new early return relies on the #1019 pre-scan, and that pre-scan's hand-rolled walker skips both kinds. The sharedChildStatementCollectorandCallbackUseCollectorskipforevertoo, and onmainthat gives uncompilable C. #2003 has the reproductions and the measured one-walker fix. - [important] One binding question, two answers: the pre-scan asks 1.4 (
lexicalDeclarationAt), and the listener asks its ownScopeStack. They disagree on afor-header variable, and the result then depends on declaration order. #2004 has the reproductions.IProgram.bindValueis the existing single decision, and it also covers the qualified-target tests. - [moderate] Test gap: a parameter that shadows a scope member, written bare (#1290's construct in #2001's arm), has no fixture.
Filed while probing
- #2003: three statement walkers skip
forever(one also skipscritical). Reproductions onmain: a struct parameter written inforeveris emittedconst(C and C++ reject it); a callback registered inforeverkeeps a by-value parameter (gcc-Werrorrejects it); and thecriticalmember write above. - #2004: E0381's own scope stack never declares a
for-header variable. That gives a false E0381 onmain, and on this head an order-dependent result for the write arm.
What Looks Good
- Round 3's fix landed with a guard that can fail:
global-write-past-parameterpasses literal arguments, so its C++ cell compiles only while the parameter stays by value. The u32, struct andthis.cases each have a control next to them, and the spelling mutation fails the fixture. - Invariants now have tests:
Program.test.tschecks the frame/symbol pairing by count and by name, with a control, andFunctionContextManager.test.tscoversparameterEmittedName, including an unsettled parameter and an out-of-range index.withSettledParametersreusesframesOf's assertion rather than keeping a second copy. - The "check everywhere" sweep found #2001, and it was fixed with error fixtures that each name the case to stay silent, plus an execution control.
🤖 Automated review by Claude Code
…ker covers forever - ChildStatementCollector gains a forever arm; CallbackUseCollector and the InitializationAnalyzer scope-member pre-scan take their children from it instead of recursing by hand (they skipped forever, the pre-scan critical too) - InitializationAnalyzer keys state by what IProgram.bindValue binds a spelling to, from the target's own root; its ScopeStack (now unused) is deleted and it no longer declares parameters itself - fixtures: forever/critical children (#2003), for-variable binding (#2004), a parameter that shadows a scope member (#2001) Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
… round-4 fixtures Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
…s a use; restore JSDoc Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
ReturnPathAnalyzer reads TStatement roles (whenTrue/whenFalse, case/default bodies) from 1.2's lowering instead of the parse tree. CallbackUseCollector, ModificationCollector, InitializationAnalyzer's scope-member scan and CodeGenWalker's callback-type scan share StatementChildren.of, which has an arm per statement kind. ChildStatementCollector, StatementExpressionCollector and AssignmentTargetExtractor are deleted; five fewer modules hold a parse tree. A nested call's argument is no longer recorded twice in the call graph. Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
…ionUtils helper Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
jlaustill
left a comment
There was a problem hiding this comment.
Review Summary
PR: #1993: fix(#1290): a parameter that shadows a file-scope name moves like a local
Reviewed by: Claude (automated review), round 5, head 14d9241c9
Overview
Round 4's three findings are resolved. #2003 and #2004 are fixed with fixtures, and parameter-write-scope-member covers a parameter that shadows a scope member. This round reviews the five commits since bd4f034d: statement children from the lowered tree, InitializationAnalyzer on bindValue, and the deleted parse-tree helpers. I found no regression on valid code. Two pre-existing defects sit in code this PR rewrites, and the PR owns both (inline). #2003's first box is ticked on a claim that a grep disproves.
Verification I ran (scratch clone at 14d9241c9, base 0cd96994b)
npm run buildandnpx tsc --noEmitclean.npm run unit: 426 files / 8447 tests pass.npm run test:q: 1634/1634, andgit statuswas clean afterwards (no snapshot drift).- CI (
gh api …/commits/14d9241c9…/check-runs, read 20:37Z): both runs on this head aresuccesson every check.Deploy Coverageisskipped. The latest run isAll Checks Passedat 20:33:23Z. Sonar:SonarCloud Code Analysissuccessat 20:35:03Z,total 0open issues forpullRequest=1993, gateOK. - Probes,
main(7d7f27158) vs this head,npx tsx src/cli/index.ts … --target host:- E0704 on a scope method, and a non-void function whose body is
critical { return 3; }: identical diagnostics and positions. framedDeclaration's invariant: locals declared in aswitchcase,default,doand a bare block all transpile on both.- A nested-block local shadowing a parameter that shadows a global: correct on this head, and it executes under
-Wall -Wextra -Werror. - A write in a
forheader: the same wrong output on both (inline, #2010). - A body local that redeclares a parameter: compiles on
main, and gcc rejects this head's output (inline, #2011).
- E0704 on a scope method, and a non-void function whose body is
Findings
| Severity | Count |
|---|---|
| Critical | 0 |
| Important | 3 |
| Moderate | 2 |
| Nit | 1 |
Key Issues
- [important] #2010: a parameter written only in a
forheader becomesconst Point*(gcc rejects the write) or by-value (the caller loses the write, exit 2). The #1019 pre-scan misses the same writes and reports a false E0381. Both walkers are rewritten here, andStatementChildrenis what leaves the header out. - [important] #2011: with the parameter exemption gone,
u32 f(u32 total) { u32 total <- 3; … }gives both declarationsf__total, and gcc rejects the C at exit 0.maincompiled it. The missing redeclaration diagnostic is the root defect, and one check covers all three shapes. - [important] #2003 box 1:
NullCheckAnalyzerandInitializationAnalyzer's listener still read anif's orwhile's children from the parse tree.BracedBodyAnalyzerre-lists the children itself. The box is not true yet under either reading. - [moderate] CLAUDE.md is stale (not in the diff, so no inline comment): "Const Inference" (lines 1014–1018) still names
ModificationCollector.walkStatementForModifications(),StatementExpressionCollector.collectAll()andChildStatementCollector.collectAll(), and says to "Update both when adding new statement types". This PR deletes all three. The section should nameStatementChildren.ofandModificationCollector.expressionsOf. - [moderate] Three switches over
TStatementend indefaultand skip a new kind without a compile error (inline).
Filed while probing (each reproduces on main)
- #2010: a write in a
forheader is invisible to 1.3's parameter-modification walk and to the #1019 member pre-scan. #1726 covers only the 2.1 listeners. - #2011: a name declared twice in one scope gets no diagnostic: two locals in one block, or a body local named like a parameter.
What Looks Good
StatementChildren.ofends in aneverarm. Each arm's mutation was checked against both unit tests and fixtures, and theforever/criticalmutation fails all four #2003 fixtures.InitializationAnalyzerkeeps state by the declaration 1.4 framed. Writes bind throughChainRoot.ofTarget. The line-only mutation, which fails 21 unit tests and a fixture, shows the span match is needed.- Three parse-tree helpers and
ScopeStackare deleted along with their tests and the depcruise entry, andparse-tree-sites.mdis regenerated (96 → 90) in the same commit.
🤖 Automated review by Claude Code
Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
…tree; exhaustive statement switches Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
…no longer stores one Co-Authored-By: Joshua Austill <jlaustill@gmail.com>
…adows-global Co-Authored-By: Joshua Austill <jlaustill@gmail.com> # Conflicts: # .dependency-cruiser.cjs # CLAUDE.md # docs/architecture/parse-tree-sites.md # scripts/__tests__/artifact-lifetime.test.ts # src/cli/__tests__/cnx/TRANSPILE/1-Analyze/InitializationAnalyzer.test.ts # src/cli/__tests__/cnx/TRANSPILE/1-Analyze/testAnalysisContextFor.ts # src/cli/__tests__/cnx/TRANSPILE/CodeGenWalker.test.ts # src/cli/__tests__/cnx/TRANSPILE/ProgramGeneration.ts # src/cli/__tests__/cnx/TRANSPILE/TranspileState.test.ts # src/cnx/PARSE/3-Declare/cnext/__tests__/ModificationCollector.test.ts # src/cnx/PARSE/4-Resolve/ScopeFunctions.ts # src/cnx/PARSE/4-Resolve/__tests__/ScopeFunctions.test.ts # src/cnx/TRANSPILE/1-Analyze/RedeclarationAnalyzer.ts # src/cnx/TRANSPILE/1-Analyze/ScopeStack.ts # src/cnx/TRANSPILE/1-Analyze/__tests__/ScopeStack.test.ts # src/cnx/TRANSPILE/1-Analyze/helpers/__tests__/AssignmentTargetExtractor.test.ts # src/cnx/TRANSPILE/1-Analyze/helpers/__tests__/ChildStatementCollector.test.ts # src/cnx/TRANSPILE/1-Analyze/helpers/__tests__/StatementExpressionCollector.test.ts # src/cnx/TRANSPILE/1-Analyze/types/IRedeclarationError.ts # src/cnx/TRANSPILE/types/IBoundParameter.ts # src/cnx/utils/LoweredStatements.ts # src/cnx/utils/StatementChildren.ts # src/cnx/utils/StatementWrites.ts # src/cnx/utils/__tests__/LoweredStatements.test.ts # src/cnx/utils/__tests__/StatementChildren.test.ts # src/cnx/utils/__tests__/StatementWrites.test.ts # src/cnx/utils/ast/AssignmentTargetExtractor.ts # src/cnx/utils/ast/ChildStatementCollector.ts # src/cnx/utils/ast/StatementExpressionCollector.ts
|



Summary
A parameter that shadows a file-scope name now moves to
<function>__<name>. This is the ADR-057 rule #1289 already applies to locals. Before this PR,global.totalinsideparamCase(u32 total)emitted astotal, so it returned the parameter. Every place a parameter is written into C now reads its settled name: signature, prototype, callback typedef, body reads/writes, member access, and arguments. That also fixes #1994's pointer-held local.A parameter that shadows nothing keeps its source name.
Changes
LexicalFrames.emittedName: the parameter exemption is gone. 1.4 settles the parameter's C name once, on itsILocalDeclaration.Program.withSettledParameters: copies each parameter's settled name ontoIParameterInfo.emittedName. A frame is matched to its symbol by declaration span, so an E0425 duplicate still reaches E0425. The.hreads the name throughIParameterSymbol.emittedNameand the.csignature throughFunctionContextManager.parameterEmittedName. Per-parameter facts (pass-by-value, modified, auto-const) stay keyed by the source name.TranspileState.localCName: the one place a bound local's or parameter's body C name is decided. It records ADR-057 provenance when the name moves.emittedLocalNameAtandTypeValidator.resolveBoundIdentifierboth call it.parameterBindingAtreturnsIBoundParameter { info, emittedName }.ParameterDereferenceResolver.resolve,SimpleIdentifierResolver(ISimpleIdentifierDeps.getParameter/resolveParameter) and the assignment-target path take that object.ArgumentGenerator.handleIdentifierArg: passes a parameter on under its C name. Before, a moved parameter passed by its source name was the global it shadows, and in C++ the callee wrote that global.PostfixExpressionGenerator:IMemberAccessContext.isRootOpis decided by position (op index 0, like the write path'sisFirstOp).tryStructParamAccessandtryBitmapFieldAccessused to compare the C name with the source name, which broke once the root moved.CodeGenWalker._buildPostfixChainDeps: the write path gets the root's holding from the parameter bound by the source name, and only for a bare root. Before, it usedfirstId(the C name), soS__f__pt.x = ...took..ScopeFunctions.adopt: the in-placescope.functionsrewriteDeferredTypesdid, shared. Unit-tested inScopeFunctions.test.ts.docs/decisions/TEMPLATE.mdadds DECLARATION KIND to the list of things the scope-context matrix axes cannot see.Pre-Submit Checklist
Code Quality
npx oxlint src/TRANSPILE src/PARSE/4-Resolve: 0 warnings, 0 errors)npx tsc --noEmit)npm test). Left to CI; see Testing.Documentation
learn-cnext-in-y-minutes.mdupdated (if syntax changed): n/a, no syntax changetests/[feature]/README.md: n/a,tests/bugs/fixtureGit Hygiene
Testing
tests/bugs/issue-1290-parameter-shadows-global/(execution, C and C++):parameter-shadows-global: scope method, free function, bare andglobal.reads, a write through the parameter, an array parameter, and a negative control (plainCase(u32 fresh)keepsfresh).parameter-shadows-included-global: a global from an included file.parameter-kinds:global.ptwrite/read past it, whole-value copy, passed onstring<16>passed on (the callee sees length 2)u32passed tobump: the caller's variable changes,global.countdoesn'ttests/bugs/issue-1994-pointer-local-shadow/: aFull*fromfull.hheld in a local that shadows a global. Write and read both take->.LexicalFramesparameter exemption fails both original fixtures.parameter-kinds:return idinArgumentGenerator, the struct or bitmap root check by text, or the write-side holding fromparameterAt(firstId). The struct root check by text also fails A pointer-held local that shadows a global reads its members with '.': readThrough__held.y on a Full*, gcc rejects it, exit 0 #1994.issue-1664-const-binding/non-const-shadows-const,issue-1666-shadow-type-registry/parameter-shadow,tests/scope/param-prefix-naming-valid. Related dirs re-run green: fix: bit-indexing a scalar function parameter emits shift-mask, not pointer deref #1100, docs(adr-057): the decision, not the implementation (#1285) #1289, The type registry andconstValuesare written by both 2.2 Plan and 2.3 Render under a flat key — #1452 box 2's remainder #1664, A shadowing local overwrites the type registry entry of the variable it shadows:.bit_lengthof a u32 emits 8, exit 0 #1666, An integer × float product is routed into the integer clamp helper: u32 3 × 2.5 evaluates to 6.0, exit 0 #1668, fix(#1668): mixed integer × float arithmetic is E0810, typed by one operand typer #1760 whole-value, A block-local shadowing a by-reference primitive parameter is read as (*x) inside its block: gcc rejects the C, exit 0 #1969,tests/scope.d9809012(round-3 review's run). For later heads, see CI'sUnit Testson the head SHA.Related Issues
Fixes #1290
Fixes #1994
Fixes #2001
Fixes #2003
Fixes #2004
Fixes #2010
Fixes #2011
Related to ADR-057, #1289, #1934
Second review
IChainHead.rootOp. The read path's planned member ops and the write path'sIPostfixOperationboth carry it asappliesToRoot, so neither counts positions on its own.typedef void (*handler_fp)(uint32_t, uint32_t handler__buf[2]);(tests/bugs/issue-1290-parameter-shadows-global/callback-typedef).Third review
global./this.assignment target no longer marks a same-spelled parameter as modified (now inModificationCollector, which reads the lowered tree; see "Three statement walkers skipforever(one also skipscritical): a parameter written there is emitted const, a callback registered there keeps a by-value parameter; gcc rejects both, transpiler exits 0 #2003 box 1" below).shadowWritesGlobal(u32 total) { global.total <- total; }now staysuint32_t shadowWritesGlobal__totalinstead ofuint32_t*/uint32_t&. Fixture:tests/bugs/issue-1290-parameter-shadows-global/global-write-past-parameter, with controls beside it. A write to a block-local that shadows a parameter is recorded as a write to the parameter: it loses const and becomes by-reference (exit 0) #2000 tracks the block-local arm.Program.test.ts"a parameter's settled C name",FunctionContextManager.test.ts"parameterEmittedName").withSettledParametersusesframesOf's existing assertion instead of a second copy. A "check everywhere" sweep found the sameglobal.-by-spelling mistake inInitializationAnalyzer(E0381), filed as A global.X assignment marks a same-named shadowing local as initialized, so E0381 never fires (exit 0) #2001 and fixed here (bd4f034d5): inInitializationAnalyzer, aglobal./this.assignment target no longer marks a same-named local initialized, and the scope-member pre-scan (Scope-member string read-before-init: not caught (should error) AND emits invalid C (= 0) #1019) skipsglobal.targets and bare writes that a local or parameter binds. Fixtures intests/bugs/issue-2001-qualified-write-initializes-local/(4 E0381 error cases + an execution control, C and C++); each of the three guards was mutation-checked against them. NineInitializationAnalyzerunit tests now build their program withtestAnalysisContextFor, because the pre-scan reads the file's lexical frames.Fourth review
forever(one also skipscritical): a parameter written there is emitted const, a callback registered there keeps a by-value parameter; gcc rejects both, transpiler exits 0 #2003: superseded by "Three statement walkers skipforever(one also skipscritical): a parameter written there is emitted const, a callback registered there keeps a by-value parameter; gcc rejects both, transpiler exits 0 #2003 box 1" below.ChildStatementCollectoris deleted;forever/criticalchildren now come from the lowered tree.InitializationAnalyzerkeeps state per binding:IProgram.bindValue(sourceFile, ChainRoot.ofTarget(target), name, at)for writes (bare for reads), keyed by theILocalDeclaration1.4 returns or the variable's C name. ItsScopeStackis gone (no other user, soScopeStack.tsand its test are deleted), it no longer declares parameters, and a local's state is keyed by the declaration 1.4 framed, found by its identifier's span inlexicalFrameAt's frame (framedDeclaration,0a8501c76; an invariant asserts it, and matching on line alone fails 21 unit tests and theglobal-write-local-shadowfixture). Parameters andforvariables have no state, so they read as initialized. TheGLOBAL()/THIS()early return and the pre-scan'slexicalDeclarationAttest are replaced by that one call.tests/bugs/issue-2003-forever-critical-children/: struct and scalar parameter written inforever(C and C++), callback registered inforever(-Werror),this.countwritten incritical(C only, host xfail IRQ wrappers reference avr-libc and CMSIS symbols without emitting their headers — AVR and fallback arms do not compile #1147 like the othercriticalfixtures) and inforever, each read by a function declared before the writer.tests/bugs/issue-2004-initialization-binds-through-1-4/: thefor-variable read executes; the write arm reports E0381 with the reader after and before the writer.issue-2001-qualified-write-initializes-local/parameter-write-scope-member: a parametercountwritten infdoes not initialize the membercount(E0381 atreturn count).foreverarm: the 3foreverfixtures. Nocriticalarm:this-write-critical.CallbackUseCollectorskipping the collector's blocks:callback-forever. The pre-scan skipping them: boththis-write-*.parameter-write-scope-member,local-write-scope-member,for-variable-write-reader-before.global-write-local-shadow,this-write-local-shadow.main's stack-based analyzer put back: all 3 E0381 binds names on its own scope stack, which never declares a for-header variable: a loop variable that shadows a scope member reads as the member (false E0381, exit 1) #2004 fixtures andparameter-write-scope-member.bd4f034d's version fails thefor-variable read and reader-after; its pre-scan already bound through 1.4, so reader-before and the parameter fixture pass there.40c60db96: the 4 fixture dirs above plus A parameter shadowing a global makes global.X return the parameter — silently wrong C, exit 0 #1290 pass;vitestonsrc/TRANSPILE/1-Analyze,src/PARSE/3-Declare,src/utils: 140 files / 2997 tests pass;tsc,oxlint src,knipclean.FunctionCallAnalyzercallable variables leak across functions) and A c_ local in an inner block resets the null check of the outer c_ local with the same name (false E0908) #2006 (NullCheckAnalyzerinnerc_shadow resets the outer one's null check).#2003 box 1: statement children come from the lowered tree only
Joshua chose to make the box literally true: one place knows each statement's children by role.
StatementLowering(1.2) is the only code that reads a statement's children from the parse tree. ItsTStatementnames them by role (if.whenTrue/whenFalse, loopbody,switch.cases[].body/defaultCase, blockstatements).ReturnPathAnalyzer.analyze(program: IProgramSyntax)reads those role fields instead ofParser.ProgramContext.IFunctionDeclarationSyntax.nameSpancarries the span its diagnostic needs.StatementChildren.of(TStatement): the direct nested statements, in source order, with an exhaustiveswitch(a new statement kind is a compile error until it is placed).forinit/update are not children; the two walkers that need them read them explicitly.CallbackUseCollector,ModificationCollector(both now take the loweredIProgramSyntax;CNextResolver.resolvereceivesparsed.program),InitializationAnalyzer's scope-member pre-scan, andCodeGenWalker's callback-type pre-pass.ChildStatementCollector,StatementExpressionCollector,AssignmentTargetExtractorand their tests,ExpressionUtils.collectUnaryFromOrExpr/collectAdditiveFromOrExprs, and their.dependency-cruiser.cjsPARSE_TREE_HELPERSentry.docs/architecture/parse-tree-sites.mdregenerated: 96 -> 90 modules hold a parse tree.StatementChildren.test.ts(10), newModificationCollector.test.ts(7: parameter lists, nested writes, qualified writes, calls inforheaders, scope-qualified/this./cast calls, nested call arguments recorded once).ReturnPathAnalyzer/CallbackUseCollector/InitializationAnalyzertests build their input withProgramLowering.program(...).StatementChildrenarm returning[](if,while/for,doWhile/forever/critical,switch,block): 2, 2, 3, 1, 1 of its unit tests fail.forever/criticalreturning[]: all 4issue-2003-forever-critical-childrenfixtures fail.ReturnPathAnalyzerwithout itsforeverarm: 2 tests fail.StatementChildren.of:CallbackUseCollector1 unit test +callback-forever;ModificationCollector1 unit test +parameter-write-forever;InitializationAnalyzer2 unit tests + boththis-write-*;CodeGenWalkerbothissue-1484-callback-local-nestedfixtures.14d9241c9:tsc,oxlint,knip(unused-code:check),depcruise,cspell,parse-tree:check,scope-joins:check,coverage:matrix:check,diagnostics:manifest:check,typecheck:scriptsclean;vitestover everysrc/dir andscripts/passes; A parameter shadowing a global makes global.X return the parameter — silently wrong C, exit 0 #1290, A global.X assignment marks a same-named shadowing local as initialized, so E0381 never fires (exit 0) #2001, Three statement walkers skipforever(one also skipscritical): a parameter written there is emitted const, a callback registered there keeps a by-value parameter; gcc rejects both, transpiler exits 0 #2003, E0381 binds names on its own scope stack, which never declares a for-header variable: a loop variable that shadows a scope member reads as the member (false E0381, exit 1) #2004 fixture dirs pass. CI on14d9241c9: 16 passed, 0 failed. SonarCloud: 0 open issues (issues API).Fifth review
StatementWrites.of(TStatement)decides which assignments a lowered statement performs: an assignment statement, or aforheader's init (when it is an assignment) and update.ModificationCollectorandInitializationAnalyzer's Scope-member string read-before-init: not caught (should error) AND emits invalid C (= 0) #1019 pre-scan both take their writes from it.void walkField(Point p) { for (p.x <- 0; …) }now emitsPoint* p(C) /Point& p(C++), notconst.tests/bugs/issue-2010-for-header-writes/(execution, C and C++): struct parameter, scalar parameter (caller sees the write), scope member set only in aforheader and read in another function. Removing theforarm fails all 3.RedeclarationAnalyzer(2.1) walks 1.4's lexical frames. A function's parameters and its outermost block are checked as one scope, every other block as its own. A second declaration of a name in one scope isE0425 '<name>' is already declared in this scope.tests/bugs/issue-2011-redeclaration-in-one-scope/: two locals in one block, a local named like a parameter, the same with the parameter shadowing a global (test-adr: 057), and the control (the local one block deeper) that executes. Not merging the parameters with the outermost block fails the 2 parameter fixtures; not reporting a repeat fails all 3 error fixtures; the control passes under both.forever(one also skipscritical): a parameter written there is emitted const, a callback registered there keeps a by-value parameter; gcc rejects both, transpiler exits 0 #2003 box 1: the 2.1 parse-tree listeners no longer read a statement's parts from the parse tree. NewLoweredStatementsindexes the loweredTStatements by kind and start position, so a listener asks the lowered node:NullCheckAnalyzer: whether anifhas anelse(whenFalse), and whether the statement being entered is theif's then-branch or thewhile's body (LoweredStatements.startsAt).analyze(tree, program)now takes the lowered program.InitializationAnalyzer's listener:elsefromwhenFalse; the deterministic-forcheck reads the loweredfor'sinitandcondition.BracedBodyAnalyzerrecurses throughStatementChildren.of; its own switch only checks theif/else/while/forbodies.grep -rnE "\.(statement|block|ELSE|forInit|forUpdate)\(\)" srcoutside tests, the grammar and the lowering: no matches.InitializationAnalyzerelsealways/never (1 each), deterministicfornever (3);NullCheckAnalyzerthen-branch never (8),whilebody never (3),elsealways (3);BracedBodyAnalyzerwithout the shared recursion (10).scripts/__tests__/artifact-lifetime.test.ts"the walk works at all": its control wasNullCheckAnalyzer.currentIfCtx, the last 2.1 field that held a parse node, so moving the listener failed it on CI. The control now adds a class holding aParser.IfStatementContextfield to the ts-morph project (in memory, removed afterwards) and expects the scan to report exactly that field; with the field typednumberit fails.ReturnPathAnalyzer,ModificationCollector.expressionsOfandBracedBodyAnalyzerlist every statement kind and end in aneverarm, likeStatementChildren.ofandStatementWrites.of.CLAUDE.md"Const Inference" namesStatementWrites.of,ModificationCollector.expressionsOfandStatementChildren.ofinstead of the deleted collectors. TheIVariableStatecomment is back aboveIVariableState.docs/error-codes.mdlistsRedeclarationAnalyzer.tsunder E0425;docs/diagnostic-manifest.mdregenerated.78424449e:tsc,oxlint,knip,cspell,error-codes:checkclean;vitestonsrc/TRANSPILE/1-Analyze,src/utils,src/PARSE/3-Declare: 141 files / 2969 tests pass; fixture dirsnull-check,initialization,control-flow,for-loops,critical,do-while, and Grammar does not enforce braces on if/while/for bodies (MISRA 15.6 / ADR-027 mismatch) #1090, A parameter shadowing a global makes global.X return the parameter — silently wrong C, exit 0 #1290, A global.X assignment marks a same-named shadowing local as initialized, so E0381 never fires (exit 0) #2001, Three statement walkers skipforever(one also skipscritical): a parameter written there is emitted const, a callback registered there keeps a by-value parameter; gcc rejects both, transpiler exits 0 #2003, E0381 binds names on its own scope stack, which never declares a for-header variable: a loop variable that shadows a scope member reads as the member (false E0381, exit 1) #2004, A write in a for header is invisible to 1.3's parameter-modification walk and the #1019 member pre-scan: a struct parameter written there is const (gcc rejects), a scalar loses the write, a member is a false E0381 #2010, A name declared twice in one scope transpiles at exit 0 and gcc rejects the C: two locals in one block, or a body local named like a parameter #2011 pass. The full suite is left to CI.main(refactor(#1992): move the transpiler under src/cnx/ #2007 moved the transpiler undersrc/cnx/). This PR's added modules moved with it; the files it deletes stay deleted; the 8 text conflicts were import paths, the.dependency-cruiser.cjsrow, CLAUDE.md and the artifact-lifetime probe path. After the merge:tsc,typecheck:scripts,oxlint src,knip,depcruise,layout:check,cspelland every generated-doc:checkclean;vitestonsrc/cnx/utils,src/cnx/TRANSPILE/1-Analyze,src/cli/__tests__/cnx/TRANSPILE,src/cnx/PARSE/4-Resolve,ModificationCollector(140 files / 3465 tests) andartifact-lifetimepass; A parameter shadowing a global makes global.X return the parameter — silently wrong C, exit 0 #1290, A write in a for header is invisible to 1.3's parameter-modification walk and the #1019 member pre-scan: a struct parameter written there is const (gcc rejects), a scalar loses the write, a member is a false E0381 #2010, A name declared twice in one scope transpiles at exit 0 and gcc rejects the C: two locals in one block, or a body local named like a parameter #2011 fixture dirs pass.Link to Devin session: https://app.devin.ai/sessions/e67b3642a2bc4254921c59a4b247eb59
Open in Devin Desktop: https://app.devin.ai/desktop/session/e67b3642a2bc4254921c59a4b247eb59?variant=devin
Requested by: @jlaustill