Skip to content

A global.X assignment marks a same-named shadowing local as initialized, so E0381 never fires (exit 0) #2001

Closed

Description

@devin-ai-integration

Bug

A global./this. assignment counts as initializing a block local that has the same name and shadows that global. So E0381 never fires for a later read of the uninitialized local, and the transpiler exits 0.

Found while checking PR #1993 (#1290) against the cnext-way "check everywhere" rule. #1993 fixes the same mistake in 1.3's modification analysis (AssignmentTargetExtractor). InitializationAnalyzer reads the target the same way and is not touched by that PR (no diff against main).

Reproduction (run on fix/1290-parameter-shadows-global @ 119f5e2; InitializationAnalyzer.ts is identical to main)

struct Point {
    u32 x;
    u32 y;
}

Point pt <- {x: 1, y: 2};

u32 shadowUninit() {
    Point pt;
    global.pt.x <- 5;
    u32 r <- pt.x;
    return r;
}

npx tsx src/cli/index.ts init2.cnx --target host → Compiled 1 files (exit 0). The emitted C:

    Point shadowUninit__pt = {0};
    pt.x = 5U;
    uint32_t r = shadowUninit__pt.x;

Control. The same function with the local named other (Point other; global.pt.x <- 5; u32 r <- other.x;) is rejected:

error[E0381]: use of uninitialized variable 'other.x'

So the only difference is that the local and the global share a spelling.

Cause

InitializationAnalyzer.enterAssignmentStatement takes targetCtx.IDENTIFIER()?.getText() without checking targetCtx.GLOBAL() / THIS(), then records the assignment against whatever that spelling resolves to: the shadowing local. _collectAssignmentsInStatement does the same (its comment says "Both bare identifier and this.member use the same IDENTIFIER token").

Definition of done

  • A global./this. assignment target never marks a local as initialized. Execution/error fixture, in C and C++, with the different-name control beside it.
    • Evidence (PR fix(#1290): a parameter that shadows a file-scope name moves like a local #1993, bd4f034d5): tests/bugs/issue-2001-qualified-write-initializes-local/ -- global-write-local-shadow and this-write-local-shadow now report E0381; qualified-write-control (execution, C+C++ parity) shows the same writes compile and run when nothing is uninitialized. The different-name control (other.x) is the existing E0381 behavior. Same commit also covers two related arms in the scope-member pre-scan: global-write-scope-member (global.count no longer initializes member count) and local-write-scope-member (a bare write to a shadowing local no longer initializes the member).
  • The fixture fails if the spelling match is put back (mutation-checked).
    • Evidence (bd4f034d5): disabling the qualified-target return in enterAssignmentStatement fails global-write-local-shadow, this-write-local-shadow and global-write-scope-member ("Expected errors but transpilation succeeded"); dropping the pre-scan's global. skip fails global-write-scope-member; dropping its local-binding check fails local-write-scope-member. Restored: 5/5 pass.

Activity

  1. added 2 commits that reference this issue on Oct 11, 2026
  2. moved this from Grooming to Done in C-Nexton Oct 11, 2026
  3. added this to the v0.4.0 milestone on Oct 17, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    • Status
      Done

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions