Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions changelog.d/0000-claimed-key-single-lattice.md
Original file line number Diff line number Diff line change
@@ -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.
64 changes: 17 additions & 47 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand All @@ -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)],
)
}

Expand Down
38 changes: 27 additions & 11 deletions crates/perry-codegen/src/expr/index_get_claim_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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}"
);
}

Expand Down
Loading