Skip to content

Symbol-keyed function value is falsy in if/! but true under Boolean() — only when stored in a local #7796

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions