Skip to content

batch: land #8872, #8875, #8877 - #8878

Merged
proggeramlug merged 22 commits into
mainfrom
merge/batch-8872-8877
Aug 27, 2026
Merged

batch: land #8872, #8875, #8877#8878
proggeramlug merged 22 commits into
mainfrom
merge/batch-8872-8877

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 27, 2026

Copy link
Copy Markdown
Contributor

Batch landing of three reviewed PRs, validated once as a single merged tree. No fixes needed — all three gate-clean as submitted.

PR
#8872 perf: remove cross-module ECS dispatch and argument-bundle overhead
#8875 perf(descriptors): index descriptors by owner instead of scanning
#8877 fix(runtime): match Node fs.readFile prototype (clears the r22 parity blocker)

Audit of #8875

These tables are process-global maps keyed by (owner_address, key), so an added secondary index in the same key domain is worth checking carefully.

The new indexes stay consistent with the primary maps at every mutation point. Traced all of them: add (:771, :992, :1051-1052, :1084), remove (:790, :1011), object destruction (:1210-1212), and owner transfer (:1259-1260). transfer_descriptor_owner moves both primary maps and calls owner_index_transfer on both new indexes. No desync path found.

It also fixes a pre-existing correctness bug. Its own comment records it: a freshly grown array has a null meta, so owner_may_have_descriptor_entries answered false authoritatively, and Object.keys / getOwnPropertyDescriptor silently lost every accessor the array had before growth. That gate sat in front of the old full-table scan too, so the scan never ran for the new owner either. Carrying the per-object Bloom summary across the transfer is a real fix, not just an indexing change.

One question flagged, not blocking. transfer_descriptor_owner is called from array growth (array/push_pop.rs:199), not from any GC move path — so owner-address keying relies on descriptor-owning objects not being relocated. That is pre-existing: #8875 adds an index in the same key domain rather than introducing the exposure. But it is the #8393 shape (a side table keyed by a raw heap address going stale after a copying minor), and I could not locate the mechanism that guarantees it. Worth confirming whether such objects are pinned; if they are not, both the old and new tables share the exposure.

Validation (merged tree)

Summary by CodeRabbit

  • Performance

    • Improved ECS workloads through faster cross-module execution and reduced call overhead.
    • Accelerated array indexing, appends, iteration callbacks, collection size reads, and short string comparisons.
    • Improved Object.keys and for…in performance, especially when unrelated objects have descriptors.
  • Bug Fixes

    • Preserved array accessors during growth and garbage collection.
    • Preserved Map key/value types in for…of destructuring.
    • Aligned callback-style native exports such as fs.readFile with expected .prototype behavior.
    • Improved array layout and garbage-collection bookkeeping correctness.

Ralph Küpper and others added 22 commits August 26, 2026 23:09
…ry entry

