Skip to content

codegen: user class method named charAt/charCodeAt/codePointAt mis-lowered to the String builtin ([object Object] receiver) #6065

Description

@proggeramlug

Symptom

A user class method whose name shadows a String.prototype char-access
method — charAt, charCodeAt, or codePointAt — is mis-lowered to the
String builtin instead of the user's method. The receiver (a class instance) is
coerced to "[object Object]", so:

class Buf {
  buffer = "hello"; pos = 0;
  charAt(n: number) { return this.buffer[this.pos + n]; }
}
const b = new Buf();
console.log(b.charAt(0) + b.charAt(1));   // node: "he"   perry: "[o"

"[o" is "[object Object]"[0] + "[object Object]"[1]String.prototype.charAt
run on the stringified receiver.

Cause

try_lower_property_get_method_call (crates/perry-codegen/src/lower_call/property_get.rs)
takes the static String-method fast path for a builtin-named method on a
non-provably-string receiver, gated by an arity heuristic
(string_only_method_arity_ok, added in #5271 so a user trim(value, schema)
with a mismatched arg count falls through to runtime dispatch). But the
char-access methods ignore surplus args per spec, so that gate returns true
for any arg count — the escape hatch never fires for them, and a user
charAt(n) on a class instance is forced onto lower_string_method.

slice / indexOf are unaffected (they are excluded from the string-only set
because they also exist on arrays); only charAt / charCodeAt / codePointAt
hit this.

Impact

The yaml package's tokenizer is built on Lexer.charAt(n) { return this.buffer[this.pos + n]; }
and calls this.charAt(0) as its scanner. Mis-dispatched, it reads garbage, so
the lexer's *lex state machine (while (next && this.hasChars(1)) next = yield* this.parseNext(next))
never advances this.pos and spins forever at 100% CPU — hanging yaml.parse()
of any non-trivial input, and any large esbuild-bundled CLI app that parses
YAML during module init.

Fix

Don't take the static String path when the receiver's statically-known class
(or an ancestor) defines its own method of that name — a user method wins over
a String builtin. A genuine string receiver is unaffected (it has no known
class), and the runtime jsval.is_string() arm still services Any-typed
strings.

PR incoming with an e2e regression suite (user charAt/charCodeAt/codePointAt
methods in plain + generator + class contexts, plus a real-string control).

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