From 9e492352d2eb5704355f7cba55fd475816535b79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Fri, 28 Aug 2026 11:38:54 +0200 Subject: [PATCH] codegen: emit the receiver-unknown numeric index tiers once per dynamic site MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `lower_claimable_array_string_key_get` — the canonical-i32 split's runtime-key arm for erased-Array receivers — still carried its own copy of the inline typed-array + dense-subclass `arrlike.ic` + dispatcher lattice from the v83 dynamic-key work. Since the brand arm (`aidx.claimed.other`) every integral key below 2^31, canonical or INT32-boxed, is served by the canonical arm's single copy, so the runtime copy only ever ran for integral keys in [2^31, 2^32) — at ~11 KB of IR per site (wolf-ecs `SparseSet.has`'s proven-this u31 clone was 46.7 KB for one statement, 22 KB of it these two lattices, against a 16 KiB pre-statepoint inline budget). Its other caller passes static string/symbol keys, where the numeric path is dead. Those keys now take the complete `js_array_get_index_or_string` route, which handles every index. The v83 test asserts exactly one inline typed-array tier and one dense-subclass tier per site, and no `aidxkey.int` block. Claude-Session: https://claude.ai/code/session_019WVcWKmYsUBnnFB7nBgbBJ --- .../0000-claimed-key-single-lattice.md | 3 + crates/perry-codegen/src/expr/index_get.rs | 64 +++++-------------- .../src/expr/index_get_claim_tests.rs | 38 +++++++---- 3 files changed, 47 insertions(+), 58 deletions(-) create mode 100644 changelog.d/0000-claimed-key-single-lattice.md diff --git a/changelog.d/0000-claimed-key-single-lattice.md b/changelog.d/0000-claimed-key-single-lattice.md new file mode 100644 index 0000000000..1e3dfcc508 --- /dev/null +++ b/changelog.d/0000-claimed-key-single-lattice.md @@ -0,0 +1,3 @@ +### Changed + +- `arr[key]` on an erased-Array receiver with a dynamic key emits the receiver-unknown numeric tiers (inline typed-array read, dense Array-subclass `arrlike.ic`, complete dispatcher) once per site instead of twice: the canonical-i32 arm already serves every integral key below 2^31, so the runtime-key arm's duplicate copy — which only served keys in `[2^31, 2^32)` at ~11 KB of IR per site — now takes the complete `js_array_get_index_or_string` route. Same semantics; one-statement methods such as wolf-ecs `SparseSet.has` shrink by ~a quarter and come closer to the pre-statepoint inline budget. diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index a5232a03cb..601bf24723 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -518,9 +518,22 @@ fn lower_array_index_get_via_canonical_i32_split( /// /// The ordinary array ABI takes an already-unboxed `ArrayHeader*`, which loses /// an SSO String's tag/payload before the runtime can validate the claim. Keep -/// the receiver boxed until that immediate representation is separated; heap -/// Strings remain real pointers and are classified inside the array-key helper, -/// while every other value retains the established fallback. +/// the immediate SSO encoding on the boxed string helper; every other key goes +/// to the complete `js_array_get_index_or_string` route, which classifies the +/// receiver itself. +/// +/// This arm no longer carries its own copy of the receiver-unknown numeric +/// tiers (inline typed-array read, dense-subclass `arrlike.ic`, complete +/// dispatcher). It is reached from two places: the canonical-i32 split's +/// runtime-key arm, where every integral key in `[0, 2^31)` — canonical or +/// INT32-boxed — has already been taken by the canonical arm, whose +/// `aidx.claimed.other` branch emits exactly those tiers once; and the +/// static string/symbol key route, where the key is never a number. The +/// copy that used to live here served only integral keys in `[2^31, 2^32)`, +/// at ~11 KB of IR per site (`tav.*` + `arrlike.ic.*`), and that IR is what +/// kept one-statement clones such as wolf-ecs `SparseSet.has` out of the +/// pre-statepoint inline budget. Those keys keep the complete route, which +/// handles every index. fn lower_claimable_array_string_key_get( ctx: &mut FnCtx<'_>, arr_box: &str, @@ -548,46 +561,7 @@ fn lower_claimable_array_string_key_get( let string_end = ctx.block().label.clone(); ctx.block().br(&merge_label); - // A dynamic key that is an integer-valued double in `[0, 2^32)` IS an - // array index, so the receiver-unknown numeric tiers apply to it exactly - // as they do to a statically proven index: the inline typed-array read, - // then the dense Array-subclass `arrlike.ic` shape cache, then the - // complete `js_packed_arraylike_index_get` → `js_dyn_index_get` - // dispatcher. Before this, 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 below. Fractional, negative, NaN - // and out-of-range keys keep that route unchanged; `-0` round-trips to - // index 0, which is what ToPropertyKey gives it too. - let int_idx = ctx.new_block("aidxkey.int"); - let int_label = ctx.block_label(int_idx); - let generic_idx = ctx.new_block("aidxkey.generic"); - let generic_label = ctx.block_label(generic_idx); ctx.current_block = array_idx; - { - let blk = ctx.block(); - let nonnegative = blk.fcmp("oge", idx_double, "0.0"); - let below_limit = blk.fcmp("olt", idx_double, "4294967296.0"); - let in_range = blk.and(I1, &nonnegative, &below_limit); - blk.cond_br(&in_range, &int_label, &generic_label); - } - ctx.current_block = int_idx; - let int_label_checked = ctx.new_block("aidxkey.int.exact"); - let int_label_checked_label = ctx.block_label(int_label_checked); - { - let blk = ctx.block(); - // In range, so `fptosi` is well-defined; the round trip rejects - // fractional keys. - let idx_i64 = blk.fptosi(DOUBLE, idx_double, I64); - let idx_back = blk.sitofp(I64, &idx_i64, DOUBLE); - let is_integer = blk.fcmp("oeq", &idx_back, idx_double); - blk.cond_br(&is_integer, &int_label_checked_label, &generic_label); - } - ctx.current_block = int_label_checked; - let index_value = lower_inline_dyn_typed_array_get(ctx, arr_box, idx_double, false); - let index_end = ctx.block().label.clone(); - ctx.block().br(&merge_label); - - ctx.current_block = generic_idx; let arr_handle = unbox_to_i64(ctx.block(), arr_box); let array_value = ctx.block().call( DOUBLE, @@ -600,11 +574,7 @@ fn lower_claimable_array_string_key_get( ctx.current_block = merge_idx; ctx.block().phi( DOUBLE, - &[ - (&string_value, &string_end), - (&index_value, &index_end), - (&array_value, &array_end), - ], + &[(&string_value, &string_end), (&array_value, &array_end)], ) } diff --git a/crates/perry-codegen/src/expr/index_get_claim_tests.rs b/crates/perry-codegen/src/expr/index_get_claim_tests.rs index 09e28734bf..1e6f5f334e 100644 --- a/crates/perry-codegen/src/expr/index_get_claim_tests.rs +++ b/crates/perry-codegen/src/expr/index_get_claim_tests.rs @@ -394,24 +394,40 @@ fn any_typed_dynamic_key_takes_the_numeric_tiers_when_it_is_an_array_index() { ], ); assert!( - ir.contains("aidxkey.int.exact") && ir.contains("aidxkey.generic"), + ir.contains("aidx.canonical") && ir.contains("aidx.claimed.other"), "the dynamic key must be classified inline before choosing a route:\n{ir}" ); - let exact = super::class_field_barrier_tests::block_body(&ir, "aidxkey.int.") - .expect("the range-checked key block exists"); - assert!( - exact.contains("fptosi double") - && exact.contains("sitofp i64") - && exact.contains("fcmp oeq"), - "the integer test must be the fptosi/sitofp round trip:\n{exact}" - ); assert!( ir.contains("tav.get.brand") && ir.contains("arrlike.ic.family_token"), "an integer key must reach the inline typed-array and dense-subclass tiers:\n{ir}" ); + // ONE copy of those tiers per site: the canonical arm's. The runtime-key + // arm used to emit a second `tav.*` + `arrlike.ic.*` lattice for integral + // keys in `[2^31, 2^32)` — ~11 KB of IR per site that kept one-statement + // clones (wolf-ecs `SparseSet.has`) out of the pre-statepoint inline + // budget — and now sends them down the complete route instead. + let defined = |prefix: &str| { + ir.lines() + .filter(|l| l.starts_with(prefix) && l.ends_with(':')) + .count() + }; + assert_eq!( + defined("tav.get.brand."), + 1, + "exactly one inline typed-array tier per dynamic site:\n{ir}" + ); + assert_eq!( + defined("arrlike.ic.family_token."), + 1, + "exactly one dense-subclass tier per dynamic site:\n{ir}" + ); + assert!( + !ir.contains("aidxkey.int"), + "the runtime-key arm must not carry its own numeric lattice:\n{ir}" + ); assert!( - ir.contains("call double @js_array_get_index_or_string("), - "non-index keys must keep the complete key route:\n{ir}" + ir.contains("aidxkey.sso") && ir.contains("call double @js_array_get_index_or_string("), + "string and non-index keys must keep the complete key route:\n{ir}" ); }