runtime: make the Array-subclass elements store the default representation (−11.4% / −11.9% on the wolf-ecs twins) - #8974
Conversation
…entation `class X extends Array` instances keep their indexed elements and `length` in `ObjectMeta.elements` (PerryTS#8966) instead of shape-carried properties, so `push`/`pop`/`obj[i]` are element operations rather than property-shape transitions. `PERRY_ARRAY_SUBCLASS_ELEMENTS=0` restores the previous representation as a bisecting kill switch. Evidence for the flip: * the whole `test-files/` corpus compiled once and run twice (the gate is a pure runtime switch): 1285 binaries, 9 output differences, every one of them nondeterministic output — random bytes, timestamps, `console.time` values, a PID in a deprecation warning, a flaky watcher fixture that fails with the store OFF — each reproducible with the switch untouched; * the Array-subclass integration suites pass with the store enabled (indexing, super-init, native member base, loop-versioned array-like, closure-capture packed loops, object array-like dispatch, interface dispatch, field-push write-back); * wolf-ecs twins on the quiet Mac, 11 alternating pairs, same binary: add/remove −11.4%, entity cycle −11.9%, 11/11 in both the 2 s and 50 ms windows. Semantics move toward node: `JSON.stringify` produces the array form, `Object.keys` no longer leaks `length`, and `sort`/`reverse`/`splice`/ `shift`/`unshift`, `length` truncation, holes and spread become node-identical (they printed the object form `{"0":…,"length":…}` before). The shape-carried form stays reachable through the kill switch, so its unit tests now pin it explicitly with `ArraySubclassRepresentationGuard`; the elements tests pin the other direction. Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: defaults Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (1)
📝 WalkthroughWalkthroughThe runtime now uses the elements store for Array subclasses by default. An environment variable can restore the shape-carried representation. Tests use scoped guards to select either representation. ChangesArray subclass representation
Estimated code review effort: 2 (Simple) | ~15 minutes Merge Risk: ⚪ Minimal · up to Array subclasses now use the more efficient elements representation by default, with the prior representation available through a startup rollback switch. The supplied tests and checks support merge readiness, and no actionable merge-blocking risk remains. 🚥 Pre-merge checks | ✅ 5✅ Passed checks (5 passed)
Full details: Description checkExplanation The description provides a clear summary, concrete behavior changes, related issue context, extensive test evidence, performance results, and validation gates. It does not use every template heading and omits the checklist, but it contains the required substantive information and is mostly complete. Full details: Docstring CoverageExplanation Docstring coverage is 90.48% which is sufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 21 functions across 3 files. (1 skipped: 1 unsupported.) ✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
Merged. This closes the finding I left on #8966, and I verified it directly rather than inferring it from the diff. When I audited #8966 I reported that with
That is what the kill-policy actually asks for — both sides of the knob exercised, so the mode is a decision that has been made rather than one nobody has run end to end. The kill switch being green matters as much as the new default: it is only a usable bisection tool if the old path still passes. The whole-corpus differential is the right kind of evidence too — compiling each fixture once and running it twice under a pure runtime switch removes compilation as a variable, and attributing all nine differences to crypto randomness, timestamps and console formatting is the sort of accounting that makes a null result believable. One fix pushed: the fragment was named with the Validation — the three states above; under |
…ops as their own kind The loop guard resolved an elements-backed receiver to its store and published it as kind 1. Kind 1 means "the receiver IS the ArrayHeader", and the generated loop computes `receiver + header + i*8` itself — on the ordinary (non-capture) path `fast_raw` is the receiver's own address — so element 0 read the object's `meta` word as a double (`1.8e-311`) while later elements happened to land on real data. `issue_8773_closure_capture_packed_loops`'s dense case caught it once the store became the default representation (PerryTS#8974). Elements-backed receivers are now admitted as **kind 3**: every proof is the store's (no descriptors, prototype latch clear, `bound <= length <= capacity`, and the whole-array raw-f64 bit for a numeric mode), the live address stays the RECEIVER so the capture-safe caller keeps reloading the binding it owns, and the payload address is published in descriptor word 3. Codegen's two plain-payload sites take their base from that word (`plain_payload_base`), so both the capture path and the ordinary path read the payload. Revalidation re-resolves the store from the receiver, refreshes word 3 after an evacuation, and side-exits when an append re-allocates it — exactly as a grown plain Array does. Mode 2 (the fused ECS entity-id clone) declines, as it does for plain Arrays. Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
…nt 0 as the object's meta word (#8976) * runtime,codegen: admit elements-backed Array subclasses to counted loops as their own kind The loop guard resolved an elements-backed receiver to its store and published it as kind 1. Kind 1 means "the receiver IS the ArrayHeader", and the generated loop computes `receiver + header + i*8` itself — on the ordinary (non-capture) path `fast_raw` is the receiver's own address — so element 0 read the object's `meta` word as a double (`1.8e-311`) while later elements happened to land on real data. `issue_8773_closure_capture_packed_loops`'s dense case caught it once the store became the default representation (#8974). Elements-backed receivers are now admitted as **kind 3**: every proof is the store's (no descriptors, prototype latch clear, `bound <= length <= capacity`, and the whole-array raw-f64 bit for a numeric mode), the live address stays the RECEIVER so the capture-safe caller keeps reloading the binding it owns, and the payload address is published in descriptor word 3. Codegen's two plain-payload sites take their base from that word (`plain_payload_base`), so both the capture path and the ordinary path read the payload. Revalidation re-resolves the store from the receiver, refreshes word 3 after an evacuation, and side-exits when an append re-allocates it — exactly as a grown plain Array does. Mode 2 (the fused ECS entity-id clone) declines, as it does for plain Arrays. Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ * chore(changelog): name the fragment for its PR --------- Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Follow-up to #8966, which landed the elements store behind
PERRY_ARRAY_SUBCLASS_ELEMENTS=1. This flips the default;PERRY_ARRAY_SUBCLASS_ELEMENTS=0restores the shape-carried form as a bisecting kill switch.Evidence
Whole-corpus differential. The gate is a pure runtime switch (codegen always emits the probes), so every fixture can be compiled once and run twice. Across
test-files/: 1386 files, 1285 compiled, 9 output differences — none attributable to the store. Those nine aretest_crypto(random bytes/UUID),test_date(timestamps),test_gap_console_methods(console.timevalues),test_parity_sys(PID in a deprecation warning),test_parcel_watcher_facade(flaky watcher timeout — it fails with the store off and passes with it on),test_perry_tui_inkcompat_useref(times out either way), plustest_issue63_escape,test_math,test_require, each of which produces a different output hash on five consecutive runs with the switch untouched.Integration suites with the store enabled:
issue_8655_array_subclass_indexing2/2,issue_array_subclass_super_init1/1,issue_4908_subclass_native_member_base2/2,issue_8690_loop_versioned_arraylike3/3,issue_8773_closure_capture_packed_loops4/4,issue_5139_object_arraylike_method_dispatch3/3,interface_typed_arraylike_method_dispatch3/3,issue_8897_field_push_writeback3/3.Performance (Mac mini, 11 alternating pairs, same binary, store off → on):
Perry vs Node 26.5 on that host goes from 3.02× / 2.24× to 2.71× / 2.01×.
Semantics
Strictly closer to node. With the store on,
JSON.stringify(sub)produces the array form,Object.keysno longer leakslength, andsort/reverse/splice/shift/unshift/lengthtruncation / holes / spread /Array.frombecome node-identical — every one of those printed the object form ({"0":5,"1":1,"length":2}) before.Unchanged divergences (identical with the store on or off, pre-existing, tracked in #8953):
fillingetOwnPropertyNames/for..in,A.from([…])not populating,f.constructor === Aaftermap,entries()/keys()unimplemented on a subclass instance.Tests
The shape-carried form stays reachable through the kill switch, so its unit tests now name it explicitly (
ArraySubclassRepresentationGuard::shape_carried(), 13 tests) and the elements tests pin the other direction.cargo test -p perry-runtime --lib2779/0.Gates
RUSTFLAGS=-D warnings cargo check --workspace --all-targetsclean; file size; GC store-site inventory; raw-handle debt unchanged (967, none raised); shape census; local-binding audit; addr-class audit. (temp_root_operand_temporaries::string_literal_concat_operand_is_re_derived_below_the_allocating_siblingfails on plainorigin/maintoo — unrelated to this change.)https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
Summary by CodeRabbit
lengthusing the standard elements representation by default.push,pop, indexing, serialization, and key enumeration now more closely match Node.js behavior.lengthfrom appearing unexpectedly in object keys.