Skip to content

fix: built-in prototype methods resolve as property values (#2058)#2077

Merged
proggeramlug merged 1 commit into
mainfrom
worktree-fix-2058-proto-method-values
May 28, 2026
Merged

fix: built-in prototype methods resolve as property values (#2058)#2077
proggeramlug merged 1 commit into
mainfrom
worktree-fix-2058-proto-method-values

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #2058.

Problem

Built-in prototype methods (Object.prototype.isPrototypeOf,
Object.prototype.hasOwnProperty, Number.prototype.isPrototypeOf,
Function.prototype.{call,apply,bind}, …) returned undefined when accessed
as 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 number like 5 as the bare f64 5.0 (not a NaN-box), so its
bits (0x4014_0000_0000_0000) are neither a NaN-box tag nor a masked heap
pointer — they fell through every guard in js_object_get_field_by_name and
got dereferenced as an ObjectHeader.

typeof Object.prototype.isPrototypeOf   // was: undefined   now: function
typeof Number.prototype.isPrototypeOf   // was: undefined   now: function
typeof Function.prototype.call          // was: undefined   now: function
var n = 5;
typeof n.isPrototypeOf                   // was: SIGSEGV     now: function

Fix

Three small, scoped parts:

  1. HIR typeof fold (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 the Object/Function ctors and to the
    universally-inherited Object.prototype methods (new
    is_known_object_prototype_method helper).

  2. Runtime number-receiver guard (field_get_set.rs) — before the generic
    pointer 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_bind for the inherited Object.prototype methods, else
    undefined. Never dereferences — this fixes the SIGSEGV and makes the value
    typeof "function" and callable.

  3. Dispatch arms (native_call_method.rs) — adds isPrototypeOf (false for
    primitive receivers, scoped to non-pointer), primitive valueOf (returns the
    value) and toLocaleString (delegates to toString) so the bound values are
    actually callable rather than throwing.

Validation

  • New test-files/test_issue_2058_proto_method_values.ts is byte-identical to
    node --experimental-strip-types.
  • cargo test -p perry-runtime (700/700) and -p perry-hir suites stay green.
  • Regression sweep over the prototype/number/typeof/Date tests is clean.

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's
deliberately-approximate "truthy receiver" arms (ramda depends on them), and
n.toLocaleString() hits a Date-dispatch confusion. This PR is about the
value-read gap #2058 describes (the typeof reads + the SIGSEGV).

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 proggeramlug merged commit 9223bfa into main May 28, 2026
10 checks passed
@proggeramlug proggeramlug deleted the worktree-fix-2058-proto-method-values branch May 28, 2026 04:07
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

bug: Function.prototype.{call,apply,bind} are undefined as property values (only the f.call() method form works)

1 participant