Three hot paths answered "what does THIS owner have?" by walking every
descriptor in the process and filtering on the owner address:

  * js_object_keys' array branch, twice (enumeration.rs) — a full
    property_descriptors walk per enumeration, just to decide whether a
    per-index enumerable check was needed;
  * accessor_descriptor_keys_for_obj, on the own-keys path;
  * transfer_descriptor_owner, on every ArrayHeader growth;
  * scan_descriptor_roots_mut, on EVERY GC cycle — so since the moving
    young-gen scavenge became default (#7019) this was a per-collection
    tax proportional to the whole program's descriptor count rather than
    to what actually moved.

Profiling `claude -p` put 46.6% of main-thread samples in
shapes/descriptors, with a HashMap Keys iteration the single hottest
self-time entry by 4x over anything else.

DescriptorTables now carries attr_keys_by_owner / accessor_keys_by_owner
mirroring the two (owner, key) maps, so each of those becomes a lookup.
The maps stay authoritative; the index is a mirror, and the tests assert
that invariant directly (index == what a full scan would return) across
install, redefine, delete, bulk-clear and owner transfer, because the
failure mode of a mirror is silent drift, not a crash.

Also fixes a pre-existing correctness bug the new tests caught:
transfer_descriptor_owner moved descriptors to the new address but never
carried the per-object Bloom summary. A freshly grown array has a null
meta, for which owner_may_have_descriptor_entries answers false
AUTHORITATIVELY — so after an array grew, Object.keys and
getOwnPropertyDescriptor silently lost every accessor it had. That was
equally true before this change: the gate sat in front of the old scan,
so the scan never ran for the new owner.
The `lint` job failed on four gates that the PR's own changes tripped:

- changelog: add the `changelog.d/8872-*` fragment for the crates/ changes.
- file size: `array/indexing.rs` reached 2,024 lines after the resolved-store
  work; move the transactional `js_array_numeric_range_add*` kernel (a block
  with no raw-handle or address-classification debt, so no per-module ratchet
  ceiling moves) into `array/numeric_range.rs`.
- local-binding-type audit: classify the synthetic `arguments.length` marker
  read in `property_get.rs::lower` (runtime-validated: the marker type exists
  only in direct-call-only clones whose caller materialized the count).
- GC store-site inventory: register `store_array_slot_resolved` as a
  chain-verified discharge helper for the three BARRIERED markers that now
  lean on it, mark its own resolved slot write, and pin the second `apush`
  codegen marker (the unconditional element store inside
  `emit_dynamic_pointer_push_store`, barriered by the same stem) with the
  self-test tree updated to match.

Every step of the lint job was replayed locally, including the raw-handle and
unrooted-local ratchets against the merge base d354443.

Claude-Session: https://claude.ai/code/session_01FUvFrRNZyc5qknBiJbYbby
@proggeramlug
proggeramlug merged commit 59a4c9a into main Aug 27, 2026
19 of 20 checks passed
@proggeramlug
proggeramlug deleted the merge/batch-8872-8877 branch August 27, 2026 10:13
@coderabbitai

coderabbitai Bot commented Aug 27, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 530c97fb-116a-468a-b3cd-332849b39780

📥 Commits

Reviewing files that changed from the base of the PR and between 41e8479 and a2560b5.

📒 Files selected for processing (69)
  • changelog.d/8872-ecs-cross-module-dispatch.md
  • changelog.d/8875-descriptor-owner-index.md
  • changelog.d/8877-release-r23-node-function-prototype.md
  • crates/perry-codegen/src/codegen/arguments.rs
  • crates/perry-codegen/src/codegen/closure.rs
  • crates/perry-codegen/src/codegen/entry.rs
  • crates/perry-codegen/src/codegen/function.rs
  • crates/perry-codegen/src/codegen/method.rs
  • crates/perry-codegen/src/codegen/method_registry.rs
  • crates/perry-codegen/src/codegen/mod.rs
  • crates/perry-codegen/src/codegen/opts.rs
  • crates/perry-codegen/src/codegen/ordinary_method_artifacts.rs
  • crates/perry-codegen/src/collectors/hot_callees.rs
  • crates/perry-codegen/src/expr/array_callback_shape_tests.rs
  • crates/perry-codegen/src/expr/array_push.rs
  • crates/perry-codegen/src/expr/array_push_guard_tests.rs
  • crates/perry-codegen/src/expr/class_method_arguments_object_tests.rs
  • crates/perry-codegen/src/expr/compare.rs
  • crates/perry-codegen/src/expr/compare_tests.rs
  • crates/perry-codegen/src/expr/index_get.rs
  • crates/perry-codegen/src/expr/index_get_claim_tests.rs
  • crates/perry-codegen/src/expr/logical_collections.rs
  • crates/perry-codegen/src/expr/mod.rs
  • crates/perry-codegen/src/expr/property_get.rs
  • crates/perry-codegen/src/expr/property_get/generic_dispatch.rs
  • crates/perry-codegen/src/expr/property_get/tests.rs
  • crates/perry-codegen/src/expr/readonly_collection_tests.rs
  • crates/perry-codegen/src/lib.rs
  • crates/perry-codegen/src/lower_call/alloc_hot_tests.rs
  • crates/perry-codegen/src/lower_call/method_override.rs
  • crates/perry-codegen/src/lower_call/property_get/dynamic_dispatch.rs
  • crates/perry-codegen/src/lower_call/typed_shape_bake_tests.rs
  • crates/perry-codegen/src/runtime_decls/strings_part2.rs
  • crates/perry-codegen/tests/typed_feedback.rs
  • crates/perry-hir/src/lower/collection_view_tests.rs
  • crates/perry-hir/src/lower_decl/body_stmt.rs
  • crates/perry-runtime/src/array/element_shape.rs
  • crates/perry-runtime/src/array/element_shape_matrix_tests.rs
  • crates/perry-runtime/src/array/element_shape_tests.rs
  • crates/perry-runtime/src/array/header.rs
  • crates/perry-runtime/src/array/header_gc_slots.rs
  • crates/perry-runtime/src/array/indexing.rs
  • crates/perry-runtime/src/array/iter_methods.rs
  • crates/perry-runtime/src/array/mod.rs
  • crates/perry-runtime/src/array/numeric_range.rs
  • crates/perry-runtime/src/array/push_pop.rs
  • crates/perry-runtime/src/array/tests.rs
  • crates/perry-runtime/src/array/typed_array_receiver_tests.rs
  • crates/perry-runtime/src/gc/layout.rs
  • crates/perry-runtime/src/gc/tests/copying/all_pointer_elements_7469.rs
  • crates/perry-runtime/src/gc/tests/layout_trace/element_shape.rs
  • crates/perry-runtime/src/object/descriptor_state.rs
  • crates/perry-runtime/src/object/field_get_set/enumeration.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/native_module/constructor_exports.rs
  • crates/perry-transform/src/aggregate_scalar.rs
  • crates/perry-transform/src/inline/call_inliner.rs
  • crates/perry-transform/src/inline/cross_module.rs
  • crates/perry-transform/src/inline/mod.rs
  • crates/perry-transform/src/lib.rs
  • crates/perry/src/commands/compile/collect_modules.rs
  • crates/perry/src/commands/compile/collect_modules/finish.rs
  • crates/perry/src/commands/compile/object_cache.rs
  • crates/perry/src/commands/compile/object_cache/object_cache_tests.rs
  • crates/perry/src/commands/compile/run_pipeline.rs
  • crates/perry/tests/issue_5268_native_ctor_prototype_undefined.rs
  • scripts/gc_store_site_inventory.py
  • scripts/local_binding_type_allowlist.json
  • test-files/test_issue_5268_native_ctor_prototype_undefined.ts

📝 Walkthrough

Walkthrough

This PR adds cross-module function inlining, scalar arguments.length dispatch, specialized array and collection lowering, demand-driven array proofs, resolved array storage, descriptor owner indexes, Map type preservation, and Node-compatible callback export prototypes.

Changes

Compiler and code generation

Layer / File(s) Summary
Cross-module function inlining and scalar rewriting
crates/perry-transform/..., crates/perry/src/commands/compile/...
The compiler gathers safe exported function graphs, localizes them into destination modules, propagates re-exports, inlines record-returning calls, and scalarizes eligible return records.
Arguments-length direct ABI
crates/perry-codegen/src/codegen/..., crates/perry-codegen/src/lower_call/..., crates/perry/src/commands/compile/...
Eligible synthetic arguments.length methods receive scalar-count clones. Dispatch, imports, metadata, cache keys, and regression tests support the additive ABI.
Specialized lowering paths
crates/perry-codegen/src/expr/..., crates/perry-codegen/src/collectors/...
The code generator adds captureless Array.some, canonical dynamic array indexes, inline Map/Set sizes, three-byte string comparisons, tiny allocation admission, and guarded pointer append bookkeeping.
Map iteration typing
crates/perry-hir/src/lower_decl/body_stmt.rs, crates/perry-hir/src/lower/collection_view_tests.rs
Map key and value types now remain available through method-body for-of destructuring and specialized array calls.

Array runtime

Layer / File(s) Summary
Resolved array storage and numeric range kernel
crates/perry-runtime/src/array/indexing.rs, crates/perry-runtime/src/array/header_gc_slots.rs, crates/perry-runtime/src/array/numeric_range.rs, crates/perry-runtime/src/array/push_pop.rs
Array operations reuse resolved header flags and use a fused barrier-aware slot store. Numeric range helpers move into a dedicated module.
Array shape, iteration, and GC layout
crates/perry-runtime/src/array/element_shape.rs, crates/perry-runtime/src/array/iter_methods.rs, crates/perry-runtime/src/gc/layout.rs, crates/perry-runtime/src/gc/tests/...
Element-shape proofs become demand-driven. Rooted array access and captureless iteration gain fast paths. Empty arrays can promote to all-pointer layout after a pointer append.

Descriptor and native export behavior

Layer / File(s) Summary
Descriptor owner indexing
crates/perry-runtime/src/object/descriptor_state.rs, crates/perry-runtime/src/object/field_get_set/enumeration.rs
Descriptor tables maintain owner-key indexes across installation, deletion, transfer, pruning, and GC rekeying. Enumeration uses owner-scoped lookups.
Callback export prototypes
crates/perry-runtime/src/object/native_module/constructor_exports.rs, crates/perry/tests/..., test-files/...
fs.readFile is classified and tested as exposing a non-null prototype object.

Estimated code review effort: 5 (Critical) | ~120 minutes

Possibly related PRs

  • PerryTS/perry#8313: Directly relates to descriptor metadata and owner-based key handling.

Suggested reviewers: thehypnoo, jdalton

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch merge/batch-8872-8877

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Aug 27, 2026
…-mutation

Second merge round after the PerryTS#8872/PerryTS#8875/PerryTS#8877 batch landed. Conflicts and
their resolutions:

- codegen/method.rs: keep this branch's guarded-falsy/index/pshape-arg clone
  handling and add main's `!arguments_length_clone` exclusions.
- expr/property_get.rs: keep both the Symbol-then-named-field IC dispatch
  (ours) and main's synthetic `arguments.length` fast path.
- property_get/generic_dispatch.rs: main's native Map/Set `size` split ahead
  of the object PIC, with this branch's `is_object_kind` naming.
- lower_call/method_override.rs: `direct_call_fn` (main, argument-length
  clone) is consulted first, then the pshape+index clone (ours); the two are
  mutually exclusive by construction.
- array/element_shape.rs: adopt main's demand-driven proofs (no eager
  `establish` on the first store) inside this branch's
  `note_element_store_with_bit` / `_resolved_flags` split; the now-unused
  `element_identity_of_bits` goes with it, and the renamed
  `pushes_do_not_create_an_unrequested_element_shape_proof` test replaces the
  eager-establishment one.
