From a4104a5971e4aebdc8a3e376102444a005e28eb6 Mon Sep 17 00:00:00 2001 From: Ralph Date: Tue, 23 Jun 2026 00:18:24 -0700 Subject: [PATCH] fix(runtime): #3986 own valueOf wins over default in member dispatch MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Object(x)` returns `x` unchanged, but `Object(x).valueOf()` (and any member `.valueOf()` on an `any`-typed receiver) ignored a user-defined own `valueOf` and returned the default `Object.prototype.valueOf` result (the receiver itself). test262 built-ins/Object/S9.9_A6 expected x's own `valueOf` to run. Two dispatch entry points needed the own-property lookup: - `js_native_call_method`'s `"valueOf"` arm now reads the own `valueOf` field and invokes it when callable before falling back to the default, mirroring the existing `toLocaleString` arm. - `js_date_value_of` (the `Expr::DateValueOf` codegen path that lowers a statically-`any` `.valueOf()` call) now routes an ordinary, non-Date pointer object through that shared dispatch instead of returning the receiver unchanged. Date cells, boxed primitives, and Temporal values keep their existing behavior. The explicit-base form `Object.prototype.valueOf.call(x)` still goes through `object_prototype_value_of_thunk`, which intentionally skips the own-property lookup (spec: returns ToObject(this)) — so there is no infinite recursion. test262 built-ins/Object subset: 170→171 pass, no regressions. Extended the node-suite valueof-tolocalestring parity fixture with own-`valueOf` coverage. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-runtime/src/date.rs | 22 ++++++++++++++++ .../src/object/native_call_method.rs | 17 ++++++++++++ .../object/valueof-tolocalestring.ts | 26 +++++++++++++++++++ 3 files changed, 65 insertions(+) diff --git a/crates/perry-runtime/src/date.rs b/crates/perry-runtime/src/date.rs index 46621f767e..b2f6843b7a 100644 --- a/crates/perry-runtime/src/date.rs +++ b/crates/perry-runtime/src/date.rs @@ -1074,6 +1074,28 @@ pub extern "C" fn js_date_value_of(timestamp: f64) -> f64 { if let Some((_, payload)) = crate::builtins::boxed_primitive_payload(timestamp) { return payload; } + // A real Date cell yields its numeric timestamp. + if is_date_value(timestamp) { + return date_cell_timestamp(timestamp); + } + // Any other ordinary object: `obj.valueOf()` follows ordinary member + // dispatch, so a user-defined own `valueOf` wins over the default + // Object.prototype.valueOf (which returns the receiver). `Object(x)` + // returns `x` unchanged, so `Object(x).valueOf()` must run x's own + // `valueOf` (test262 built-ins/Object/S9.9_A6). Route through the shared + // `valueOf` dispatch in `js_native_call_method`, which performs the own- + // property lookup and falls back to the default for plain objects. + if crate::value::JSValue::from_bits(timestamp.to_bits()).is_pointer() { + return unsafe { + crate::object::js_native_call_method( + timestamp, + b"valueOf".as_ptr() as *const i8, + "valueOf".len(), + std::ptr::null(), + 0, + ) + }; + } date_cell_timestamp(timestamp) } diff --git a/crates/perry-runtime/src/object/native_call_method.rs b/crates/perry-runtime/src/object/native_call_method.rs index e59b432e3c..be2807fd42 100644 --- a/crates/perry-runtime/src/object/native_call_method.rs +++ b/crates/perry-runtime/src/object/native_call_method.rs @@ -4681,6 +4681,23 @@ pub unsafe extern "C" fn js_native_call_method( // while ordinary objects now get the inherited default instead of // falling through to "valueOf is not a function". "valueOf" => { + // A user-defined own `valueOf` wins over the default, mirroring the + // `toLocaleString` arm below. `Object(x)` returns `x` unchanged, so + // `Object(x).valueOf()` must run x's own `valueOf` + // (test262 built-ins/Object/S9.9_A6). The explicit-base form + // `Object.prototype.valueOf.call(x)` goes through + // `object_prototype_value_of_thunk` instead and correctly skips this + // own-property lookup. + let own = + crate::object::js_object_get_own_field_or_undef(object, b"valueOf".as_ptr(), 7); + if let Some(result) = call_primitive_closure_value( + object, + JSValue::from_bits(own.to_bits()), + args_ptr, + args_len, + ) { + return result; + } return js_object_default_value_of(object); } diff --git a/test-parity/node-suite/object/valueof-tolocalestring.ts b/test-parity/node-suite/object/valueof-tolocalestring.ts index b33713654a..215fe705b3 100644 --- a/test-parity/node-suite/object/valueof-tolocalestring.ts +++ b/test-parity/node-suite/object/valueof-tolocalestring.ts @@ -38,3 +38,29 @@ logTypeError("call toLocaleString null-prototype", () => ); logCall("call valueOf null", () => Object.prototype.valueOf.call(null)); logCall("call toLocaleString undefined", () => Object.prototype.toLocaleString.call(undefined)); + +// #3986: a user-defined own `valueOf` wins over the default Object.prototype +// .valueOf during ordinary member dispatch. `Object(x)` returns `x` unchanged, +// so `Object(x).valueOf()` must run x's own `valueOf` (test262 S9.9_A6). The +// explicit-base form `Object.prototype.valueOf.call(x)` must NOT consult the +// own property (it returns the receiver) — guarding against infinite recursion. +function MyValue(this: any, v: number) { + this.value = v; + this.valueOf = function () { + return this.value; + }; +} +const mv = new (MyValue as any)(1); +const boxed: any = Object(mv); +logCall("Object(x) identity", () => boxed === mv); +logCall("Object(x) own valueOf", () => boxed.valueOf()); +logCall("Object(x) coercion", () => boxed + 4); +logCall("own valueOf via member", () => mv.valueOf()); +logCall("own valueOf base-call identity", () => Object.prototype.valueOf.call(boxed) === mv); +const litValueOf = { + valueOf() { + return 42; + }, +}; +logCall("object literal own valueOf", () => litValueOf.valueOf()); +logCall("object literal valueOf coercion", () => (litValueOf as any) * 2);