Summary
Reading a method off a primitive string with a dynamically-keyed member
expression returns undefined. The same read with a literal key works, and
calling through the dynamic key works — so only the value read is wrong, which
is what makes it easy to miss and nasty in practice.
const k = "charAt";
const s = "abcde";
typeof s[k] // perry: "undefined" node: "function"
s[k](1) // perry: "b" node: "b" (call form is fine)
typeof s["charAt"] // (literal key — not affected, see below)
typeof String.prototype[k] // perry: "function" node: "function"
String.prototype[k].call(s, 1) // perry: "b" node: "b"
Measured on a compiled binary from 1d63fa91f (runtime-only branch), against
node on the same source. 46-case probe, one differing line — this one.
Why it matters more than the one-liner suggests
Every "take the method, then call it" shape breaks, and that is the ordinary way
bundled and generic code dispatches:
for (const name of ["trim", "toUpperCase"]) {
const fn = s[name]; // undefined in perry
fn.call(s); // TypeError: fn.call is not a function
}
if (typeof s[name] === "function") { /* never taken */ }
const m = s[name]?.bind(s); // silently undefined
I hit it writing a probe that enumerated String.prototype and called each
method as recv[k].apply(recv, args): all 79 methods threw TypeError in
perry where node returned a value, purely because recv[k] read as undefined.
That is the realistic blast radius — any reflective loop over a primitive's
methods.
Where it is not
String.prototype[k] (dynamic key, object receiver) is fine.
s[k](...) (dynamic key, call position) is fine — the call goes through
js_native_call_method, which resolves the name on the prototype.
- So the defect is specific to a property GET on a primitive receiver with a
non-literal key returning without consulting the primitive's prototype.
What I have not established
Whether the literal-key value read (typeof s["charAt"], no call) is affected,
whether number/boolean/bigint receivers behave the same, and whether a
user-assigned String.prototype.foo is affected as well. My probe covered the
dynamic-key case only; those three are the first things to add to a fixture.
Falsifier for whoever takes it
typeof s[k] must be "function" and s[k] === String.prototype[k] must be
true for a primitive s and a variable k — and the fix must not slow the
call form, which already resolves correctly through a different path. If a
change makes the read work but routes the call through the read, check the
primitive-method dispatch cost before landing: that path is the subject of
#9810.
Provenance
Found while measuring #9800 (built-in callee receives the primitive, not a
ToObject wrapper). It is not that change: the divergence is identical with
#9800's env gate on and off, so it predates it and is independent of it.
Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m
Summary
Reading a method off a primitive string with a dynamically-keyed member
expression returns
undefined. The same read with a literal key works, andcalling through the dynamic key works — so only the value read is wrong, which
is what makes it easy to miss and nasty in practice.
Measured on a compiled binary from
1d63fa91f(runtime-only branch), againstnodeon the same source. 46-case probe, one differing line — this one.Why it matters more than the one-liner suggests
Every "take the method, then call it" shape breaks, and that is the ordinary way
bundled and generic code dispatches:
I hit it writing a probe that enumerated
String.prototypeand called eachmethod as
recv[k].apply(recv, args): all 79 methods threwTypeErrorinperry where node returned a value, purely because
recv[k]read asundefined.That is the realistic blast radius — any reflective loop over a primitive's
methods.
Where it is not
String.prototype[k](dynamic key, object receiver) is fine.s[k](...)(dynamic key, call position) is fine — the call goes throughjs_native_call_method, which resolves the name on the prototype.non-literal key returning without consulting the primitive's prototype.
What I have not established
Whether the literal-key value read (
typeof s["charAt"], no call) is affected,whether number/boolean/bigint receivers behave the same, and whether a
user-assigned
String.prototype.foois affected as well. My probe covered thedynamic-key case only; those three are the first things to add to a fixture.
Falsifier for whoever takes it
typeof s[k]must be"function"ands[k] === String.prototype[k]must betruefor a primitivesand a variablek— and the fix must not slow thecall form, which already resolves correctly through a different path. If a
change makes the read work but routes the call through the read, check the
primitive-method dispatch cost before landing: that path is the subject of
#9810.
Provenance
Found while measuring #9800 (built-in callee receives the primitive, not a
ToObjectwrapper). It is not that change: the divergence is identical with#9800's env gate on and off, so it predates it and is independent of it.
Claude-Session: https://claude.ai/code/session_014UZWia6L37DpA93VLtNK9m