Skip to content

fix: #809 — object literal spread+methods+computed-keys; Object.create wires prototype#815

Open
proggeramlug wants to merge 2 commits into
mainfrom
worktree-fix-809-objlit-spread
Open

fix: #809 — object literal spread+methods+computed-keys; Object.create wires prototype#815
proggeramlug wants to merge 2 commits into
mainfrom
worktree-fix-809-objlit-spread

Conversation

@proggeramlug
Copy link
Copy Markdown
Contributor

Closes #809. Refs #321, #711.

The Effect end-to-end repro (#321) advanced past ParseResult.ts and threw TypeError: value is not a function in HashRing.ts__init. The 20-line standalone repro from the issue (Proto = { [TypeId]:TypeId, [Symbol.iterator]() {}, pipe() {}, ...Inspectable.BaseProto, toJSON() {} } + Object.create(Proto).toJSON()) printed keys: 2 (node: 4 — the issue's "5" predates checking against node, which excludes Symbol.iterator from Object.keys), inst.toJSON()\"1970-01-01T00:00:00.000Z\", inst[TypeId]undefined.

Four independent bugs in the chain, all needed to pass

  1. Object-literal has_spread lowering dropped entries. crates/perry-hir/src/lower/expr_object.rs's spread path lowered to Expr::ObjectSpread { parts } whose parts only carries static-string KeyValue + spreads; Prop::Method fell into _ => {} and computed KeyValue keys into _ => continue — so [Symbol.iterator](), pipe(), the [TypeId] computed string key, and the trailing toJSON() were all silently discarded.
  2. Over-eager DateToJSON lowering. inst.toJSON() where inst's static class is unknown rewrote to DateToJSON(inst); the runtime read the pointer's NaN-box bits as a timestamp → epoch.
  3. js_object_create ignored its argument. Allocated a bare empty object, dropping proto entirely. Object.create(P).x saw nothing.
  4. Prototype-walk gaps exposed once (3) was fixed. The codegen PIC fast path for obj.prop spuriously "hit" when obj->keys_array == null (an Object.create result) because the zero-init per-site cache matched, returning slot[0] without invoking the miss handler. js_object_get_field_by_name early-returned undefined on null keys_array before its class_id prototype-walk. js_native_call_method's prototype-object walk was gated inside if let Some(ref reg) = *CLASS_VTABLE_REGISTRY — skipped entirely for programs with no user classes.

Fix

  1. Rewrote the has_spread path as a source-ordered IIFE ((__o) => { …ordered ops…; return __o })({}) so static props, computed keys, this-binding methods, and spreads interleave correctly (a later prop/spread overrides an earlier same key — the non-spread fast path can't be used because it applies all static props before any post-init, which would let a trailing ...src clobber the literal's own toJSON()). Spreads emit js_object_assign_one; static this-methods a new js_object_set_method_by_name runtime helper (string-key analog of js_object_set_symbol_method); computed this-methods reuse js_object_set_symbol_method; everything else IndexSet. Extracted resolve_keyvalue_key / lower_method_prop shared helpers used by both spread and non-spread paths (non-spread behavior identical).
  2. static_receiver_class now returns Some(\"Object\") for object-literal / Object.create(...) receivers and locals in a new LoweringContext.plain_object_locals set (populated in lower_var_decl_with_destructuring); the ambiguous-Date-method gate treats it like Some(\"URL\") and skips the Date arms.
  3. js_object_create now allocates a synthetic class_id, maps it to proto in CLASS_PROTOTYPE_OBJECTS (reusing the perry-runtime: Effect framework throws TypeError: pipe is not a function during Schema.ts module init (Effect end-to-end blocker, post-#685) #711 machinery), and stamps the object. Object.create(null) / non-object / builtin-backed sources fall back to the original prototype-less object.
  4. PIC hit predicate now requires keys_array != 0 so keyless receivers route to the slow path. js_object_get_field_by_name's null-keys_array arm consults the prototype chain via a new shared resolve_proto_chain_field helper (which also de-duplicates the existing inline perry-runtime: Effect framework throws TypeError: pipe is not a function during Schema.ts module init (Effect end-to-end blocker, post-#685) #711 walk). js_native_call_method runs an independent resolve_proto_chain_field resolution after the registry walk, with IMPLICIT_THIS set so methods see the right receiver.

Validation

  • Issue's exact repro byte-identical to node --experimental-strip-types: keys: 4, inst.toJSON: {\"_id\":\"HashRing\",\"x\":1}, inst[TypeId]: ~test/HashRing.
  • Gap suite 34/36 (2 = pre-existing console.dir/group + lookbehind regex categorical gaps, unchanged).
  • cargo test --release for perry-hir / perry-codegen / perry-runtime all green (0 failed).
  • 12-file curated smoke over spread/object/prototype test-files (incl. test_issue_711_function_prototype, test_spread, test_edge_rest_spread_defaults, test_get_prototype_of_instance) byte-identical to node.
  • Targeted regression confirms real new Date(0).toJSON()/toISOString() still work, spread override ordering preserved ({a:1,...{a:2}}{a:2}, {...{a:2},a:1}{a:1}), Object.create(null), empty-object missing-prop, Object.create(proto).method() with this-binding all match node.

Known remaining pre-existing gaps (not in #809's DoD, not regressions)

Both tracked separately.

…e wires prototype

Four independent bugs in the chain, all needed for the standalone repro to
match node (`Proto = { [TypeId]:TypeId, [Symbol.iterator]() {}, pipe() {},
...Inspectable.BaseProto, toJSON() {} }; Object.create(Proto).toJSON()`):

1. expr_object.rs `has_spread` path lowered to `Expr::ObjectSpread` whose
   `parts` only carries static-string KeyValue + spreads, silently dropping
   `Prop::Method` and computed `KeyValue` keys. Rewritten as a source-ordered
   IIFE so static props, computed keys, this-binding methods, and spreads
   interleave correctly (later same-key wins). Shared key/method-resolution
   helpers extracted (`resolve_keyvalue_key`, `lower_method_prop`) and used
   by both spread and non-spread paths — behavior-identical for non-spread.

2. `static_receiver_class` now returns `Some("Object")` for object-literal /
   `Object.create(...)` receivers and locals tracked in a new
   `plain_object_locals` set, so the ambiguous-Date-method gate skips
   `DateToJSON`/`DateToString`/`DateValueOf`/... for plain objects.
   Pre-fix `inst.toJSON()` where `inst = Object.create(Proto)` lowered to
   `DateToJSON(inst)`, reading the pointer bits as a timestamp → epoch.

3. `js_object_create` ignored its prototype argument and returned a bare
   empty object. Now allocates a synthetic class_id, maps it to `proto` in
   `CLASS_PROTOTYPE_OBJECTS` (reusing the #711 machinery), and stamps the
   object. `Object.create(null)` / non-object / builtin-backed sources
   preserve the original prototype-less behavior.

4. Prototype-walk gaps exposed by (3):
   - Codegen PIC fast path spuriously "hit" when `obj->keys_array == null`
     (zero-init cache matched), bypassing the miss handler. Added a
     non-null keys_array requirement to the hit predicate.
   - `js_object_get_field_by_name`'s `keys_array == null` arm now consults
     the prototype chain before returning undefined; the existing inline
     #711 walk also routes through the new shared `resolve_proto_chain_field`
     helper (DRY).
   - `js_native_call_method`'s prototype-object walk was gated inside
     `if let Some(ref reg) = *CLASS_VTABLE_REGISTRY` — skipped entirely for
     programs with no user classes. Added an independent
     `resolve_proto_chain_field` resolution after the registry walk, with
     `IMPLICIT_THIS` set so methods see the right receiver.

Plus a new runtime helper `js_object_set_method_by_name` (string-key analog
of `js_object_set_symbol_method`) used by the IIFE lowering for static
this-binding methods placed after a spread.

Validation: repro byte-identical to `node --experimental-strip-types`; gap
suite 34/36 (2 = pre-existing console.dir/group + lookbehind regex gaps);
`cargo test` for perry-hir / perry-codegen / perry-runtime all green;
12-file curated smoke over spread/object/prototype test-files matches node;
real `new Date(0).toJSON()`/`toISOString()` still work, `Object.create(null)`
still works, spread override ordering preserved.

Known remaining pre-existing gaps not in #809's DoD: symbol-keyed property
inheritance through an `Object.create` prototype chain still returns
undefined (symbol side-table is keyed by receiver pointer); and
`Object.getPrototypeOf(Object.create(p))` returns the class-ref shim, not
`p`. Tracked separately.

Closes #809. Refs #321, #711.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

1 participant