diff --git a/changelog.d/6883-inline-ta-param-numeric-read.md b/changelog.d/6883-inline-ta-param-numeric-read.md new file mode 100644 index 0000000000..1acc2a9331 --- /dev/null +++ b/changelog.d/6883-inline-ta-param-numeric-read.md @@ -0,0 +1,12 @@ +perf(codegen): inline checked-f64 typed-array-param reads in numeric context + +A typed-array element read through a parameter that feeds arithmetic (`n += S[i]`, +the bcryptjs `_encipher` S-box shape) now lowers to an inline checked f64 load — +the same guard/bounds machinery as the checked-i32 read, widened to f64 and +bit-exact with `js_typed_array_get` (the `TAG_UNDEFINED` double on OOB) — instead +of a per-read runtime call. Guard misses defer to a new memory-safe +`js_typed_array_read_f64` helper. Gated on a proven non-negative integer index; +covers every numeric kind incl. Uint32 (unsigned widening) and the float kinds. +Env flag `PERRY_TA_PARAM_F64_READ` (default on). Measured ~1.32× on the real +`_encipher` shape (1787ms → 1351ms, byte-exact); stacks with the #6860 non-BigInt +inline-bitwise fast path. diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index 9251185e4e..cbfde109ba 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -1439,6 +1439,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { return Ok(result); } + // Numeric-context read of a typed-array PARAM (e.g. bcryptjs + // `_encipher`'s `n = S[l >>> 24]; n += S[...]`): an inline checked + // f64 load that is bit-exact with `js_typed_array_get` (numeric + // element in-bounds, `TAG_UNDEFINED` OOB), replacing the per-read + // runtime call. Gated on a proven integer index; guard misses + // (view/detached/wrong-kind) defer to the memory-safe helper. + if let Some(value) = + super::ta_param_f64_read::try_lower_ta_param_f64_read(ctx, object, index)? + { + return Ok(value); + } + // Width-aware typed-array native lowering is only sound for // tracked fresh views with proven/guarded element bounds. All // aliases, reassigned locals, and unknown bounds stay on the diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 1187a4c2e8..d41e0cdaba 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1403,6 +1403,7 @@ mod env_clones; mod fs_await; mod index_get; mod masked_window; +mod ta_param_f64_read; pub(crate) use index_get::packed_f64_loop_index_parts; pub(crate) use masked_window::masked_window_fact_for_index; mod index_set; diff --git a/crates/perry-codegen/src/expr/ta_param_f64_read.rs b/crates/perry-codegen/src/expr/ta_param_f64_read.rs new file mode 100644 index 0000000000..cff3f3f46d --- /dev/null +++ b/crates/perry-codegen/src/expr/ta_param_f64_read.rs @@ -0,0 +1,267 @@ +//! Inline **checked f64** typed-array element read for a typed-array *parameter* +//! consumed in numeric (non-`| 0`) context. +//! +//! Motivating shape — `bcryptjs`'s `_encipher` (its own profiled bottleneck, +//! ~90% of a cost-N hash): the Blowfish S-box lookups +//! `n = S[l >>> 24]; n += S[0x100 | ((l >> 16) & 0xff)]; …` read `Int32Array` +//! *parameters* and feed the element into `+`/`+=`, i.e. an f64 numeric context. +//! The i32 fast path (`i32_fast_path.rs`) only fires when the read is in a +//! `ToInt32` (`| 0`) context, so these reads fell back to a per-element +//! `call double @js_typed_array_get` runtime call — measured ~26× slower than V8, +//! which compiles the same read to a single bounds-checked load. +//! +//! The runtime getter already returns `double` (the numeric element in-bounds, +//! the `TAG_UNDEFINED` double OOB / negative), so an inline load that reproduces +//! **exactly** those two return values is a bit-exact drop-in — no consumer +//! analysis, no string-vs-number disambiguation, no OOB-semantics divergence. +//! That is what this module emits: the guard/bounds machinery of the checked i32 +//! load, but widening the element to f64 and merging in the `TAG_UNDEFINED` +//! double (not `0`) on OOB. Guard misses defer to the memory-safe +//! `js_typed_array_read_f64` cold helper. + +use anyhow::Result; +use perry_hir::Expr; + +use super::index_get::numeric_index_has_integer_array_index_proof; +use super::{lower_expr, lower_expr_as_i32, FnCtx}; +use crate::nanbox::{double_literal, i64_literal, TAG_UNDEFINED}; +use crate::native_value::{BoundsState, BufferAccessMode, LoweredValue}; +use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8}; + +/// How a loaded element widens into the f64 result. +#[derive(Clone, Copy)] +enum F64Conv { + /// Signed integer element (I8/I16/I32) → `sitofp`. + SInt, + /// Unsigned integer element (U8/U8Clamped/U16/U32) → `uitofp`. + UInt, + /// `Float32` element → `fpext`. + F32, + /// `Float64` element → direct load, no conversion. + F64, +} + +/// Numeric element kind of a statically-typed typed-array receiver eligible for +/// the inline **checked f64** element read: `(kind_tag, elem_llvm_ty, +/// elem_size_bytes, conv)`. Covers *every* numeric kind — unlike the i32 sibling +/// (`checked_typed_array_i32_kind`), which stops at the i32-representable integer +/// kinds — because an f64 result represents `Uint32Array` and the float kinds +/// exactly. `None` for the BigInt kinds (BigInt64/BigUint64 are BigInt, not +/// Number) and any non-typed-array / non-local receiver. +/// +/// `kind_tag` values MUST match `perry-runtime` `KIND_*` +/// (`typedarray/mod.rs`): the runtime `PERRY_TA_KIND_CACHE` stores `kind as u64`, +/// and the entry guard compares against this tag — a mismatch would merely miss +/// the cache and route every read to the slow helper (correct, but no speedup). +fn checked_typed_array_f64_kind( + ctx: &FnCtx<'_>, + object: &Expr, +) -> Option<(u8, crate::types::LlvmType, u32, F64Conv)> { + if ctx.disable_buffer_fast_path { + return None; + } + // Plain local/param read so the receiver is re-fetched at every access + // (reassignment / capture stay correct — the emission caches nothing). + let Expr::LocalGet(id) = object else { + return None; + }; + // A tracked buffer view owns this receiver via its own (stronger-bounds) + // native path; don't shadow it. + if ctx.buffer_view_slots.contains_key(id) { + return None; + } + match crate::type_analysis::receiver_class_name(ctx, object).as_deref()? { + "Int8Array" => Some((0, I8, 1, F64Conv::SInt)), + "Uint8Array" => Some((1, I8, 1, F64Conv::UInt)), + "Uint8ClampedArray" => Some((8, I8, 1, F64Conv::UInt)), + "Int16Array" => Some((2, I16, 2, F64Conv::SInt)), + "Uint16Array" => Some((3, I16, 2, F64Conv::UInt)), + "Int32Array" => Some((4, I32, 4, F64Conv::SInt)), + "Uint32Array" => Some((5, I32, 4, F64Conv::UInt)), + "Float32Array" => Some((6, F32, 4, F64Conv::F32)), + "Float64Array" => Some((7, DOUBLE, 8, F64Conv::F64)), + _ => None, + } +} + +/// Compile-time gate (bisection): unset / `1` / `on` / `true` enable; `0` / +/// `off` / `false` disable. Object-cache keys every codegen env var, so a +/// flipped value re-codegens rather than serving a stale cache. +fn ta_param_f64_read_enabled() -> bool { + match std::env::var("PERRY_TA_PARAM_F64_READ") { + Ok(v) => !matches!(v.as_str(), "0" | "off" | "false" | "OFF" | "FALSE"), + Err(_) => true, + } +} + +/// If `object[index]` is a numeric-context read of a typed-array parameter with +/// a proven non-negative integer index, emit the inline checked f64 load and +/// return its DOUBLE SSA value; otherwise `Ok(None)` so the caller keeps its +/// existing `js_typed_array_get` fallback. Records CheckedNative access-mode +/// evidence for the buffer-facts artifact, mirroring the slow-path sibling. +pub(crate) fn try_lower_ta_param_f64_read( + ctx: &mut FnCtx<'_>, + object: &Expr, + index: &Expr, +) -> Result> { + if !ta_param_f64_read_enabled() { + return Ok(None); + } + // Fractional / unproven indices must stay on the runtime getter: the inline + // path lowers `index` via ToInt32 (`fptosi`), so `S[3.9]` would read element + // 3, but JS reads a fractional typed-array index as `undefined`. Require the + // same proven-integer index the i32 read paths use. + if !numeric_index_has_integer_array_index_proof(ctx, index) { + return Ok(None); + } + let Some((kind, elem_ty, elem_size, conv)) = checked_typed_array_f64_kind(ctx, object) else { + return Ok(None); + }; + let value = + lower_checked_typed_array_f64_load(ctx, object, index, kind, elem_ty, elem_size, conv)?; + let lowered = LoweredValue::js_value(value.clone()); + ctx.record_lowered_value_with_access_mode( + "TypedArrayGet", + None, + "TypedArrayGet.checked_f64_param", + &lowered, + Some(BoundsState::Unknown), + None, + Some(BufferAccessMode::CheckedNative), + Some(super::buffer_views::buffer_access_materialization_reason( + ctx, object, + )), + false, + false, + vec!["typed_array_param_f64=checked_inline".to_string()], + ); + Ok(Some(value)) +} + +/// Emit the checked inline f64 element load. Same runtime-fact guard and header +/// bounds check as [`super::i32_fast_path`]'s `lower_checked_typed_array_i32_load` +/// (pointer + inline-storage `PERRY_TA_VIEW_GUARD == 0` + kind-cache addr/kind), +/// but the load arm widens the element to f64, the OOB arm merges in the +/// `TAG_UNDEFINED` double, and guard misses defer to `js_typed_array_read_f64`. +#[allow(clippy::too_many_arguments)] +fn lower_checked_typed_array_f64_load( + ctx: &mut FnCtx<'_>, + object: &Expr, + index: &Expr, + kind: u8, + elem_ty: crate::types::LlvmType, + elem_size: u32, + conv: F64Conv, +) -> Result { + let obj_box = lower_expr(ctx, object)?; + let idx_i32 = lower_expr_as_i32(ctx, index)?; + + let chk_idx = ctx.new_block("ctaf.get.chk"); + let load_idx = ctx.new_block("ctaf.get.load"); + let oob_idx = ctx.new_block("ctaf.get.oob"); + let slow_idx = ctx.new_block("ctaf.get.slow"); + let merge_idx = ctx.new_block("ctaf.get.merge"); + let chk_label = ctx.block_label(chk_idx); + let load_label = ctx.block_label(load_idx); + let oob_label = ctx.block_label(oob_idx); + let slow_label = ctx.block_label(slow_idx); + let merge_label = ctx.block_label(merge_idx); + + let tag_mask = i64_literal(crate::nanbox::TAG_MASK); + + // ---- entry guard: pointer + inline-storage + kind-cache addr/kind ---- + let raw = { + let blk = ctx.block(); + let obj_bits = blk.bitcast_double_to_i64(&obj_box); + let raw = blk.and(I64, &obj_bits, crate::nanbox::POINTER_MASK_I64); + let tagged = blk.and(I64, &obj_bits, &tag_mask); + let is_ptr = blk.icmp_eq(I64, &tagged, crate::nanbox::POINTER_TAG_I64); + 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); // also rejects empty slot 0 + let kind_bits = blk.and(I64, &entry_val, "255"); + let kind_ok = blk.icmp_eq(I64, &kind_bits, &kind.to_string()); + 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); + blk.cond_br(&g, &chk_label, &slow_label); + raw + }; + + // ---- chk: bounds check against header length (u32 at offset 0) ---- + ctx.current_block = chk_idx; + { + let blk = ctx.block(); + let hdr_ptr = blk.inttoptr(I64, &raw); + let len = blk.load(I32, &hdr_ptr); + // `ult` also rejects a negative index (wraps huge unsigned) — JS `S[-1]` + // is undefined; the oob arm merges `TAG_UNDEFINED`. + let in_bounds = blk.icmp_ult(I32, &idx_i32, &len); + blk.cond_br(&in_bounds, &load_label, &oob_label); + } + + // ---- load: bare per-kind element load (data base = raw + 16) → f64 ---- + ctx.current_block = load_idx; + let (load_val, load_end) = { + let blk = ctx.block(); + let data_base = blk.add(I64, &raw, "16"); + let idx_i64 = blk.zext(I32, &idx_i32, I64); + let shift = elem_size.trailing_zeros().to_string(); + 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 = match conv { + F64Conv::F64 => raw_elem, + F64Conv::F32 => blk.fpext(F32, &raw_elem, DOUBLE), + F64Conv::SInt => blk.sitofp(elem_ty, &raw_elem, DOUBLE), + F64Conv::UInt => blk.uitofp(elem_ty, &raw_elem, DOUBLE), + }; + let end = blk.label.clone(); + blk.br(&merge_label); + (val, end) + }; + + // ---- oob: in-kind out-of-bounds -> TAG_UNDEFINED (== js_typed_array_get) -- + ctx.current_block = oob_idx; + let (oob_val, oob_end) = { + let blk = ctx.block(); + let end = blk.label.clone(); + blk.br(&merge_label); + (double_literal(f64::from_bits(TAG_UNDEFINED)), end) + }; + + // ---- slow: view / detached / wrong-kind / non-TA -> memory-safe helper --- + ctx.current_block = slow_idx; + let (slow_val, slow_end) = { + let blk = ctx.block(); + let v = blk.call( + DOUBLE, + "js_typed_array_read_f64", + &[(I64, &raw), (I32, &idx_i32)], + ); + let end = blk.label.clone(); + blk.br(&merge_label); + (v, end) + }; + + // ---- merge ---- + ctx.current_block = merge_idx; + Ok(ctx.block().phi( + DOUBLE, + &[ + (load_val.as_str(), load_end.as_str()), + (oob_val.as_str(), oob_end.as_str()), + (slow_val.as_str(), slow_end.as_str()), + ], + )) +} diff --git a/crates/perry-codegen/src/runtime_decls/strings_part2.rs b/crates/perry-codegen/src/runtime_decls/strings_part2.rs index 8e8a26894d..02e97422de 100644 --- a/crates/perry-codegen/src/runtime_decls/strings_part2.rs +++ b/crates/perry-codegen/src/runtime_decls/strings_part2.rs @@ -105,6 +105,10 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) { // Cold fallback for the inline checked-i32 typed-array element read // (returns ToInt32 of the element, or 0 for OOB / view / wrong-kind). module.declare_function("js_typed_array_read_int32", I32, &[I64, I32]); + // Cold fallback for the inline checked-f64 typed-array element read (numeric + // context): numeric element in-bounds, TAG_UNDEFINED double for OOB / view / + // wrong-kind — bit-exact with js_typed_array_get. + module.declare_function("js_typed_array_read_f64", DOUBLE, &[I64, I32]); // #2063: string / dynamic-key `ta[key]` [[Get]] dispatcher (canonical // numeric index → element, else ordinary named-property [[Get]]). module.declare_function("js_typed_array_index_get_dynamic", DOUBLE, &[I64, DOUBLE]); diff --git a/crates/perry-runtime/src/typedarray/access.rs b/crates/perry-runtime/src/typedarray/access.rs index f1e1aca2f3..08149b8f63 100644 --- a/crates/perry-runtime/src/typedarray/access.rs +++ b/crates/perry-runtime/src/typedarray/access.rs @@ -91,6 +91,39 @@ pub extern "C" fn js_typed_array_read_int32(ta: *const TypedArrayHeader, index: static KEEP_JS_TYPED_ARRAY_READ_INT32: extern "C" fn(*const TypedArrayHeader, i32) -> i32 = js_typed_array_read_int32; +/// Cold fallback for the codegen inline **checked f64** typed-array element read +/// (a typed-array parameter read in *numeric* context — `bcryptjs`'s +/// `_encipher` S-box reads `n = S[l >>> 24]; n += S[...]`, where the element +/// flows into `+`/`+=` rather than `| 0`). The inline path +/// (`perry-codegen/src/expr/ta_param_f64_read.rs`) serves the common +/// inline-storage, correct-kind case with a bare native load widened to f64, and +/// yields the `TAG_UNDEFINED` double directly for a genuine out-of-bounds read — +/// **bit-exact** with [`js_typed_array_get`] (numeric element in-bounds, +/// `TAG_UNDEFINED` OOB), so the fast path is a pure call→load swap needing no +/// consumer-context analysis. It routes here only on a guard miss +/// (view/detached/resizable backing, kind-cache miss, or a receiver that is not +/// the statically-expected kind). +/// +/// Memory safety mirrors [`js_typed_array_read_int32`]: a kind-cache miss can be +/// entered with a receiver that is not a typed array at all (TS types are +/// erased), so validate the raw pointer is a registered typed array before any +/// header deref — a non-typed-array receiver has no element and reads +/// `undefined` (`TAG_UNDEFINED`). Otherwise defer to the full ECMAScript +/// `[[Get]]`. +#[no_mangle] +pub extern "C" fn js_typed_array_read_f64(ta: *const TypedArrayHeader, index: i32) -> f64 { + let ta = clean_ta_ptr(ta); + if ta.is_null() || lookup_typed_array_kind(ta as usize).is_none() { + return f64::from_bits(crate::value::TAG_UNDEFINED); + } + js_typed_array_get(ta, index) +} + +// Codegen-only export (see the i32 sibling above): pin under whole-program LTO. +#[used] +static KEEP_JS_TYPED_ARRAY_READ_F64: extern "C" fn(*const TypedArrayHeader, i32) -> f64 = + js_typed_array_read_f64; + /// #2063 — dynamic / string-key `[[Get]]` on a TypedArray (`ta[key]`). /// /// The codegen element-read fast path only fires for statically-proven diff --git a/crates/perry/src/commands/compile/object_cache.rs b/crates/perry/src/commands/compile/object_cache.rs index 42637ad8b4..185854f8c4 100644 --- a/crates/perry/src/commands/compile/object_cache.rs +++ b/crates/perry/src/commands/compile/object_cache.rs @@ -939,6 +939,15 @@ fn compute_object_cache_key_with_env( .as_deref() .unwrap_or(""), ); + // Inline checked-f64 typed-array-param read: `=0`/`off`/`false` reverts a + // numeric-context typed-array parameter read (`n += S[i]`) from the inline + // checked load back to the `js_typed_array_get` runtime call, which changes + // the emitted IR / .o bytes — a warm cache must not serve an object built + // under the other setting. + h.field( + "env_ta_param_f64_read", + env_var("PERRY_TA_PARAM_F64_READ").as_deref().unwrap_or(""), + ); h.finish() } diff --git a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs index a27863f941..fd01f108ec 100644 --- a/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs +++ b/crates/perry/src/commands/compile/object_cache/object_cache_tests.rs @@ -611,6 +611,8 @@ fn key_changes_with_codegen_env_vars() { "PERRY_INLINE_HOT_SMALL_MAX_SITES", // Non-BigInt inline bitwise fast path. "PERRY_INLINE_NONBIGINT_BITWISE", + // Inline checked-f64 typed-array-param read. + "PERRY_TA_PARAM_F64_READ", ] { // Sample state without the var, with the var, and with a different // value — all three keys must be distinct. diff --git a/test-files/test_gap_ta_param_numeric_read.ts b/test-files/test_gap_ta_param_numeric_read.ts new file mode 100644 index 0000000000..abb052f069 --- /dev/null +++ b/test-files/test_gap_ta_param_numeric_read.ts @@ -0,0 +1,132 @@ +// Reading a typed-array element through a *parameter* in NUMERIC (arithmetic) +// context — `n += S[i]`, not `(a ^ S[i]) | 0`. perry-codegen +// expr/ta_param_f64_read.rs lowers such a read to an inline checked f64 load +// (guard: pointer + inline-storage PERRY_TA_VIEW_GUARD + kind-cache; header +// bounds check; bare load widened to f64; OOB/negative -> the TAG_UNDEFINED +// double == js_typed_array_get; slow fallback js_typed_array_read_f64 for +// view/detached/wrong-kind). Bit-exact drop-in for the runtime getter, so every +// line must match `node --experimental-strip-types` exactly. This is the +// bcryptjs `_encipher` shape (`n = S[l>>>24]; n += S[...]`). + +// ---- additive reads, one per numeric kind, in a loop ---- +function sumI8(S: Int8Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumU8(S: Uint8Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumU8C(S: Uint8ClampedArray, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumI16(S: Int16Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumU16(S: Uint16Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumI32(S: Int32Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumU32(S: Uint32Array, n: number): number { + // U32 must widen UNSIGNED: 0xffffffff -> 4294967295, not -1. + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 7]; + return s; +} +function sumF32(S: Float32Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 3]; + return s; +} +function sumF64(S: Float64Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 3]; + return s; +} + +const i8 = Int8Array.from([-5, 100, -128, 7, 127, -1, 42, 99]); +const u8 = Uint8Array.from([1, 200, 255, 7, 128, 0, 42, 99]); +const u8c = Uint8ClampedArray.from([1, 200, 255, 7, 128, 0, 42, 99]); +const i16 = Int16Array.from([-5, 30000, -32768, 7, 32767, -1, 42, 999]); +const u16 = Uint16Array.from([1, 60000, 65535, 7, 32768, 0, 42, 999]); +const i32 = Int32Array.from([-5, 100000, -2000000000, 7, 0x7fffffff, -1, 42, 999]); +const u32 = Uint32Array.from([1, 4000000000, 0xffffffff, 7, 0x80000000, 0, 42, 999]); +const f32 = Float32Array.from([0.5, -1.5, 2.5, 100.25]); +const f64 = Float64Array.from([1.5, 2.25, -3.75, 100.125]); + +console.log("i8", sumI8(i8, 8)); +console.log("u8", sumU8(u8, 8)); +console.log("u8c", sumU8C(u8c, 8)); +console.log("i16", sumI16(i16, 8)); +console.log("u16", sumU16(u16, 8)); +console.log("i32", sumI32(i32, 8)); +console.log("u32", sumU32(u32, 8)); // exercises unsigned widening +console.log("f32", sumF32(f32, 4)); +console.log("f64", sumF64(f64, 4)); + +// ---- the bcryptjs `_encipher` shape: `n = S[..]; n += S[..]` with masked ---- +// ---- (always in-bounds) integer indices from bitwise ops ---- +function feistel(S: Int32Array, x: number): number { + let n = S[(x >>> 24) & 7]; + n += S[(x >> 16) & 7]; + n ^= S[(x >> 8) & 7]; + n += S[x & 7]; + return n | 0; +} +console.log("feistel", feistel(i32, 0x12345678)); + +// ---- an in-bounds `1000 + S[i]` (numeric add, real element) ---- +function readAdd(S: Int32Array, i: number): number { + return 1000 + S[i]; +} +console.log("inb", readAdd(i32, 3)); // 1000 + 7 + +// ---- OOB / negative / fractional reads observed in SAFE contexts (the read +// itself yields `undefined`; we avoid `+` here because a separate, pre-existing +// codegen issue mishandles `number + ` — tracked apart +// from this fast path, which is bit-exact with the runtime getter). ---- +function eqUndef(S: Int32Array, i: number): boolean { + return S[i] === undefined; +} +function strOf(S: Int32Array, i: number): string { + return String(S[i]); +} +console.log("oob-eq", eqUndef(i32, 8), eqUndef(i32, -1), eqUndef(i32, 3)); // true true false +console.log("oob-str", strOf(i32, 8), strOf(i32, -1), strOf(i32, 3)); // undefined undefined 7 +// Fractional index reads `undefined` (must NOT round to element 3 via ToInt32). +console.log("frac-eq", eqUndef(i32, 3.9), eqUndef(i32, 3)); // true false + +// ---- view over an ArrayBuffer (non-inline storage -> slow fallback) ---- +function viewSum(S: Int32Array, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i]; + return s; +} +const ab = new ArrayBuffer(16); +const view = new Int32Array(ab); +view[0] = 111; +view[1] = -222; +view[2] = 333; +view[3] = -444; +console.log("view", viewSum(view, 4)); // -222 + +// ---- detached buffer: an in-bounds read pre-detach, `=== undefined` post ---- +const ab2 = new ArrayBuffer(16); +const det = new Int32Array(ab2); +det[0] = 7; +det[1] = 9; +console.log("predetach", viewSum(det, 2)); +ab2.transfer(); // detach +console.log("postdetach", det[0] === undefined, det[1] === undefined); // true true