- array/header_gc_slots.rs + mod.rs: keep both resolved-head store helpers
  (`note_array_slot_resolved_flags` ours, `store_array_slot_resolved` main).
- array/push_pop.rs: `js_array_push_f64_resolved` now stores through main's
  `store_array_slot_resolved`.
- array/indexing.rs: the strict setter keeps this branch's dense fast path
  first, then main's resolved-head strict path; main moved the numeric-range
  helpers into `array/numeric_range.rs` (byte-identical bodies), so the
  in-file copies and their keepalive anchors are dropped; main's fused
  strict store in `js_array_set_index_or_string_strict` is ported into
  `indexing_keyed.rs`.
- expr/index_get_claim_tests.rs: union of imports/constants and both test
  sets (main's canonical-i32 split tier and this branch's `Any`-key tier
  are complementary arms).
- lower_call/property_get/dynamic_dispatch.rs grew past the 2,000-line gate;
  the tower-of-pshape routing moved to `dynamic_dispatch_tower.rs`.

Verified locally: fmt; perry-codegen and perry-runtime lib + test targets
build warning-free; both suites green; file-size, GC store-site, addr-class,
raw-handle, shape-descriptor census, binding and architecture audits pass.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
proggeramlug pushed a commit to proggeramlug/perry that referenced this pull request Aug 27, 2026
…ses densely

