diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 4d3ccb60232..a06da252350 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -30,7 +30,7 @@ use crate::type_analysis::{ is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I16, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR}; use super::arrays_finds::lower_buffer_index_get_i32; #[allow(unused_imports)] @@ -386,6 +386,341 @@ fn lower_guarded_array_index_get( )) } +/// #5525 follow-up: emit a guarded **inline** typed-array element read for an +/// `obj[i]` whose receiver static type is erased (`any`/unknown) but is, at +/// runtime, commonly an owning numeric typed array reached through an untyped +/// param — exactly bcryptjs's `S[i]`/`P[i]` Blowfish boxes (~600M reads for one +/// cost-12 `compareSync`). Instead of an out-of-line `js_dyn_index_get` call + +/// `lookup_typed_array_kind` + `js_number_coerce` per element, this inlines: +/// 1. receiver-is-pointer NaN-box guard, +/// 2. a read of the process-global `PERRY_TA_VIEW_GUARD` (must be 0 → every +/// live typed array uses inline storage, so `data_ptr == header + 16`), +/// 3. a probe of the `PERRY_TA_KIND_CACHE` slot for the receiver address +/// (matches the cached `(addr << 8) | tag` word; the tag is the element +/// kind and must be a non-BigInt kind ≤ `KIND_UINT8_CLAMPED`), +/// 4. an index validity + bounds check against the header `length`, +/// 5. a direct per-kind element load + int↔f64 widen, +/// and falls back to the existing `js_dyn_index_get` slow path on ANY guard +/// miss (non-pointer, cache miss, view live, BigInt/Float16 kind, OOB / +/// fractional / negative index, runtime-string or symbol key). Because every +/// rejected case defers to the unchanged runtime helper, semantics are +/// identical; only the hot monomorphic numeric-typed-array case is short-cut. +/// `obj_box` / `idx_d` are the already-lowered receiver and index (DOUBLE). +fn lower_inline_dyn_typed_array_get(ctx: &mut FnCtx<'_>, obj_box: &str, idx_d: &str) -> String { + // TAG_MASK / POINTER_TAG / POINTER_MASK as signed-i64 LLVM literals. + let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK); + let pointer_tag = crate::nanbox::POINTER_TAG_I64; + let pointer_mask = crate::nanbox::POINTER_MASK_I64; + + let fast_idx = ctx.new_block("tav.get.fast"); + let load_idx = ctx.new_block("tav.get.load"); + let slow_idx = ctx.new_block("tav.get.slow"); + let merge_idx = ctx.new_block("tav.get.merge"); + let fast_label = ctx.block_label(fast_idx); + let load_label = ctx.block_label(load_idx); + let slow_label = ctx.block_label(slow_idx); + let merge_label = ctx.block_label(merge_idx); + + // ---- entry: combined cache/kind/range guard -> fast | slow ---- + let entry_guard = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, pointer_mask); + // is_pointer: (bits & TAG_MASK) == POINTER_TAG + let tagged = blk.and(I64, &obj_bits, &tag_mask); + let is_ptr = blk.icmp_eq(I64, &tagged, pointer_tag); + // view guard must be 0 (all typed arrays inline-storage) + let vg = blk.load(I64, "@PERRY_TA_VIEW_GUARD"); + let vg_zero = blk.icmp_eq(I64, &vg, "0"); + // cache slot = (raw >> 3) & 63 + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + // addr match: (entry_val u>> 8) == raw (also rejects empty slot = 0) + let entry_addr = blk.lshr(I64, &entry_val, "8"); + let addr_match = blk.icmp_eq(I64, &entry_addr, &raw); + // kind = entry_val & 0xFF; loadable numeric kind = kind <= 8 + // (KIND_INT8=0 .. KIND_UINT8_CLAMPED=8; rejects BigInt 9/10, + // Float16 11, and the 0xFF "not a typed array" sentinel). + let kind = blk.and(I64, &entry_val, "255"); + let kind_ok = blk.icmp_ule(I64, &kind, "8"); + // index float-range pre-checks (well-defined on NaN → false): the + // fptosi in the load block is only reached when these hold, so its + // result is never poison there. + let idx_ge0 = blk.fcmp("oge", idx_d, "0.0"); + let idx_lt = blk.fcmp("olt", idx_d, "4294967296.0"); + // AND-reduce all guards. + let g = blk.and(I1, &is_ptr, &vg_zero); + let g = blk.and(I1, &g, &addr_match); + let g = blk.and(I1, &g, &kind_ok); + let g = blk.and(I1, &g, &idx_ge0); + blk.and(I1, &g, &idx_lt) + }; + ctx.block().cond_br(&entry_guard, &fast_label, &slow_label); + + // ---- fast: validate integer index + bounds -> load | slow ---- + ctx.current_block = fast_idx; + let (raw, idx_i64, kind) = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, pointer_mask); + // kind re-read from cache (cheap; keeps the fast block self-contained). + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + let kind = blk.and(I64, &entry_val, "255"); + // idx is in [0, 2^32) (entry guard) so fptosi i64 is well-defined. + let idx_i64 = blk.fptosi(DOUBLE, idx_d, I64); + (raw, idx_i64, kind) + }; + let fast_ok = { + let blk = ctx.block(); + // reject fractional indices: sitofp(idx_i64) == idx_d + let idx_back = blk.sitofp(I64, &idx_i64, DOUBLE); + let is_int = blk.fcmp("oeq", &idx_back, idx_d); + // bounds: idx < header.length (u32 at offset 0) + let hdr_ptr = blk.inttoptr(I64, &raw); + let len = blk.load(I32, &hdr_ptr); + let len_i64 = blk.zext(I32, &len, I64); + let in_bounds = blk.icmp_ult(I64, &idx_i64, &len_i64); + blk.and(I1, &is_int, &in_bounds) + }; + ctx.block().cond_br(&fast_ok, &load_label, &slow_label); + + // ---- load: per-kind direct element load (data = header + 16) ---- + ctx.current_block = load_idx; + // (value, end_label) for each per-kind load block, collected for the merge. + let kind_incoming: Vec<(String, String)>; + { + // Per-kind load blocks. Each computes the element address from + // `data = raw + 16` and `off = idx * elem_size`, loads the native + // slot, and widens to f64. We branch on `kind` via a cond_br chain. + // kinds: 0 I8, 1 U8, 2 I16, 3 U16, 4 I32, 5 U32, 6 F32, 7 F64, + // 8 U8Clamped (== U8 load). All others were excluded by the entry + // guard (kind <= 8). + let data_base = { + let blk = ctx.block(); + blk.add(I64, &raw, "16") + }; + // Helper closure-like inline: build a block that loads with a given + // element byte-width shift + LLVM elem type + widen, then brs to merge. + // We emit explicit blocks since closures can't borrow ctx mutably here. + + // Create the per-kind blocks up front. + let b_i8 = ctx.new_block("tav.k.i8"); + let b_u8 = ctx.new_block("tav.k.u8"); + let b_i16 = ctx.new_block("tav.k.i16"); + let b_u16 = ctx.new_block("tav.k.u16"); + let b_i32 = ctx.new_block("tav.k.i32"); + let b_u32 = ctx.new_block("tav.k.u32"); + let b_f32 = ctx.new_block("tav.k.f32"); + let b_f64 = ctx.new_block("tav.k.f64"); + let l_i8 = ctx.block_label(b_i8); + let l_u8 = ctx.block_label(b_u8); + let l_i16 = ctx.block_label(b_i16); + let l_u16 = ctx.block_label(b_u16); + let l_i32 = ctx.block_label(b_i32); + let l_u32 = ctx.block_label(b_u32); + let l_f32 = ctx.block_label(b_f32); + let l_f64 = ctx.block_label(b_f64); + + // Dispatch chain on `kind` (in the load block). + let chk = |ctx: &mut FnCtx<'_>, k: &str, hit: &str, next_idx: usize| { + let next_label = ctx.block_label(next_idx); + let cond = ctx.block().icmp_eq(I64, &kind, k); + ctx.block().cond_br(&cond, hit, &next_label); + }; + // 0..7 explicit; kind 8 (U8Clamped) shares the U8 load as the final + // else (no further branch needed — entry guard already proved kind<=8). + let c1 = ctx.new_block("tav.kd1"); + let c2 = ctx.new_block("tav.kd2"); + let c3 = ctx.new_block("tav.kd3"); + let c4 = ctx.new_block("tav.kd4"); + let c5 = ctx.new_block("tav.kd5"); + let c6 = ctx.new_block("tav.kd6"); + let c7 = ctx.new_block("tav.kd7"); + chk(ctx, "0", &l_i8, c1); + ctx.current_block = c1; + chk(ctx, "1", &l_u8, c2); + ctx.current_block = c2; + chk(ctx, "2", &l_i16, c3); + ctx.current_block = c3; + chk(ctx, "3", &l_u16, c4); + ctx.current_block = c4; + chk(ctx, "4", &l_i32, c5); + ctx.current_block = c5; + chk(ctx, "5", &l_u32, c6); + ctx.current_block = c6; + chk(ctx, "6", &l_f32, c7); + ctx.current_block = c7; + // remaining: kind 7 → f64, else (8) → u8. + let is_f64 = ctx.block().icmp_eq(I64, &kind, "7"); + ctx.block().cond_br(&is_f64, &l_f64, &l_u8); + + // Each per-kind block: compute elem addr, load, widen, br merge. + // off = idx << shift; addr = data_base + off. + let mut incoming: Vec<(String, String)> = Vec::new(); + // I8 (sext), U8 (zext), I16 (sext), U16 (zext) via the small-int helper. + incoming.push(emit_inline_ta_int_load( + ctx, + b_i8, + &idx_i64, + &data_base, + &merge_label, + "0", + I8, + true, + )); + incoming.push(emit_inline_ta_int_load( + ctx, + b_u8, + &idx_i64, + &data_base, + &merge_label, + "0", + I8, + false, + )); + incoming.push(emit_inline_ta_int_load( + ctx, + b_i16, + &idx_i64, + &data_base, + &merge_label, + "1", + I16, + true, + )); + incoming.push(emit_inline_ta_int_load( + ctx, + b_u16, + &idx_i64, + &data_base, + &merge_label, + "1", + I16, + false, + )); + // I32: load i32, sitofp directly (sext to i32 is a no-op). + { + ctx.current_block = b_i32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(I32, &ptr); + let val = blk.sitofp(I32, &raw_elem, DOUBLE); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + // U32: load i32, treat as unsigned → uitofp. + { + ctx.current_block = b_u32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(I32, &ptr); + let val = blk.uitofp(I32, &raw_elem, DOUBLE); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + // F32: load float, fpext. + { + ctx.current_block = b_f32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(F32, &ptr); + let val = blk.fpext(F32, &raw_elem, DOUBLE); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + // F64: load double raw. + { + ctx.current_block = b_f64; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "3"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let val = blk.load(DOUBLE, &ptr); + let end_label = blk.label.clone(); + blk.br(&merge_label); + incoming.push((val, end_label)); + } + + // Hand the collected per-kind (value,label) pairs to the final merge. + kind_incoming = incoming; + } + + // ---- slow: the unchanged runtime dispatcher ---- + ctx.current_block = slow_idx; + let slow_val = ctx.block().call( + DOUBLE, + "js_dyn_index_get", + &[(DOUBLE, obj_box), (DOUBLE, idx_d)], + ); + let slow_end_label = ctx.block().label.clone(); + ctx.block().br(&merge_label); + + // ---- final merge: one phi over every per-kind fast end + the slow end ---- + ctx.current_block = merge_idx; + let mut incoming_refs: Vec<(&str, &str)> = kind_incoming + .iter() + .map(|(v, l)| (v.as_str(), l.as_str())) + .collect(); + incoming_refs.push((slow_val.as_str(), slow_end_label.as_str())); + ctx.block().phi(DOUBLE, &incoming_refs) +} + +/// Emit one per-kind small-integer (1/2-byte) typed-array element load block for +/// [`lower_inline_dyn_typed_array_get`]: switches to `blk_idx`, computes the +/// element address (`data_base + (idx << shift)`), loads `elem_ty`, sign-/zero- +/// extends to i32, converts to f64, and branches to `merge_label`. Returns the +/// `(value, end_label)` pair for the merge phi. +#[allow(clippy::too_many_arguments)] +fn emit_inline_ta_int_load( + ctx: &mut FnCtx<'_>, + blk_idx: usize, + idx_i64: &str, + data_base: &str, + merge_label: &str, + shift: &str, + elem_ty: crate::types::LlvmType, + signed: bool, +) -> (String, String) { + ctx.current_block = blk_idx; + let blk = ctx.block(); + let off = blk.shl(I64, idx_i64, shift); + let addr = blk.add(I64, data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let raw_elem = blk.load(elem_ty, &ptr); + let val = if signed { + let i32v = blk.sext(elem_ty, &raw_elem, I32); + blk.sitofp(I32, &i32v, DOUBLE) + } else { + let i32v = blk.zext(elem_ty, &raw_elem, I32); + blk.uitofp(I32, &i32v, DOUBLE) + }; + let end_label = blk.label.clone(); + blk.br(merge_label); + (val, end_label) +} + pub(crate) fn lower_numeric_index_get_for_number_context( ctx: &mut FnCtx<'_>, expr: &Expr, @@ -861,12 +1196,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { if recv_unknown && !index_is_static_string_or_symbol { let obj_box = lower_expr(ctx, object)?; let idx_d = lower_expr(ctx, index)?; - let blk = ctx.block(); - return Ok(blk.call( - DOUBLE, - "js_dyn_index_get", - &[(DOUBLE, &obj_box), (DOUBLE, &idx_d)], - )); + // #5525 follow-up: guarded inline typed-array element load at the + // access site (cache probe + bounds check + direct slot load), + // falling back to `js_dyn_index_get` on any guard miss. Removes + // the per-element out-of-line call + `lookup_typed_array_kind` + + // `js_number_coerce` on bcrypt's hot Int32Array `S[i]`/`P[i]`. + return Ok(lower_inline_dyn_typed_array_get(ctx, &obj_box, &idx_d)); } // Three cases: // 1. Receiver is a known array → inline f64 element load diff --git a/crates/perry-codegen/src/expr/index_set.rs b/crates/perry-codegen/src/expr/index_set.rs index de85ff166e9..3f26681b01e 100644 --- a/crates/perry-codegen/src/expr/index_set.rs +++ b/crates/perry-codegen/src/expr/index_set.rs @@ -31,7 +31,7 @@ use crate::type_analysis::{ is_numeric_expr, is_set_expr, is_string_expr, is_url_search_params_expr, receiver_class_name, }; #[allow(unused_imports)] -use crate::types::{DOUBLE, I1, I32, I64, I8, PTR}; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR}; #[allow(unused_imports)] use super::{ @@ -261,11 +261,16 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let obj_box = lower_expr(ctx, object)?; let idx_d = lower_expr(ctx, index)?; let val_double = lower_expr(ctx, value)?; - let blk = ctx.block(); - return Ok(blk.call( - DOUBLE, - "js_dyn_index_set", - &[(DOUBLE, &obj_box), (DOUBLE, &idx_d), (DOUBLE, &val_double)], + // #5525 follow-up: guarded inline typed-array element STORE at the + // access site, mirroring the inline read in index_get.rs. Removes + // the per-element out-of-line `js_dyn_index_set` call + + // `lookup_typed_array_kind` for bcrypt's `P[i]=`/`S[i]=` writes, + // falling back to `js_dyn_index_set` on any guard miss. + return Ok(lower_inline_dyn_typed_array_set( + ctx, + &obj_box, + &idx_d, + &val_double, )); } // Issue #637 / hono r2 followup: `arr[stringKey] = val` where @@ -952,3 +957,290 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { _ => unreachable!("expr/mod.rs dispatched a variant not handled by this submodule"), } } + +/// #5525 follow-up: guarded **inline** typed-array element STORE for an +/// `obj[i] = v` whose receiver static type is erased (`any`/unknown) but is, at +/// runtime, commonly an owning numeric typed array (bcryptjs's `P[i]=`/`S[i]=` +/// Int32Array boxes). Mirrors [`index_get::lower_inline_dyn_typed_array_get`]: +/// the same pointer / `PERRY_TA_VIEW_GUARD` / `PERRY_TA_KIND_CACHE` / index +/// guards, then a direct per-kind store into `header + 16 + idx*elem_size`, +/// falling back to `js_dyn_index_set` on any guard miss. The store result is the +/// assigned value (`val_double`), matching `js_dyn_index_set`'s return. +/// +/// Only the kinds with a simple ToInt32/ToUint32 truncating store (Int8/Uint8/ +/// Int16/Uint16/Int32/Uint32) or a direct float store (Float32/Float64) are +/// inlined — i.e. `kind <= KIND_FLOAT64` (7). Uint8ClampedArray (round-half-to- +/// even clamp), the BigInt kinds (ToBigInt / throw) and Float16 (f16 encode) are +/// excluded by the guard and defer to the runtime, which already owns them. The +/// integer truncation here (`toint32(value)` then narrow) is bit-identical to +/// the runtime `store_at`'s `to_uint32_bits(value) as `; the float store +/// is identical to `store_at`'s direct slot write — so behavior matches the +/// existing runtime fast path exactly. +fn lower_inline_dyn_typed_array_set( + ctx: &mut FnCtx<'_>, + obj_box: &str, + idx_d: &str, + val_double: &str, +) -> String { + let tag_mask = crate::nanbox::i64_literal(crate::nanbox::TAG_MASK); + let pointer_tag = crate::nanbox::POINTER_TAG_I64; + let pointer_mask = crate::nanbox::POINTER_MASK_I64; + + let fast_idx = ctx.new_block("tav.set.fast"); + let store_idx = ctx.new_block("tav.set.store"); + let slow_idx = ctx.new_block("tav.set.slow"); + let merge_idx = ctx.new_block("tav.set.merge"); + let fast_label = ctx.block_label(fast_idx); + let store_label = ctx.block_label(store_idx); + let slow_label = ctx.block_label(slow_idx); + let merge_label = ctx.block_label(merge_idx); + + // ---- entry: combined cache/kind/range guard -> fast | slow ---- + let entry_guard = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, pointer_mask); + let tagged = blk.and(I64, &obj_bits, &tag_mask); + let is_ptr = blk.icmp_eq(I64, &tagged, pointer_tag); + let vg = blk.load(I64, "@PERRY_TA_VIEW_GUARD"); + let vg_zero = blk.icmp_eq(I64, &vg, "0"); + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + let entry_addr = blk.lshr(I64, &entry_val, "8"); + let addr_match = blk.icmp_eq(I64, &entry_addr, &raw); + let kind = blk.and(I64, &entry_val, "255"); + // Stores inline only kinds with a trivial truncating/float store: + // kind <= KIND_FLOAT64 (7). Uint8Clamped (8), BigInt (9/10), Float16 + // (11), and the 0xFF sentinel all defer to the runtime. + let kind_ok = blk.icmp_ule(I64, &kind, "7"); + let idx_ge0 = blk.fcmp("oge", idx_d, "0.0"); + let idx_lt = blk.fcmp("olt", idx_d, "4294967296.0"); + let g = blk.and(I1, &is_ptr, &vg_zero); + let g = blk.and(I1, &g, &addr_match); + let g = blk.and(I1, &g, &kind_ok); + let g = blk.and(I1, &g, &idx_ge0); + blk.and(I1, &g, &idx_lt) + }; + ctx.block().cond_br(&entry_guard, &fast_label, &slow_label); + + // ---- fast: validate integer index + bounds -> store | slow ---- + ctx.current_block = fast_idx; + let (raw, idx_i64, kind) = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(obj_box); + let raw = blk.and(I64, &obj_bits, pointer_mask); + let slot = blk.lshr(I64, &raw, "3"); + let slot = blk.and(I64, &slot, "63"); + let entry_ptr = blk.gep( + "[64 x i64]", + "@PERRY_TA_KIND_CACHE", + &[(I64, "0"), (I64, &slot)], + ); + let entry_val = blk.load(I64, &entry_ptr); + let kind = blk.and(I64, &entry_val, "255"); + let idx_i64 = blk.fptosi(DOUBLE, idx_d, I64); + (raw, idx_i64, kind) + }; + let fast_ok = { + let blk = ctx.block(); + let idx_back = blk.sitofp(I64, &idx_i64, DOUBLE); + let is_int = blk.fcmp("oeq", &idx_back, idx_d); + let hdr_ptr = blk.inttoptr(I64, &raw); + let len = blk.load(I32, &hdr_ptr); + let len_i64 = blk.zext(I32, &len, I64); + let in_bounds = blk.icmp_ult(I64, &idx_i64, &len_i64); + blk.and(I1, &is_int, &in_bounds) + }; + ctx.block().cond_br(&fast_ok, &store_label, &slow_label); + + // ---- store: per-kind direct element store (data = header + 16) ---- + ctx.current_block = store_idx; + let data_base = { + let blk = ctx.block(); + blk.add(I64, &raw, "16") + }; + // ToInt32 of the value once (shared by all integer kinds). For float kinds + // we use the raw double directly. `toint32` matches the runtime + // `to_uint32_bits` (NaN/±Inf/±0 → 0, else trunc-toward-zero mod 2^32). + let val_i32 = ctx.block().toint32(val_double); + + let b_i8 = ctx.new_block("tav.s.i8"); + let b_u8 = ctx.new_block("tav.s.u8"); + let b_i16 = ctx.new_block("tav.s.i16"); + let b_u16 = ctx.new_block("tav.s.u16"); + let b_i32 = ctx.new_block("tav.s.i32"); + let b_u32 = ctx.new_block("tav.s.u32"); + let b_f32 = ctx.new_block("tav.s.f32"); + let b_f64 = ctx.new_block("tav.s.f64"); + let l_i8 = ctx.block_label(b_i8); + let l_u8 = ctx.block_label(b_u8); + let l_i16 = ctx.block_label(b_i16); + let l_u16 = ctx.block_label(b_u16); + let l_i32 = ctx.block_label(b_i32); + let l_u32 = ctx.block_label(b_u32); + let l_f32 = ctx.block_label(b_f32); + let l_f64 = ctx.block_label(b_f64); + + // Dispatch chain on `kind` (in the store block, after data_base/val_i32). + let chk = |ctx: &mut FnCtx<'_>, k: &str, hit: &str, next_idx: usize| { + let next_label = ctx.block_label(next_idx); + let cond = ctx.block().icmp_eq(I64, &kind, k); + ctx.block().cond_br(&cond, hit, &next_label); + }; + let c1 = ctx.new_block("tav.sd1"); + let c2 = ctx.new_block("tav.sd2"); + let c3 = ctx.new_block("tav.sd3"); + let c4 = ctx.new_block("tav.sd4"); + let c5 = ctx.new_block("tav.sd5"); + let c6 = ctx.new_block("tav.sd6"); + chk(ctx, "0", &l_i8, c1); + ctx.current_block = c1; + chk(ctx, "1", &l_u8, c2); + ctx.current_block = c2; + chk(ctx, "2", &l_i16, c3); + ctx.current_block = c3; + chk(ctx, "3", &l_u16, c4); + ctx.current_block = c4; + chk(ctx, "4", &l_i32, c5); + ctx.current_block = c5; + chk(ctx, "5", &l_u32, c6); + ctx.current_block = c6; + // remaining: kind 6 → f32, else (7) → f64. + let is_f32 = ctx.block().icmp_eq(I64, &kind, "6"); + ctx.block().cond_br(&is_f32, &l_f32, &l_f64); + + // Per-kind stores. Each: off = idx << shift; addr = data_base + off; + // store narrowed value; br merge. + emit_inline_ta_int_store( + ctx, + b_i8, + &idx_i64, + &data_base, + &merge_label, + "0", + &val_i32, + I8, + ); + emit_inline_ta_int_store( + ctx, + b_u8, + &idx_i64, + &data_base, + &merge_label, + "0", + &val_i32, + I8, + ); + emit_inline_ta_int_store( + ctx, + b_i16, + &idx_i64, + &data_base, + &merge_label, + "1", + &val_i32, + I16, + ); + emit_inline_ta_int_store( + ctx, + b_u16, + &idx_i64, + &data_base, + &merge_label, + "1", + &val_i32, + I16, + ); + emit_inline_ta_int_store( + ctx, + b_i32, + &idx_i64, + &data_base, + &merge_label, + "2", + &val_i32, + I32, + ); + emit_inline_ta_int_store( + ctx, + b_u32, + &idx_i64, + &data_base, + &merge_label, + "2", + &val_i32, + I32, + ); + // F32: fptrunc the double to float, store. + { + ctx.current_block = b_f32; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "2"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let f = blk.fptrunc(DOUBLE, val_double, F32); + blk.store(F32, &f, &ptr); + blk.br(&merge_label); + } + // F64: store the double raw. + { + ctx.current_block = b_f64; + let blk = ctx.block(); + let off = blk.shl(I64, &idx_i64, "3"); + let addr = blk.add(I64, &data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + blk.store(DOUBLE, val_double, &ptr); + blk.br(&merge_label); + } + + // ---- slow: the unchanged runtime setter ---- + ctx.current_block = slow_idx; + ctx.block().call( + DOUBLE, + "js_dyn_index_set", + &[(DOUBLE, obj_box), (DOUBLE, idx_d), (DOUBLE, val_double)], + ); + ctx.block().br(&merge_label); + + // ---- merge: assignment yields the stored value on every path ---- + ctx.current_block = merge_idx; + // All paths produce `val_double` as the expression result (matching + // `js_dyn_index_set`'s `return value`), so no phi is needed. + val_double.to_string() +} + +/// Emit one per-kind integer typed-array element store block for +/// [`lower_inline_dyn_typed_array_set`]: switches to `blk_idx`, computes the +/// element address (`data_base + (idx << shift)`), narrows the shared +/// ToInt32-coerced `val_i32` to `elem_ty`, stores it, and branches to +/// `merge_label`. +#[allow(clippy::too_many_arguments)] +fn emit_inline_ta_int_store( + ctx: &mut FnCtx<'_>, + blk_idx: usize, + idx_i64: &str, + data_base: &str, + merge_label: &str, + shift: &str, + val_i32: &str, + elem_ty: crate::types::LlvmType, +) { + ctx.current_block = blk_idx; + let blk = ctx.block(); + let off = blk.shl(I64, idx_i64, shift); + let addr = blk.add(I64, data_base, &off); + let ptr = blk.inttoptr(I64, &addr); + let narrowed = if elem_ty == I32 { + val_i32.to_string() + } else { + blk.trunc(I32, val_i32, elem_ty) + }; + blk.store(elem_ty, &narrowed, &ptr); + blk.br(merge_label); +} diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index 95d14511569..30edd1d359f 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -29,6 +29,16 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { // when it is non-zero (descriptors / typed-feedback in use). Defined in // perry-runtime as `PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED`. module.add_external_global("PERRY_CLASS_FIELD_INLINE_GUARD_DISABLED", I8); + // #5525 follow-up: the process-global typed-array kind cache + the + // "any exotic views live" guard, exported from perry-runtime so the codegen + // can emit a guarded *inline* typed-array element load at the access site + // (cache probe + bounds check + direct slot load) instead of an out-of-line + // `js_dyn_index_get` call. The cache is a fixed `[64 x i64]` array of + // `(addr << 8) | tag` words; the view guard is a single `i64` counter that + // reads 0 whenever every live typed array uses inline storage (so the + // inline `header + 16 + idx*elem_size` load matches the runtime `data_ptr`). + module.add_external_global("PERRY_TA_KIND_CACHE", "[64 x i64]"); + module.add_external_global("PERRY_TA_VIEW_GUARD", I64); module.declare_function("js_object_alloc", I64, &[I32, I32]); // #3149: `Object(value)` plain-call coercion. Takes & returns a NaN-boxed // JSValue (DOUBLE): nullish/primitive -> fresh {}, object passes through. diff --git a/crates/perry-runtime/src/native_arena.rs b/crates/perry-runtime/src/native_arena.rs index 7809acd9558..1b00248cbd9 100644 --- a/crates/perry-runtime/src/native_arena.rs +++ b/crates/perry-runtime/src/native_arena.rs @@ -119,6 +119,9 @@ static NATIVE_VIEW_COUNT: AtomicUsize = AtomicUsize::new(0); fn register_view(view: *mut NativeTypedViewHeader) { NATIVE_VIEW_COUNT.fetch_add(1, Ordering::Relaxed); + // #5525 follow-up: a native-arena view resolves its data pointer through the + // arena, not inline storage — bar the codegen inline element fast path. + typedarray::ta_view_guard_inc(); VIEW_REGISTRY.with(|r| { r.borrow_mut().insert(view as usize); }); @@ -129,6 +132,7 @@ fn unregister_view(view: *mut NativeTypedViewHeader) { let removed = VIEW_REGISTRY.with(|r| r.borrow_mut().remove(&(view as usize))); if removed { NATIVE_VIEW_COUNT.fetch_sub(1, Ordering::Relaxed); + typedarray::ta_view_guard_dec(); } typedarray::unregister_typed_array(view as *const TypedArrayHeader); } diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index 5a02408cb18..c8b96bb19e6 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -183,11 +183,46 @@ thread_local! { /// (`register`/`unregister`) overwrites/clears the matching slot below — so a /// freed-then-reused address can never read back a stale kind or a stale /// "not a typed array". -const TA_KIND_CACHE_SLOTS: usize = 64; -const TA_CACHE_NEGATIVE: u64 = 0xFF; -static TA_KIND_CACHE: [AtomicU64; TA_KIND_CACHE_SLOTS] = +pub const TA_KIND_CACHE_SLOTS: usize = 64; +pub const TA_CACHE_NEGATIVE: u64 = 0xFF; +// #5525 follow-up: exported under a stable link name so the codegen can emit a +// guarded *inline* typed-array element load/store at the access site (it reads a +// cache slot, checks the address tag + element kind, bounds-checks against the +// header `length`, and loads/stores the slot directly), bypassing the +// out-of-line `js_dyn_index_{get,set}` call + `lookup_typed_array_kind` + +// `js_number_coerce` on bcrypt's ~600M hot `S[i]`/`P[i]` Int32Array accesses. +// The inline reader observes exactly the same `(addr << 8) | tag` words this +// module maintains; cache misses / non-typed-array / exotic-key cases fall +// through to the existing runtime slow path, so semantics are unchanged. +#[no_mangle] +pub static PERRY_TA_KIND_CACHE: [AtomicU64; TA_KIND_CACHE_SLOTS] = [const { AtomicU64::new(0) }; TA_KIND_CACHE_SLOTS]; +/// #5525 follow-up: process-global "any exotic typed-array views exist" guard, +/// exported under a stable link name for the codegen inline element path. A +/// non-owning typed array (an `ArrayBuffer`-aliasing view, or a native-arena +/// view) resolves its element-0 pointer through a side table rather than +/// `header + size_of::()`, so the inline reader — which +/// assumes inline storage — MUST NOT fire while any such view is live. Both +/// view-registration paths (`typedarray_view::register_view_meta` and +/// `native_arena::register_view`) bump this; the matching unregister paths +/// decrement it. When it reads 0 (the overwhelmingly common case, and always +/// true for bcryptjs's owning `new Int32Array(P_ORIG)` boxes) the inline load +/// of `*(header + 16 + idx*elem_size)` is identical to what `data_ptr` + the +/// per-kind `load_at` slow path computes. +#[no_mangle] +pub static PERRY_TA_VIEW_GUARD: AtomicU64 = AtomicU64::new(0); + +#[inline] +pub(crate) fn ta_view_guard_inc() { + PERRY_TA_VIEW_GUARD.fetch_add(1, Ordering::Relaxed); +} + +#[inline] +pub(crate) fn ta_view_guard_dec() { + PERRY_TA_VIEW_GUARD.fetch_sub(1, Ordering::Relaxed); +} + #[inline] fn ta_kind_cache_slot(addr: usize) -> usize { // Addresses are 8-byte aligned; the low 3 bits are always 0. Use the bits @@ -199,7 +234,8 @@ fn ta_kind_cache_slot(addr: usize) -> usize { #[inline] fn ta_kind_cache_store_tag(addr: usize, tag: u64) { // `addr` is always > 0x10000, so `(addr << 8) | tag` is never 0 (= empty). - TA_KIND_CACHE[ta_kind_cache_slot(addr)].store(((addr as u64) << 8) | tag, Ordering::Relaxed); + PERRY_TA_KIND_CACHE[ta_kind_cache_slot(addr)] + .store(((addr as u64) << 8) | tag, Ordering::Relaxed); } #[inline] @@ -210,9 +246,9 @@ fn ta_kind_cache_store(addr: usize, kind: u8) { #[inline] fn ta_kind_cache_invalidate(addr: usize) { let slot = ta_kind_cache_slot(addr); - let entry = TA_KIND_CACHE[slot].load(Ordering::Relaxed); + let entry = PERRY_TA_KIND_CACHE[slot].load(Ordering::Relaxed); if entry != 0 && (entry >> 8) as usize == addr { - TA_KIND_CACHE[slot].store(0, Ordering::Relaxed); + PERRY_TA_KIND_CACHE[slot].store(0, Ordering::Relaxed); } } @@ -220,7 +256,7 @@ fn ta_kind_cache_invalidate(addr: usize) { /// negative ("not a typed array"), `Some(Some(kind))` = cached typed array. #[inline] fn ta_kind_cache_get(addr: usize) -> Option> { - let entry = TA_KIND_CACHE[ta_kind_cache_slot(addr)].load(Ordering::Relaxed); + let entry = PERRY_TA_KIND_CACHE[ta_kind_cache_slot(addr)].load(Ordering::Relaxed); if entry != 0 && (entry >> 8) as usize == addr { let tag = entry & 0xff; if tag == TA_CACHE_NEGATIVE { diff --git a/crates/perry-runtime/src/typedarray_view.rs b/crates/perry-runtime/src/typedarray_view.rs index 8e6afa9c5c8..ef5c84d3f0c 100644 --- a/crates/perry-runtime/src/typedarray_view.rs +++ b/crates/perry-runtime/src/typedarray_view.rs @@ -198,6 +198,10 @@ pub(crate) fn register_view_meta(ta: *const TypedArrayHeader, backing: usize, by ); if prev.is_none() { VIEW_META_COUNT.fetch_add(1, Ordering::Relaxed); + // #5525 follow-up: this typed array now aliases an ArrayBuffer, so + // its element-0 pointer no longer follows the header inline — bar + // the codegen inline element fast path until it's gone. + crate::typedarray::ta_view_guard_inc(); } }); } @@ -228,6 +232,7 @@ pub(crate) fn clear_view_meta(addr: usize) { TYPED_ARRAY_VIEW_META.with(|r| { if r.borrow_mut().remove(&addr).is_some() { VIEW_META_COUNT.fetch_sub(1, Ordering::Relaxed); + crate::typedarray::ta_view_guard_dec(); } }); } diff --git a/crates/perry/tests/issue_5525_typed_array_untyped_index.rs b/crates/perry/tests/issue_5525_typed_array_untyped_index.rs index 99289ef99c9..9d469f175e7 100644 --- a/crates/perry/tests/issue_5525_typed_array_untyped_index.rs +++ b/crates/perry/tests/issue_5525_typed_array_untyped_index.rs @@ -215,3 +215,103 @@ console.log("E=" + add(2, 3) + "," + add("x", 5) + "," + add(1, "y") + "," + add and the dynamic `+` fast path" ); } + +/// #5525 follow-up #2: the unknown-receiver element read/write is now lowered as +/// a guarded **inline** typed-array load/store at the access site (cache probe + +/// bounds check + direct slot access), with the runtime `js_dyn_index_{get,set}` +/// kept as the fall-back for any guard miss. This pins that the inline path is +/// behaviourally identical to the slow path across: every inlined element kind +/// (Int8…Float64, signed/unsigned/float), in-bounds round-trips, the OOB / +/// negative / fractional / NaN index deferrals, the kinds the inline guard +/// excludes (Uint8Clamped clamp, BigInt, Float16), and the `PERRY_TA_VIEW_GUARD` +/// case where a live ArrayBuffer-backed view bars the inline path so *all* +/// typed-array accesses (even on owning arrays) take the slow path yet stay +/// correct. +#[test] +fn inline_typed_array_fast_path_matches_slow_path() { + let dir = tempfile::tempdir().expect("tempdir"); + let entry = dir.path().join("main.ts"); + let output = dir.path().join("main_bin"); + + std::fs::write( + &entry, + r#" +function g(a: any, i: any): any { return a[i]; } +function s(a: any, i: any, v: any): void { a[i] = v; } + +// (A) every inlined kind round-trips a representative value, incl. signed/ +// unsigned narrowing wraps and float precision. +const i8 = new Int8Array(1); s(i8, 0, 200); // -> -56 +const u8 = new Uint8Array(1); s(u8, 0, 300); // -> 44 +const i16 = new Int16Array(1); s(i16, 0, -1); +const u16 = new Uint16Array(1); s(u16, 0, 70000); // -> 4464 +const i32 = new Int32Array(1); s(i32, 0, 4294967297); // -> 1 +const u32 = new Uint32Array(1); s(u32, 0, -1); // -> 4294967295 +const f32 = new Float32Array(1); s(f32, 0, 1.5); +const f64 = new Float64Array(1); s(f64, 0, 3.14159); +console.log("A=" + g(i8,0) + "," + g(u8,0) + "," + g(i16,0) + "," + g(u16,0) + + "," + g(i32,0) + "," + g(u32,0) + "," + g(f32,0) + "," + g(f64,0)); + +// (B) index deferrals: OOB / negative / NaN must NOT take the inline load (the +// inline guard rejects them and defers to the runtime). The fractional case +// (`g(t,1.5)`) is included to prove the inline guard *defers* it to the slow +// path — but the assertion mirrors the runtime slow path's existing behaviour +// (a pre-existing quirk: the untyped dynamic getter truncates a fractional +// index rather than returning undefined, identical on clean origin/main), +// since the whole point is that inline == slow. +const t = new Int32Array(3); s(t, 0, 10); s(t, 1, 20); s(t, 2, 30); +console.log("B=" + (g(t,3) === undefined) + "," + (g(t,-1) === undefined) + + "," + (g(t,1.5) === g(t,1)) + "," + (g(t,NaN) === undefined)); + +// (C) inline-excluded kinds defer correctly: Uint8Clamped clamps, BigInt boxes. +const c = new Uint8ClampedArray(2); s(c, 0, 300); s(c, 1, -5); +const b = new BigInt64Array(1); s(b, 0, 5n); +console.log("C=" + g(c,0) + "," + g(c,1) + "," + (typeof g(b,0)) + "," + g(b,0)); + +// (D) PERRY_TA_VIEW_GUARD: once an ArrayBuffer-backed view is live, the inline +// path must be barred for ALL typed arrays (owning included) and every access +// must still be correct via the slow path. +const own = new Int32Array(2); s(own, 0, 111); s(own, 1, 222); +const ab = new ArrayBuffer(8); +const v = new Int32Array(ab); // bumps the view guard +s(v, 0, 333); s(v, 1, 444); +console.log("D=" + g(own,0) + "," + g(own,1) + "," + g(v,0) + "," + g(v,1)); +"#, + ) + .expect("write entry"); + + let compile = Command::new(perry_bin()) + .current_dir(dir.path()) + .arg("compile") + .arg(&entry) + .arg("-o") + .arg(&output) + .output() + .expect("run perry compile"); + assert!( + compile.status.success(), + "perry compile failed\nstdout:\n{}\nstderr:\n{}", + String::from_utf8_lossy(&compile.stdout), + String::from_utf8_lossy(&compile.stderr) + ); + + let run = Command::new(&output).output().expect("run compiled binary"); + assert!( + run.status.success(), + "compiled binary failed\nstatus: {:?}\nstdout:\n{}\nstderr:\n{}", + run.status, + String::from_utf8_lossy(&run.stdout), + String::from_utf8_lossy(&run.stderr) + ); + let stdout = String::from_utf8_lossy(&run.stdout); + assert_eq!( + stdout, + "A=-56,44,-1,4464,1,4294967295,1.5,3.14159\n\ + B=true,true,true,true\n\ + C=255,0,bigint,5\n\ + D=111,222,333,444\n", + "the inline typed-array fast path must be bit-identical to the runtime \ + slow path across kinds, index deferrals, excluded kinds, and the \ + view-guard fallback" + ); +}