Skip to content

A for body sees the variable its condition narrows - #514

Merged
ASDAlexander77 merged 2 commits into
mainfrom
fix-for-condition-narrowing
Oct 5, 2026
Merged

ASDAlexander77 merged 2 commits into
mainfrom
fix-for-condition-narrowing

Conversation

@ASDAlexander77

Copy link
Copy Markdown
Owner

A while body sees the variable its condition narrows; a for body did not. So for (; typeof v === "string"; v = 1) { v.length } failed with "Can't resolve property 'length' of type number", and so did a type-guard condition (isStr(v)). The same while loops compiled. This is the narrowing gap noted in #510.

Two commits. The first is a fix the second needs.

1. An object literal assigned to a narrowed variable is typed as declared

After s.kind === "sq" narrows s: Sq | Ci to Sq, s = { kind: "ci", r: 1 } was generated with the narrowed Sq as its receiver type. The literal became a { kind: "sq", r } and nothing was stored, with no error:

  • In an if, s kept "sq".
  • In a while, a write meant to end the loop never ended it, or crashed the compiler.

mlirGenSaveLogic now takes a narrowed variable's (SafeCastOp) receiver type from the value it narrows. The store already saw through the SafeCastOp.

This had to come first: for (; s.kind === "sq"; ) { s = { kind: "ci", r: 1 } } works on main because the for does not narrow. Narrowing alone would have turned it into an infinite loop.

Test: 00narrowed_assign_other_member.ts (if, while, same member, and the other member's field read afterwards). On main the first assertion fails.

2. A for body sees the variable its condition narrows

The body is generated in a scope of its own that narrows as a while body does (checkSafeCast). That scope ends before the incrementor, which assigns the variable as declared (node = node.next). Two differences from while:

  • The narrowing reads the condition before its boolean cast, as if does, so a type guard's predicate still narrows.
  • A condition known to be false narrows nothing, but the body is still generated, so a var in it is still declared.

Test: 00for_condition_narrowing.ts covers:

  • typeof, with the incrementor and with the body writing the variable
  • a type guard
  • instanceof
  • a discriminant, with the body writing the other member
  • an optional list walk, with continue and nested
  • a condition known to be false
  • a var in an unbraced body

On main it fails to compile.

Both tests are registered for compile, JIT and the rc/none corpus. They pass under gc, rc and none, with and without --opt. 00for_optional_class_condition.ts (#510) still passes. ctest -C Release: 3876/3876 passed (gtest unittests not built in this tree).

Found on the way, filed separately

🤖 Generated with Claude Code

ASDAlexander77 and others added 2 commits October 5, 2026 16:32
After `s.kind === "sq"` narrows `s: Sq | Ci` to `Sq`, `s = { kind: "ci",
r: 1 }` was generated with the narrowed `Sq` as its receiver type: the
literal became a `{ kind: "sq", r }` and nothing was stored, with no
error. An `if` kept "sq", and a `while` that wrote the variable to end
the loop never ended (or crashed the compiler).

mlirGenSaveLogic now takes the receiver type of a narrowed variable (a
SafeCastOp) from the value it narrows, so the literal is typed as the
variable is declared. The store already saw through the SafeCastOp.
A variable on the right, or a literal of the narrowed member, was
already right.

00narrowed_assign_other_member.ts: the other member assigned in an if
and in a while (which must end), the same member, and the other
member's field read after the if. On main the first assertion fails.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
mlirGen(WhileStatement) narrows the tested variable for its body
(checkSafeCast); mlirGen(ForStatement) did not, so
`for (; typeof v === "string"; v = 1) { v.length }` failed with "Can't
resolve property 'length' of type number", where the same `while`
compiled. A type guard (`isStr(v)`) failed the same way.

The body is now generated in a scope of its own that narrows as a while
body does. The scope ends before the incrementor, which assigns the
variable as it is declared (`node = node.next`). The narrowing reads
the condition before its boolean cast, as mlirGen(IfStatement) does, so
a type guard's predicate still narrows. A condition known to be false
narrows nothing, as in an if; the body is still generated, so a `var`
in it is declared.

Writing the narrowed variable in the body relies on the previous
commit: `for (; s.kind === "sq"; ) { s = { kind: "ci", r: 1 } }` works
on main because the for did not narrow, and would otherwise have never
ended.

00for_condition_narrowing.ts: typeof with the incrementor or the body
writing the variable, a type guard, instanceof, a discriminant with the
body writing the other member, an optional list walk (with continue,
and nested), a condition known to be false, and a var in an unbraced
body. On main it fails to compile.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ASDAlexander77
ASDAlexander77 merged commit c358ba2 into main Oct 5, 2026
2 checks passed
@ASDAlexander77
ASDAlexander77 deleted the fix-for-condition-narrowing branch October 5, 2026 16:31
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.

1 participant