Skip to content
Merged
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
12 changes: 12 additions & 0 deletions changelog.d/6883-inline-ta-param-numeric-read.md
Original file line number Diff line number Diff line change
@@ -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.
12 changes: 12 additions & 0 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1439,6 +1439,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
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
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
267 changes: 267 additions & 0 deletions crates/perry-codegen/src/expr/ta_param_f64_read.rs
Original file line number Diff line number Diff line change
@@ -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<Option<String>> {
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<String> {
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()),
],
))
}
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings_part2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]);
Expand Down
33 changes: 33 additions & 0 deletions crates/perry-runtime/src/typedarray/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions crates/perry/src/commands/compile/object_cache.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
Loading
Loading