Skip to content

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

Closed

Description

@jlaustill

Bug

E0381's analyzer (InitializationAnalyzer) keeps its own scope stack and looks each name up on it by spelling. enterFunctionDeclaration declares the parameters on the stack, and enterVariableDeclaration declares the locals. Nothing declares a for-header variable (forVarDecl). So in for (u32 count <- 0; …), every read and write of count binds whatever outer count the stack holds.

Found while reviewing PR #1993.

Reproduction (main @ 0cd96994b; the same on PR #1993's head bd4f034d5)

scope S {
    u32 count;

    public u32 f() {
        u32 sum <- 0;
        for (u32 count <- 0; count < 3; count +<- 1) {
            sum +<- count;
        }
        return sum;
    }
}
6:29 error[E0381]: use of uninitialized variable 'count'
7:20 error[E0381]: use of uninitialized variable 'count'

The transpiler exits 1, but the for variable is initialized by its header. The reads bind the scope member, which is never assigned.

Control: rename the loop variable to i, and the transpiler exits 0.

Write arm: on PR #1993 the result depends on declaration order

PR #1993 changes the scope-member pre-scan (#1019) to bind a bare write through 1.4 (IProgram.lexicalDeclarationAt), which does see the for variable. The listener still binds through its own stack, which does not.

u32 flag <- 0;

scope S {
    u32 count;

    public void set() {
        for (u32 count <- 0; flag < 3; flag +<- 1) {
            count <- 5;
        }
    }

    public u32 get() {
        return count;
    }
}
get() after set() (as shown) get() before set()
main exit 0 exit 0
PR #1993 exit 0 E0381 at return count

The member is never written, so E0381 is the correct result in both orders. On the PR, when get() comes after set(), the listener has recorded count <- 5 against the member, and that record persists into get(). On main, the pre-scan matched by spelling as well, so both orders are wrong.

Cause

There are two binding decisions in one analyzer. CLAUDE.md says: "Do not record declarations in an analyzer's own enterVariableDeclaration / enterParameter / enterForVarDecl: that is a second binding decision." 1.4's lexical frames already hold every local, parameter and for variable with its span. IProgram.bindValue(sourceFile, root, name, at) is "the one place a spelling becomes a declaration", and it already takes global. / this. as its root.

Definition of done

  • InitializationAnalyzer decides what a name binds through IProgram.bindValue, with the target's own global. / this. / bare root, and not through its own stack or a spelling test. It no longer declares parameters or locals itself, and initialization state is keyed by the declaration that 1.4 returns.
  • Fixtures: the for-variable read compiles, and the write arm reports E0381 in both declaration orders. Each fixture fails if the stack binding is put back (mutation-checked).

Activity

  1. added
    bugSomething isn't working
    scopeScope and namespace features
    validation-bugMissing compile-time validation
    on Oct 11, 2026
  2. added a commit that references this issue on Oct 11, 2026
  3. devin-ai-integration commented on Oct 11, 2026

    @devin-ai-integration
    Contributor

    Both boxes are met on PR #1993 at 40c60db96:

    • Box 1: InitializationAnalyzer.bindingKey calls IProgram.bindValue(sourceFile, root, name, at) for every read, write, argument and pre-scan target. Writes pass ChainRoot.ofTarget(target) as root.
      • State is keyed by the ILocalDeclaration that 1.4 returns, or by the C name of a file-scope variable or scope member.
      • ScopeStack is deleted, and nothing declares parameters any more. A parameter or for variable has no state, so it reads as initialized.
      • A local's state is created under the declaration 1.4 framed for it, and an invariant asserts that 1.4 framed it.
    • Box 2: the fixtures are in tests/bugs/issue-2004-initialization-binds-through-1-4/: the for-variable read executes, and the write arm reports E0381 in both orders.
      • With main's stack-based analyzer put back, all three fail.
      • With bd4f034d's version put back, the read and reader-after fail. Reader-before passes there, because that pre-scan already bound through 1.4 (see the table above). It fails when the pre-scan binds the bare target as the member.
  4. added a commit that references this issue on Oct 11, 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

    bugSomething isn't workingscopeScope and namespace featuresvalidation-bugMissing compile-time validation

    Projects

    • Status
      Done

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions