fix: built-in prototype methods resolve as property values (#2058)#2077
Merged
Conversation
Built-in prototype methods (`isPrototypeOf`, `hasOwnProperty`, `toString`,
`Function.prototype.{call,apply,bind}`, …) returned `undefined` when read as
property *values* — via the prototype object (`Object.prototype.isPrototypeOf`)
or off a primitive receiver (`n.isPrototypeOf` where `n` is a number). The
primitive-receiver case additionally SIGSEGV'd: a raw, unboxed `number` (Perry
stores `5` as the bare f64 `5.0`, not a NaN-box) fell through every pointer
guard in `js_object_get_field_by_name` and got dereferenced as an
`ObjectHeader`. Direct method calls worked; member-access-as-value did not.
Three parts:
- HIR `typeof` fold (`lower_expr.rs`): extend the #1777 `<Ctor>.prototype.<m>`
fold to `Object`/`Function` ctors and to the universal `Object.prototype`
methods inherited by every prototype (new `is_known_object_prototype_method`
helper). Fixes `typeof Object.prototype.isPrototypeOf`,
`typeof Number.prototype.hasOwnProperty`, `typeof Function.prototype.call`,
etc.
- Runtime number-receiver guard (`field_get_set.rs`): before the generic
pointer logic, detect a raw finite-f64 number receiver (top16 != 0, not a
NaN-box tag, not a masked pointer, not a registered Date) and return a
bound-method closure (`js_class_method_bind`) for the inherited
`Object.prototype` methods, else `undefined` — never dereference. Fixes the
SIGSEGV and makes `typeof n.toString === "function"` hold with a callable
value.
- Dispatch arms (`native_call_method.rs`): add `isPrototypeOf` (false for
primitive receivers), primitive `valueOf` (returns the value) and
`toLocaleString` (delegates to `toString`) so the bound values are callable.
Adds test-files/test_issue_2058_proto_method_values.ts (byte-identical to
`node --experimental-strip-types`). perry-runtime 700/700 and perry-hir suites
stay green.
proggeramlug
added a commit
that referenced
this pull request
May 28, 2026
… property values (#2142) (#2176) Extends #2058/#2077 to the remaining built-in prototypes (`Array`/`Date`/`RegExp`/`String`/`Promise`/`Map`/`Set`/`Error`/typed arrays/etc.). Before this change, the typeof-fold made `typeof Array.prototype.map === "function"` pass at AST level, but any INDIRECT read — storing into a local, an array literal, or otherwise hiding the value behind a variable — returned `undefined`. The issue's repro (`for ([n,v] of [[ ... , Array.prototype.map], ...])`) hit exactly that gap. Two coordinated changes: 1. HIR (`expr_member.rs`) — broaden the #2060 typed-array-only collapse exemption. The #973 builtin-ident reroute collapsed `<Ctor>.prototype` to `globalThis.prototype` (which is undefined, not the constructor's real prototype). Typed arrays already exempted themselves so the per-kind proto with the `length` / `byteLength` / `byteOffset` / `buffer` accessor descriptors stayed reachable; the same logic applies to every built-in proto, so the exemption now fires unconditionally when the outer member is `.prototype`. 2. Runtime (`global_this.rs`) — extend `populate_builtin_prototype_methods` to install named callable closures for every well-known method on `Array`/`Object`/`Function`/`String`/`Number`/`Boolean`/`Date`/ `RegExp`/`Promise`/`Map`/`Set`/`WeakMap`/`WeakSet`/`Error*`/typed arrays. Each closure is backed by the shared `noop_thunk` so `typeof === "function"`, `.name` reads the method name, and `.length` reads the spec arity — covering Test262's `verifyProperty` / `assert.throws` introspection paths. `Array.prototype.slice` and `Object.prototype.toString` keep their dedicated thunks (ramda's curry/variadic helpers depend on `Array.prototype.slice.call(args,…)` returning a real sliced array even via indirection, and ramda's `_isArguments.js` IIFE calls `Object.prototype.toString.call(arguments)` at module-init time). The hot-path calls — `arr.map(fn)` (codegen NativeMethodCall) and `Array.prototype.map.call(arr, fn)` (HIR rewrite via `try_builtin_prototype_method_apply_call`) — are unaffected. Adds `test-files/test_issue_2142_builtin_proto_method_values.ts`, byte-identical to `node --experimental-strip-types`. Per #2142, this is the single biggest remaining cascade in Test262's `built-ins/{Object,Array,TypedArray,Date,RegExp,Function}` categories after #2058/#2059/#2060/#2063 — the `(no output)` / `reading 'name'` / `reading 'constructor'` cascades all collapse on the same prototype- method-value gap. Co-authored-by: Ralph Küpper <ralph@skelpo.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #2058.
Problem
Built-in prototype methods (
Object.prototype.isPrototypeOf,Object.prototype.hasOwnProperty,Number.prototype.isPrototypeOf,Function.prototype.{call,apply,bind}, …) returnedundefinedwhen accessedas property values — via the prototype object or off a primitive receiver.
Direct method calls worked; member-access-as-value did not.
The primitive-receiver case was worse than the issue reported: it SIGSEGV'd.
Perry stores a
numberlike5as the bare f645.0(not a NaN-box), so itsbits (
0x4014_0000_0000_0000) are neither a NaN-box tag nor a masked heappointer — they fell through every guard in
js_object_get_field_by_nameandgot dereferenced as an
ObjectHeader.Fix
Three small, scoped parts:
HIR
typeoffold (lower_expr.rs) — extends the existing Function.prototype.call/.apply on builtin prototype methods returns undefined ([].slice.call(arguments)) — generalize #1722 #1777<Ctor>.prototype.<method>fold to theObject/Functionctors and to theuniversally-inherited
Object.prototypemethods (newis_known_object_prototype_methodhelper).Runtime number-receiver guard (
field_get_set.rs) — before the genericpointer logic, detects a raw finite-f64 number receiver (not a NaN-box, not
a masked pointer, not a registered Date) and returns a bound-method closure
via
js_class_method_bindfor the inheritedObject.prototypemethods, elseundefined. Never dereferences — this fixes the SIGSEGV and makes the valuetypeof "function"and callable.Dispatch arms (
native_call_method.rs) — addsisPrototypeOf(false forprimitive receivers, scoped to non-pointer), primitive
valueOf(returns thevalue) and
toLocaleString(delegates totoString) so the bound values areactually callable rather than throwing.
Validation
test-files/test_issue_2058_proto_method_values.tsis byte-identical tonode --experimental-strip-types.cargo test -p perry-runtime(700/700) and-p perry-hirsuites stay green.Out of scope (pre-existing, separate gaps)
Method call accuracy on primitives is unchanged and tracked separately:
n.hasOwnProperty("x")/n.propertyIsEnumerable("x")use the runtime'sdeliberately-approximate "truthy receiver" arms (ramda depends on them), and
n.toLocaleString()hits a Date-dispatch confusion. This PR is about thevalue-read gap #2058 describes (the
typeofreads + the SIGSEGV).