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 skipped
It 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 = … and const g: any = … (the explicit any annotation 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.
Summary
A symbol-keyed property get returns a function.
typeofsays"function",Boolean()saystrue,=== undefinedsaysfalse— but the implicit truthiness test says falsy, only when the value has been stored in a local first.Three lines, same expression, one program:
Committed as
gc-handoff/m0810/m9.ts.The value is fine; only the implicit test is wrong
typeof ffunctionfunction✓f === undefinedfalsefalse✓f == nullfalsefalse✓Boolean(f)truetrue✓f ? … : …!ffalsetrue✗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:
It 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:
Symbol.iterator) and a user symbol (Symbol("mine")),const f = …andconst g: any = …(the explicitanyannotation does not help),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 correctin both engines.
Suspected shape
"Correct inline, wrong once stored in a local" is a known failure shape in this
codebase — see #7700 (
Uint8ArrayGetkey-kind), whose tell was recorded as "wrongonly 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_typeshandling for body-let/const(notedelsewhere as not covering body locals) and the inlined
js_is_truthyfast 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 probeto the 7 lines above.