Repository navigation
runtime/object-model: fix Object(...) boxing and Object.assign ToObject/copy semantics #3986
Description
Summary
The current c262 sample shows Object(...), new Object(...), and Object.assign(...) still diverging on core ToObject behavior. Perry often returns or reports primitive-ish values where Node exposes wrapper objects, loses constructor/prototype identity, and misses descriptor/getter/proxy behavior during assignment.
Evidence
Run from origin/main at 1f6d20e19 on 2026-06-01, Node v25.9.0, Test262 4249661388e5d3f92a85186213da140a6481490f:
scripts/test262_subset.py --root /tmp/perry-test262-4249661 --dir built-ins/Object --max 180 --timeout 10 --sample-cap 50 --report /tmp/perry-c262-builtins-object.jsonObserved: 97 pass, 83 runtime-fail, 0 compile-fail, 53.9% sample parity.
Representative failures:
built-ins/Object/S9.9_A3.js:Object(true).valueOf()expectedtrue, Perry returns an object-ish value.built-ins/Object/S9.9_A4.js:Object(0).valueOf()expected0, Perry returns an object-ish value.built-ins/Object/S9.9_A5.js:Object("some string").valueOf()expected the original string, Perry returns an object-ish value.built-ins/Object/assign/Target-String.js:Object.assignshould box a string target and return an object; Perry reportstypeofasstring.built-ins/Object/assign/source-get-attr-error.js: getter abrupt completion should propagate; Perry reports no expectedTest262Error.built-ins/Object/assign/source-own-prop-desc-missing.js: proxy/property descriptor path is not observed as Node does.
Product triage
User outcome: library code that uses object coercion, option merging, primitive wrappers, and descriptor-aware assignment behaves like Node instead of silently dropping values.
Shortest path: implement the shared ToObject/wrapper path and reuse it from Object(...), new Object(...), and Object.assign(...) before expanding into less-used Object static helpers.
Manual/narrow assumptions to test: boxed primitive probes for boolean/number/string/symbol plus a focused Object.assign fixture with primitive targets, non-enumerable source props, throwing getters, read-only target props, and proxies.
Core vs nice: core is ToObject, valueOf, constructor/prototype identity, enumerable own-property copy, and abrupt propagation. Nice is full Object static method descriptor polish already partially covered by #3655 and validation work in #3146/#3662.
Keep flexible: centralize ToObject so String wrappers, primitive receiver boxing (#3577), and future exotic wrappers can share it.
Suggested scope
- Make
Object(value)andnew Object(value)return the original object for object inputs and proper wrapper objects for primitive inputs. - Preserve wrapper
valueOf(),.constructor, prototype chain, and observabletypeof/object behavior. - Make
Object.assignperform spec ToObject on the target, enumerate only own enumerable source props, respect descriptors, and propagate getter/proxy/readonly abrupt completions. - Coordinate with runtime/object-model: support primitive receiver boxing and temporary property visibility #3577 for primitive receiver boxing and runtime: complete own name/length descriptors on builtins (follow-on to closed #3143) #3655 for builtin own
name/lengthdescriptors.
Acceptance criteria
- The representative rows above pass under the Test262 differential.
built-ins/Object --max 180improves materially without moving cases to skip.- A small local probe covering
Object(true/0/"x").valueOf()and primitive-targetObject.assignmatches Node output.
Activity
fix(runtime): Object.prototype.toString brands for Map/Set/WeakMap/We…
proggeramlug commentedon Jun 2, 2026
Triage note (v0.5.1097):
Object.prototype.toString.call(x)still returns the wrong brand for functions (ordinary/arrow →[object Object], class constructors →[object Number]) and the namespace objectsMath/JSON/Reflect(→[object Number]); Node returns[object Function]/[object Math]/ etc.Attempted to add a
[object Function]arm (via the canonicaljs_value_typeof) tojs_object_to_string, but it was never reached:typeof fncorrectly returns"function", yetObject.prototype.toString.call(fn)bypassesjs_object_to_stringentirely (verified — the brand stays[object Object]with the arm in place). This is a fragmented dispatch: in recent fixes (v0.5.1095-1097) Map/Set/etc reachjs_object_to_stringdirectly and typed-arrays/Symbol/BigInt reach it via the now-delegatedobject_prototype_to_string_thunk, but function and namespace receivers take a third codegen-determined path that produces[object Object]/[object Number]without calling either runtime brand function. (Class/Math/JSON/Reflect →[object Number]because a raw-i64 pointer's small bit pattern reads as a finite f64.)Fixing these needs locating that codegen path for
Object.prototype.toString.call(<function|namespace>)and routing it throughjs_object_to_string(the now-canonical brand table), rather than a runtime-only arm. Deferred pending that codegen investigation.fix(hir): typeof queueMicrotask/structuredClone/btoa/atob is "functio…
proggeramlug commentedon Jun 2, 2026
Shipped a typeof fix in #4065 (v0.5.1098):
typeof queueMicrotask/structuredClone/btoa/atobnow return"function"(were"object"), matching Node.Root cause: a bare read of these identifiers resolves to
GlobalGet(0)(globalThis) in HIR, so a valuetypeoffolded to"object"— even though all four are fully callable. The HIRtypeof-of-bare-global fold (lower/lower_expr.rs) constant-folds known global functions (setTimeout,fetch, …) to"function"; added these four (local-shadow-guarded). Call paths unchanged.(Separately noted on this epic:
Object.prototype.toString.call(<function|namespace>)brands —[object Function]/[object Math]/etc — still route through a codegen-determined path that bypassesjs_object_to_string; that needs the codegen-side fix, not a runtime arm.)andrewtdiz commentedon Jun 2, 2026
Claiming a focused wrapper/ToObject slice.
Scope for this cut: primitive
Object(value)/new Object(value)wrappers, wrappervalueOf/constructor/prototype identity, and the shared machinery needed for temporary primitive receiver boxing. I am intentionally leaving the broaderObject.assigndescriptor/proxy tail for a separate PR.andrewtdiz commentedon Jun 2, 2026
Opened draft PR for this cut: #4161
Scope covered here:
Object(value)/ wrapper constructor identity for Number/String/Boolean-family wrapperslength, descriptors, keys, and own property namesThe broader
Object.assigndescriptor/proxy tail remains outside this PR.andrewtdiz commentedon Jun 3, 2026
I am taking the remaining
Object.assigndescriptor/ToObject tail after the merged #4161 wrapper cut.Intended PR cut: focused runtime object-model behavior for
Object.assignonly: target ToObject for primitive targets, own enumerable source copy order, string-source virtual indices, getter abrupt completion propagation, and descriptor/read-only target behavior where the current object path already has local support. Non-goals: redoing primitive wrapper identity work from #4161, broad Proxy internals if the existing runtime has no proxy substrate, unrelated builtin name/length descriptor cleanup, and wider Object static method conformance.andrewtdiz commentedon Jun 3, 2026
Opened draft PR #4239 for the remaining Object.assign descriptor/symbol-enumerability tail: #4239
Validation:
cargo test -p perry-runtime symbol_define_property_attrs_round_trip_descriptorcargo check -p perry-runtime -p perry-stdlib -p perrynpm exec --yes --package=node@26 -- bash -lc 'node --version; ./run_parity_tests.sh --suite node-suite --module object --filter assign'(Node v26.3.0; 3 pass / 0 fail / 0 compile fail)cargo fmt --all -- --checkgit diff --check./scripts/check_file_size.shandrewtdiz commentedon Jun 3, 2026
I am taking the remaining
Object.prototype.toStringbrand tail noted on this issue.Planned PR cut: route
Object.prototype.toString.call(value)through the canonical brand path for callable function values and native namespace objects so ordinary functions, arrows, class constructors, and built-in namespaces such asMath,JSON, andReflectreport Node-shaped[object Function]/[object Math]/[object JSON]/[object Reflect]instead of falling through to generic object/number brands.Non-goals for this cut: redoing primitive wrapper/Object.assign work already covered by earlier PRs, Proxy internals, descriptor/name/length cleanup, and unrelated Reflect static-method semantics.
andrewtdiz commentedon Jun 3, 2026
Opened draft PR #4269 for the Object.prototype.toString brand tail: #4269
Scope covered:
[object Function]instead of falling through to[object Object]/[object Number].Math,JSON, andReflectnow carry intrinsicSymbol.toStringTagvalues and descriptor shape, soObject.prototype.toString.call(...)matches Node.Validation:
ordinary: [object Object]vs Node's[object Function].object-tostring-brandsparity passes under Node v26.builtin-namespaceandfunction-tostringglobals parity filters pass.cargo check -p perry-runtime,cargo fmt --all -- --check,git diff --check, and./scripts/check_file_size.shpass.andrewtdiz commentedon Jun 4, 2026
Current-main validation update: the checked-in Object-model fixtures are green on
2e0551cb9(2026-06-04) with Node 26.Commands:
fix(runtime,hir): object-model semantics — Object boxing, this-bindin…
fix(runtime,hir): object-model semantics — Object boxing, this-bindin…
fix(runtime,hir): object-model semantics — Object boxing, this-bindin…
fix(hir): queueMicrotask/structuredClone/atob/btoa as first-class fun…
fix(runtime): Object.getPrototypeOf returns installed [[Prototype]] b…
fix(runtime): #3986 own valueOf wins over default in member dispatch (#…
fix(runtime): bound has_property prototype recursion and extend BigIn…
Improve Zod compatibility support (#5874)
proggeramlug commentedon Jul 13, 2026
Closing — every semantic listed in this epic now matches Node byte-for-byte on main, including the
Object.prototype.toStringfunction/namespace brand tail that was the last deferred item (#4269).Verified with a 19-assertion probe covering
Object(...)boxing (number/string/boolean),Object.assignToObject + copy semantics (string sources, primitive targets, getter flattening, symbol keys, non-enumerable skipping, frozen-target throw, null/undefined sources, abrupt getter), and the brand tail ([object Function]for fn/arrow/class;[object Math]/[object JSON]/[object Reflect]).diffagainstnode --experimental-strip-typesis empty.Shipped across #4161, #4239, #4269, #4065.