After merging main (PerryTS#8878 / PerryTS#8872's canonical-i32 read split), a declared-array
receiver with a non-static key takes the guarded plain-array tier first. On an
object-backed `class X extends Array` receiver (wolf-ecs `Archetype`,
`packed[sparse[x]]` in SparseSet.has/remove) that guard always misses and
`js_typed_feedback_array_index_get_fallback_boxed`'s GC_TYPE_OBJECT arm
stringified every index into a by-name lookup (from_utf8 + string alloc +
reflection ladder per read): both wolf-ecs benchmarks regressed ~2.2x.

The fallback now asks `array_subclass_fast_index_get` for a canonical
(plain or INT32-boxed) non-negative index before its registry probes and the
by-name path; receivers without a dense proof keep the established route.

Mac mini 11-pair screen vs the pre-merge build: add/remove +0.5%, entity-cycle
-1.2% (from +126% / +121%); semantics probe byte-identical to Node.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ
proggeramlug added a commit that referenced this pull request Aug 27, 2026
* perf: cache owning Uint32Array admissions

* perf: fast-path Array subclass length misses

* perf(array): accumulated ECS optimization work through v74

Accumulated codex ECS campaign work (v40–v74) on top of the two prior
commits on this branch: Array-subclass dense-tail fast paths and
validated-object prototype-override reads (v72), pre-statepoint inlining of
compact exact-receiver ($pshape) guarded specializations using the lowered
LLVM IR size (v74), plus the supporting collectors/tests. Details, rejected
experiments and measurements are in
secret-tests/ECS_PERFORMANCE_HANDOFF_2026-08-27.md.

Mac mini (taskpolicy -t 0 -l 0, 11 alternating pairs) at v74:
wolf-ecs add/remove 0.5562 ms/op, entity-cycle 0.4988 ms/op
(Node 26.5.1: 0.1337 / 0.1492).

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(codegen): follow one growth-forwarding edge in the guarded array store

`this.vals[i] = v` has no writeback slot: once the array grows past its
initial capacity the object field keeps the pre-grow forwarding stub, and
the guarded property-receiver STORE tier rejected the stub on every later
store (`!GC_FLAG_FORWARDED`), sending the whole store out of line through
the extend helper and the allocator/registry resolver. The READ tier already
followed one edge inline; mirror it: `deref` selects the stub's forwarding
word (heap-band checked), a new `deref.live` block re-validates the
destination header, and the fast arm stores into the live head.

wolf-ecs (Mac mini, 11 pairs): add/remove -9.15% (11/11),
entity-cycle -13.67% (11/11). Test:
index_set_barrier_tests::the_guarded_property_receiver_store_follows_one_forwarding_edge_inline

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(array): gate the raw-f64 downgrade note inline; typed-array pre-dispatch

- index_set_guarded.rs: the fast arm only calls js_array_note_numeric_write
  when the live head's `_reserved` word (already loaded by `deref.live`)
  has a raw-f64 bit set; the note is exactly "clear those bits if the value
  is not a Number" and was re-resolving the receiver through the tracked
  resolver on every pointer store.
- header.rs: js_array_note_numeric_write returns early for Number values and
  for already-clear live headers before paying clean_arr_ptr.
- indexing.rs: js_array_get_f64 dispatches a GC_TYPE_TYPED_ARRAY-tagged,
  registered receiver to js_typed_array_get before clean_arr_ptr (a
  guaranteed tracked miss for a typed array).

wolf-ecs (Mac mini, 11 pairs): add/remove -4.86% (11/11),
entity-cycle -5.50% (11/11).

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(codegen): exact inline typeof-number compare; header-branded typed-array reads

- compare.rs: `typeof local === "number"` / `!==` decides the
  definitely-Number cases inline (top 16 bits outside 0x7FF9..=0x7FFF, not the
  untagged raw typed-array pointer shape, outside the Web Streams id band) and
  keeps js_value_typeof_tag on the slow arm, so the two routes can never
  disagree. A 33-kind differential probe matches Node byte-for-byte.
- index_get/inline_dyn_typed_array.rs: the inline dynamic typed-array read
  brands the receiver off its GC_TYPE_TYPED_ARRAY header and reads the element
  kind from the TypedArrayHeader instead of probing the 64-slot direct-mapped
  PERRY_TA_KIND_CACHE, which every ordinary-array registry miss also writes
  negative entries into (hot typed arrays kept being evicted and missed the
  tier). PERRY_TA_VIEW_GUARD still gates the whole tier.

wolf-ecs (Mac mini, 11 pairs): add/remove -1.28% (11/11),
entity-cycle -0.73% (11/11).

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(array): route object receivers to the subclass fast read before clean_arr_ptr

An ordinary-object receiver (the object-backed `class X extends Array`
instance behind wolf-ecs' `packed[sparse[x]]`) can never be an ArrayHeader,
so clean_arr_ptr's tracked-allocation resolver was a guaranteed miss on every
js_array_get_f64 call for it. Ask array_subclass_fast_index_get_raw first when
the header tag already read for the Map/Set probes says GC_TYPE_OBJECT; every
rejected case still reaches the complete resolver and spec-generic Get.

wolf-ecs (Mac mini, 11 pairs): add/remove -2.03% (11/11),
entity-cycle -2.39% (11/11). Cumulative vs v74: -16.5% / -20.9%
(0.4645 / 0.3944 ms/op).

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(codegen): give integer-valued dynamic keys the inline numeric read tiers

A declared-array receiver read with an `Any`-typed key (`packed[sparse[x]]`
in the wolf-ecs SparseSet, `a[b[i]]` in general) always took the out-of-line
`js_array_get_index_or_string` route because the key carried no integer
array-index proof. Test the key inline — nonnegative, below 2^32, and equal
to its own fptosi/sitofp round trip — and on a hit take exactly the tiers a
statically proven index takes: the inline typed-array read, the dense
Array-subclass `arrlike.ic` shape cache, then the complete
`js_packed_arraylike_index_get` → `js_dyn_index_get` dispatcher. Fractional,
negative, NaN and out-of-range keys keep the previous route.

wolf-ecs (Mac mini, 11 pairs): add/remove -2.37% (11/11),
entity-cycle -2.89% (11/11); the js_array_get_index_or_string →
js_array_get_f64 → array_subclass_fast_index_get_raw chain (4.4% of the
add/remove profile) is gone. Cumulative vs v74: -18.5% / -23.1%
(0.4531 / 0.3836 ms/op). Test:
index_get_claim_tests::any_typed_dynamic_key_takes_the_numeric_tiers_when_it_is_an_array_index

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* test(codegen): update the proven-number strict-eq rooting test to the inline lowering

`x === {…}` with a proven-Number left operand now lowers to an inline
`fcmp oeq` (every non-Number NaN-box reads as a NaN double, so the object
compares unequal exactly as `js_eq` answered), leaving no `js_eq` call for
the test to find. Keep the test's actual claim — the non-pointer left
operand stays in the register produced above the right operand's
allocation instead of being rooted/re-read — on the fcmp operands, and pin
that no runtime equality call remains.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* ci: ratchet baselines for the file splits, census gate for cache carriers, changelog fragment

- addr-class ratchet/allowlist and raw-handle debt ceilings: the sites that
  the 2,000-line split moved from `array/indexing.rs` into
  `array/indexing_keyed.rs` keep their existing justification under the new
  path (indexing 4→3 / 13→7, indexing_keyed 1 / 6); lower the stale
  `field_set_by_name/fast_paths.rs` handle-floor count 3→2.
- shape-descriptor census: refresh the exact call-site multiset for the
  moved `property_get/composed_ics.rs` sites and the new
  `stmt/cached_field_index_return.rs` / `generic_dispatch.rs` header-size
  reads, and pin the scanner's rooting gate as
  `descriptor.old_carrier || descriptor.cache_carrier` — a runtime
  optimization cache that can reinstall a historical shape is a strong
  metadata owner a minor cannot enumerate (see `ShapeDescriptor::
  cache_carrier`), so its keys array must be rooted and rewritten before
  weak pruning. The sabotage self-test is updated to the new gate.
- changelog.d/8876 fragment.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(runtime): array-read fallback serves object-backed Array subclasses densely

After merging main (#8878 / #8872's canonical-i32 read split), a declared-array
receiver with a non-static key takes the guarded plain-array tier first. On an
object-backed `class X extends Array` receiver (wolf-ecs `Archetype`,
`packed[sparse[x]]` in SparseSet.has/remove) that guard always misses and
`js_typed_feedback_array_index_get_fallback_boxed`'s GC_TYPE_OBJECT arm
stringified every index into a by-name lookup (from_utf8 + string alloc +
reflection ladder per read): both wolf-ecs benchmarks regressed ~2.2x.

The fallback now asks `array_subclass_fast_index_get` for a canonical
(plain or INT32-boxed) non-negative index before its registry probes and the
by-name path; receivers without a dense proof keep the established route.

Mac mini 11-pair screen vs the pre-merge build: add/remove +0.5%, entity-cycle
-1.2% (from +126% / +121%); semantics probe byte-identical to Node.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* runtime(array): keyed index paths reload the receiver via across_* (raw-handle debt -6)

The file split moved six bare `get_raw_{mut,const}_ptr` reads into
`indexing_keyed.rs`, which the raw-handle ratchet rejects as a module that was
not listed at the merge base. Every site had the sanctioned shape already —
root the receiver, run the allocating stringify / symbol store, reload — so
they now use `across_const` / `across_mut`. `indexing_keyed.rs` needs no
ceiling; the baseline ratchets 970 -> 964.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* review: keep the fused u31 push non-reentrant, bound ECS columns, lifecycle the cache-carrier gate

Review follow-ups on #8876:

- `js_array_push_u31_with_length` stays allocate-but-never-reenter: it now
  answers null for receivers whose push can run user code (indexed
  descriptors / prototype indices, Proxy traps, foreign families) instead of
  calling the spec / public push itself; the generated caller takes the
  complete guarded push (`js_array_push_guard` + `js_array_push_f64`) in a
  new `apush.u31.generic` block. Test: the fused-push runtime test declines a
  typed-array receiver; the composed-clone IR test pins the hot path / fallback
  split.
- `js_packed_ecs_u32_loop_guard` declines admission when a component column
  is shorter than the admitted bound the receiver guard published (`out[6]`),
  so the fused loop cannot read past a column's payload. Test added.
- `object_hot_for_owner` validates the cached table pointer against the
  current thread's `RuntimeState` before reuse.
- `cache_carrier` gets a lifecycle: noted only after an entry naming the pair
  was inserted, and recomputed from live table occupancy after every full
  trace (`recompute_cache_carriers_after_full_trace`, called beside the
  old-carrier rotation) so a descriptor whose entries were evicted stops being
  rooted. Test: carrier bits follow live occupancy across a recompute.
- `js_object_get_symbol_then_field_ic_miss` is declared with the runtime's
  pointer parameter type.
- Minor: parenthesized mixed `&&`/`||` assertion, unique test class ids,
  `function_this_safe` visited-key includes the terminal-`this` allowance,
  exhaustive `UnaryOp` match, changelog fragment restated as shipped behavior.

Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ

* perf(shapes): drop SipHash from ids_by_facts, the last one on the shape path

ids_by_facts was the only shape-table map still on std's RandomState.
Profiling `claude -p` showed RandomState::hash_one at 17 self-samples
inside shapes:: alone (57 across the process) — pure hashing overhead on
a lookup that runs on every descriptor install and retire.

Its sibling maps already moved off SipHash (#8125). The standing comment
argued only against PtrHasher, whose write_* methods OVERWRITE the
accumulator — correct for a single-word key, and wrong for this
five-field one, which would collapse to its last field. That objection
does not apply to FastKeyHasher: it implements only `write`, so the
derived Hash's write_u32/write_u64 calls all forward there and FOLD with
FNV-1a, reaching every field.

The key is internal shape state, never program input, so DoS-resistant
hashing buys nothing — the same rationale already applied to the
descriptor side tables.

Test pins the folding property by varying one field at a time and
requiring a distinct hash. Sabotage-checked against PtrHasher: it fails
with 'changing keys alone must change the hash'. Suite 2717 passed.

* changelog: add fragment for the ids_by_facts hasher change

---------

Co-authored-by: Ralph Küpper <ralph@skelpo.com>
Co-authored-by: Ralph Küpper <ralph3@skelpo.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant