Skip to content

Fix #653/#654: variable reassignment flow narrowing + widen all enclosing guards on escape#744

Merged
nickna merged 1 commit into
mainfrom
wrk/crazy-elgamal-7bef69
Jun 16, 2026
Merged

Fix #653/#654: variable reassignment flow narrowing + widen all enclosing guards on escape#744
nickna merged 1 commit into
mainfrom
wrk/crazy-elgamal-7bef69

Conversation

@nickna

@nickna nickna commented Jun 16, 2026

Copy link
Copy Markdown
Owner

Fixes #653 and #654 — two related gaps in the type checker's control-flow narrowing of variables across assignments.

#653 — reassignment now narrows to the RHS type (was over-strict)

SharpTS narrowed a property slot to the RHS type after a write (o.x = "s"o.x.length ok, #48) but did not do the equivalent for a variable, so it rejected code tsc accepts:

function f(x: string | null): number {
  x = "hi";
  return x.length;   // tsc: ok (x is string). Was: SharpTS error "possibly null".
}

CheckAssign previously restored the declared type after every variable write. It now restores the post-write flow-narrowed type — the declared union filtered to the members the RHS can be (NarrowToDeclaredSlot, the same primitive #48 uses for properties). The narrowed binding lives in the current lexical scope and is discarded at its block's join, so it can't leak a too-narrow type past a conditional.

Two guards keep it sound and non-regressive:

#654 — escaping reassignment now widens every enclosing guard (was a soundness hole)

function f(x: string | undefined): void {
  if (x !== undefined) {        // outer: x -> string
    if (x !== "foo") { x = undefined; }
    x.length;   // tsc: error (x is string | undefined). Was: SharpTS accepted.
  }
}

if-guard variable narrowing is applied by redefining the variable in a child TypeEnvironment. WidenEnclosingNarrowing (the #570 fix) widened only the nearest enclosing rebinding on an escaping reassignment, so with two guards on the same variable the outer guard's narrowing stayed stale. It now walks outward and widens every enclosing scope that narrows the variable, stopping at the declaration (binding == declared type) so it never crosses into an outer same-named shadowing variable.

Tests

Verification

  • Full unit suite: 12993 passed, 0 failed.
  • TypeScript conformance subset (assignmentCompatibility, conditional): 31 passed, 0 failed — no baseline diff.
  • Test262 not run: change is type-checker-only (no interpreter/compiler edits), and Test262 runs .js with type-checking off, so its baseline can't move.
  • Verified the now-accepted Variable reassignment doesn't narrow to the RHS type (over-strict; tsc accepts) #653 cases run correctly in both interpreted and compiled modes.

Follow-ups filed

…ing guards on escape

#653: after `x = v`, a variable read as its full declared type even when the RHS
put it safely inside that type, so SharpTS rejected code tsc accepts (`x = "s"` on a
`string | null` `x`, then `x.length`). CheckAssign now restores a tracked variable to
the post-write flow-narrowed type (declared union filtered to the RHS-compatible
members, via NarrowToDeclaredSlot) instead of unconditionally to the declared type —
mirroring the property-write narrowing of #48. The narrowed binding lives in the
current lexical scope and is discarded at its block's join, so it can't leak past a
conditional. Two guards keep it sound: only function-local/parameter variables narrow
(module vars aren't tracked in the declared-type stack — see #743), and a purely-nullish
slot is not installed so a later access still raises "possibly null/undefined" (the
bare-nullish access gap is #742).

#654: WidenEnclosingNarrowing now widens EVERY enclosing scope that holds a narrowing
of the reassigned variable, not just the nearest. When two guards narrowed the same
variable and an escaping reassignment sat under the inner one, only the inner guard was
widened, so a read at the outer level after the inner block kept the stale outer
narrowing (a soundness hole — SharpTS accepted code tsc rejects). The walk stops at the
variable's declaration (binding == declared type) so it never crosses into an outer,
same-named shadowing variable whose own narrowing is still valid.

Tests: new VariableAssignmentFlowNarrowingTests (#653, companion to the #48 property
tests) and three #654 cases in NestedReassignmentNarrowingTests (verbatim repro,
three-level nesting, and a shadowing non-regression guard).
@nickna nickna merged commit bb46c4d into main Jun 16, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Variable reassignment doesn't narrow to the RHS type (over-strict; tsc accepts)

1 participant