Skip to content

runtime/object-model: fix Object(...) boxing and Object.assign ToObject/copy semantics #3986

Closed

Description

@andrewtdiz

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.json

Observed: 97 pass, 83 runtime-fail, 0 compile-fail, 53.9% sample parity.

Representative failures:

  • built-ins/Object/S9.9_A3.js: Object(true).valueOf() expected true, Perry returns an object-ish value.
  • built-ins/Object/S9.9_A4.js: Object(0).valueOf() expected 0, 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.assign should box a string target and return an object; Perry reports typeof as string.
  • built-ins/Object/assign/source-get-attr-error.js: getter abrupt completion should propagate; Perry reports no expected Test262Error.
  • 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

Acceptance criteria

  • The representative rows above pass under the Test262 differential.
  • built-ins/Object --max 180 improves materially without moving cases to skip.
  • A small local probe covering Object(true/0/"x").valueOf() and primitive-target Object.assign matches Node output.

Activity

  1. proggeramlug commented on Jun 2, 2026

    @proggeramlug
    Contributor

    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 objects Math/JSON/Reflect (→ [object Number]); Node returns [object Function] / [object Math] / etc.

    Attempted to add a [object Function] arm (via the canonical js_value_typeof) to js_object_to_string, but it was never reached: typeof fn correctly returns "function", yet Object.prototype.toString.call(fn) bypasses js_object_to_string entirely (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 reach js_object_to_string directly and typed-arrays/Symbol/BigInt reach it via the now-delegated object_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 through js_object_to_string (the now-canonical brand table), rather than a runtime-only arm. Deferred pending that codegen investigation.

  2. proggeramlug commented on Jun 2, 2026

    @proggeramlug
    Contributor

    Shipped a typeof fix in #4065 (v0.5.1098): typeof queueMicrotask / structuredClone / btoa / atob now return "function" (were "object"), matching Node.

    Root cause: a bare read of these identifiers resolves to GlobalGet(0) (globalThis) in HIR, so a value typeof folded to "object" — even though all four are fully callable. The HIR typeof-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 bypasses js_object_to_string; that needs the codegen-side fix, not a runtime arm.)

  3. andrewtdiz commented on Jun 2, 2026

    @andrewtdiz
    ContributorAuthor

    Claiming a focused wrapper/ToObject slice.

    Scope for this cut: primitive Object(value) / new Object(value) wrappers, wrapper valueOf/constructor/prototype identity, and the shared machinery needed for temporary primitive receiver boxing. I am intentionally leaving the broader Object.assign descriptor/proxy tail for a separate PR.

  4. andrewtdiz commented on Jun 2, 2026

    @andrewtdiz
    ContributorAuthor

    Opened draft PR for this cut: #4161

    Scope covered here:

    • primitive Object(value) / wrapper constructor identity for Number/String/Boolean-family wrappers
    • boxed String virtual indices, length, descriptors, keys, and own property names
    • temporary primitive receiver boxing for dynamic Number/String/Boolean prototype method calls

    The broader Object.assign descriptor/proxy tail remains outside this PR.

  5. andrewtdiz commented on Jun 3, 2026

    @andrewtdiz
    ContributorAuthor

    I am taking the remaining Object.assign descriptor/ToObject tail after the merged #4161 wrapper cut.

    Intended PR cut: focused runtime object-model behavior for Object.assign only: 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.

  6. andrewtdiz commented on Jun 3, 2026

    @andrewtdiz
    ContributorAuthor

    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_descriptor
    • cargo check -p perry-runtime -p perry-stdlib -p perry
    • npm 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 -- --check
    • git diff --check
    • ./scripts/check_file_size.sh
  7. andrewtdiz commented on Jun 3, 2026

    @andrewtdiz
    ContributorAuthor

    I am taking the remaining Object.prototype.toString brand 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 as Math, JSON, and Reflect report 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.

  8. andrewtdiz commented on Jun 3, 2026

    @andrewtdiz
    ContributorAuthor

    Opened draft PR #4269 for the Object.prototype.toString brand tail: #4269

    Scope covered:

    • Function/class values now brand as [object Function] instead of falling through to [object Object] / [object Number].
    • Math, JSON, and Reflect now carry intrinsic Symbol.toStringTag values and descriptor shape, so Object.prototype.toString.call(...) matches Node.
    • Added a focused globals parity fixture covering callable brands, namespace brands/descriptors, and explicit tag overrides.

    Validation:

    • Pre-fix latest main plus the new fixture failed at ordinary: [object Object] vs Node's [object Function].
    • Focused object-tostring-brands parity passes under Node v26.
    • Existing builtin-namespace and function-tostring globals parity filters pass.
    • cargo check -p perry-runtime, cargo fmt --all -- --check, git diff --check, and ./scripts/check_file_size.sh pass.
  9. andrewtdiz commented on Jun 4, 2026

    @andrewtdiz
    ContributorAuthor

    Current-main validation update: the checked-in Object-model fixtures are green on 2e0551cb9 (2026-06-04) with Node 26.

    Commands:

    CARGO_TARGET_DIR=/root/perry-worktrees/perry-node-probe-20260604/target PERRY_NO_AUTO_OPTIMIZE=1 npm exec --yes --package=node@26 -- bash -lc './run_parity_tests.sh --suite node-suite --module object'\nCARGO_TARGET_DIR=/root/perry-worktrees/perry-node-probe-20260604/target PERRY_NO_AUTO_OPTIMIZE=1 npm exec --yes --package=node@26 -- bash -lc './run_parity_tests.sh --suite node-suite --module globals --filter object'\n```\n\nResults:\n- `object`: 17 parity pass / 0 parity fail / 0 compile fail / 0 skipped; report `test-parity/reports/parity_report_20260604_232110.json`\n- `globals --filter object`: 2 parity pass / 0 parity fail / 0 compile fail / 0 skipped; report `test-parity/reports/parity_report_20260604_232146.json`\n\nI do not see a remaining checked-in Object/Object.assign/Object.prototype.toString fixture failure on current main, so I am treating this as stale-open unless a new reproduction appears.
  10. added a commit that references this issue on Jun 23, 2026
  11. proggeramlug commented on Jul 13, 2026

    @proggeramlug
    Contributor

    Closing — every semantic listed in this epic now matches Node byte-for-byte on main, including the Object.prototype.toString function/namespace brand tail that was the last deferred item (#4269).

    Verified with a 19-assertion probe covering Object(...) boxing (number/string/boolean), Object.assign ToObject + 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]). diff against node --experimental-strip-types is empty.

    Shipped across #4161, #4239, #4269, #4065.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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