Repository navigation
Symbol-keyed function value is falsy in if/! but true under Boolean() — only when stored in a local #7796
Description
Summary
A symbol-keyed property get returns a function. typeof says "function", Boolean() says true, === undefined says false — but the implicit truthiness test says falsy, only when the value has been stored in a local first.
Three lines, same expression, one program:
const a = [1];
const sym = Symbol.iterator;
console.log("inline :", (a as any)[sym] ? "truthy" : "FALSY");
const f = (a as any)[sym];
console.log("stored :", f ? "truthy" : "FALSY");
const g: any = (a as any)[sym];
console.log("stored any:", g ? "truthy" : "FALSY");node --experimental-strip-types perry 0.5.1455 (0a2bf15bd)
inline : truthy inline : truthy ✓
stored : truthy stored : FALSY ✗
stored any: truthy stored any: FALSY ✗
Committed as gc-handoff/m0810/m9.ts.
The value is fine; only the implicit test is wrong
const f = (a as any)[Symbol.iterator];| expression | node | perry |
|---|---|---|
typeof f |
function |
function ✓ |
f === undefined |
false |
false ✓ |
f == null |
false |
false ✓ |
Boolean(f) |
true |
true ✓ |
f ? … : … |
truthy | falsy ✗ |
!f |
false |
true ✗ |
Boolean(f) — which goes through the runtime conversion — is correct. The inlined truthiness test used by ?: and ! is wrong. So the NaN-boxed value itself is right and it is the codegen'd truthiness path that misreads it.
Why this is worse than it looks
This is exactly the shape of the standard feature-detection idiom:
const it = obj[Symbol.iterator];
if (it) { /* iterate */ } // silently skippedIt fails silently and takes the wrong branch — no throw, no diagnostic. Any library doing capability detection on a symbol-keyed slot (iterables, Symbol.asyncIterator, Symbol.toPrimitive, Symbol.hasInstance, custom protocol symbols) gets the wrong answer whenever it binds the lookup to a variable first, which is the normal way to write it.
Scope
Reproduces with:
- a well-known symbol (
Symbol.iterator) and a user symbol (Symbol("mine")), const f = …andconst g: any = …(the explicitanyannotation does not help),- both an array receiver and a plain-object receiver.
Does not reproduce when the get is used inline in the condition, nor for
string-keyed gets (a["push"], a.push) either inline or stored — those are correct
in both engines.
Suspected shape
"Correct inline, wrong once stored in a local" is a known failure shape in this
codebase — see #7700 (Uint8ArrayGet key-kind), whose tell was recorded as "wrong
only when STORED in a local". The likely mechanism is the same: the value's kind/type
proof is attached to the expression, and binding it to a local drops to a declared or
defaulted type whose inlined truthiness test does not handle the symbol-keyed result
correctly. Worth checking binding_types handling for body-let/const (noted
elsewhere as not covering body locals) and the inlined js_is_truthy fast path.
Found by
Surfaced as a side finding during the for-of startup-cost investigation
(2026-08-10, perry 0.5.1455 @ 0a2bf15bd) and minimized from a 40-line timing probe
to the 7 lines above.
Activity
fix(codegen): stop typing a symbol-keyed element read as a number (#7796