Skip to content

fix(compile): object-slot number locals that may hold undefined (#367)#371

Merged
nickna merged 1 commit into
mainfrom
fix-issue-367
Jun 12, 2026
Merged

fix(compile): object-slot number locals that may hold undefined (#367)#371
nickna merged 1 commit into
mainfrom
fix-issue-367

Conversation

@nickna

@nickna nickna commented Jun 12, 2026

Copy link
Copy Markdown
Owner

Summary

Closes #367 — the residual of #344 where a number/boolean-typed local unsoundly assigned an any/undefined value, then returned, compiled to NaN/false instead of undefined.

#344 fixed the case where the return value's own static type is any/unknown. This one is invisible to that detection: return x where x: number is statically number. Two distinct slots corrupt the sentinel:

  1. The local's unboxed double slot coerces undefinedNaN at the store (let x: number = 0; x = undefined as any).
  2. The return slot coerces it again (already handled by the compiled: typed number/boolean return slots silently coerce undefined to NaN/false #344 ReturnSlotAnalysis seam).

Approach

After a function/arrow/method/accessor body with a number/boolean return is type-checked (so every sub-expression type is in the TypeMap), run an order-independent taint pass (ReturnLocalTaintCollector + a fixpoint in MarkUndefinedReachableLocalReturns):

  • Compute the set of locals that may hold the sentinel — seeded by direct any/undefined assignments, grown transitively through other tainted locals (let z: number = y).
  • Flag returns of a tainted local → the compiler widens the return slot to object.
  • Flag the tainted locals' declarationsCanUseUnboxedLocal gives them an object slot instead of an unboxed double.

Order-independent, so a taint reaching an earlier return via a loop back-edge is still caught. Over-approximate (no narrowing): at worst a needless object slot, never a wrong value. Clean number locals keep their sound unboxed double slot (verified by a guard test). The reused ValueMayBeUndefinedSentinel predicate already recurses through grouping/ternary/||/&& pass-throughs; this PR also adds the missing ?? (NullishCoalescing) case.

Tests

5 new cases in ILVerificationTests (each asserts compiled IL verifies, runs correctly, and matches the interpreter): local-with-any-init, transitive taint, loop back-edge, method+getter, and a sound-body guard. Full suite: 11137 passed, 0 failed.

Out of scope (filed as follow-up)

The same unboxed-double-slot root cause also corrupts: inferred-return functions, non-return observations (console.log/arithmetic in void functions), and reassigned number parameters. These are outside #367's return scope — filed separately.

A `: number` local unsoundly assigned an `any`/`undefined` value (e.g.
`let x: number = undefined as any`) can hold the runtime `undefined`
sentinel, yet `return x` is statically `number` — so the static #344
detection (which keys on the return value's own type being any/unknown)
never fires. Worse, the compiler gives such a local an unboxed `double`
slot, which coerces the sentinel to NaN at the *store*, before the return
is even reached.

After a function/arrow/method/accessor body with a number/boolean return
is type-checked, run an order-independent taint pass: compute (to a
fixpoint, transitively through other tainted locals) the set of locals
that may hold the sentinel, then
  - flag returns of a tainted local so the return slot widens to object
    (the existing #344 ReturnSlotAnalysis seam), and
  - flag the tainted locals' declarations so CanUseUnboxedLocal gives
    them an object slot instead of an unboxed double.
Order-independent, so a taint reaching an earlier return via a loop
back-edge is still caught. Over-approximate (no narrowing): at worst a
needless object slot, never a wrong value. Clean number locals keep their
sound unboxed slot.

Residuals (filed separately): inferred-return functions, void/non-return
observations (console.log, arithmetic), and reassigned number parameters
share the same double-slot root cause but are outside #367's return scope.
@nickna nickna merged commit 55d5dd5 into main Jun 12, 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.

compiled: number/boolean-typed local holding undefined still coerces to NaN/false at a typed return (#344 residual)

1 participant