Skip to content
9 changes: 9 additions & 0 deletions changelog.d/6794-untyped-param-masked-window.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
perf(codegen): masked-window read hoist for loop-invariant array params of unknown static type, plus straight-line region versioning (#6794; follow-up to #6750)
Comment thread
coderabbitai[bot] marked this conversation as resolved.

#6750's masked-index fast paths only fired when the array's static type proved a numeric array, so an array arriving as an untyped (`any`) function parameter — the bcryptjs Blowfish S-box shape, and the common shape across real npm code — kept paying one guard call per access (~40× slower than Node on `S[i & 1023]` loops).

- **Dense range-loop tiers for untyped bindings**: the read-only dense matcher also admits bindings with no usable static type (the entry guards re-validate the actual runtime value; a wrong hint costs one failed guard → slow loop). New O(1) typed-array probe tiers (`js_typed_feedback_masked_window_ta_kind` + preheader data-pointer hoist) give Int32Array / Uint32Array / Float64Array receivers width-correct bare inline loads; untyped plain Arrays version through the existing plain tiers. `MaskedWindowArrayFact` now carries a `MaskedWindowElem` storage kind.
- **Masked-window region versioning** (`stmt/masked_window_region.rs`): bcryptjs ships `_encipher` fully unrolled — ~130 consecutive masked reads with no loop — so a maximal straight-line run of scalar statements with ≥8 static-window reads gets the same probe → fast-copy/slow-copy treatment.
- **Fast copies made real for untyped locals**: unknown-receiver IndexGet routes consult masked-window facts before the per-access inline-TA/`js_dyn_index_get` paths; `is_numeric_expr` and the shadow-value classifier recognize fact-covered reads; region-local flow refinement types untyped locals as `Number` once every prior write is provably numeric (killing `js_dynamic_*` dispatch calls), with shadow-slot suppression and, outside `try`, privatization into non-escaping allocas so LLVM promotes the region to registers.

Measured: untyped-param Int32Array `S[i & 1023]` 20M-read loop 535 ms → 3 ms, untyped-param plain Array 934 ms → 5 ms (Node: 15/21 ms); real `bcryptjs.compareSync` (cost 10) 3.68 s → 1.03 s per op (5.6 s before the #6750 series; Node 71 ms). Statically-typed loops and pure-arithmetic loops unchanged. New gap test `test_gap_untyped_param_masked_window.ts` covers the deopt matrix (OOB → `undefined`, holey/mixed plain arrays, unsupported TA kinds, detached views, heterogeneous multi-array loops, mid-loop/mid-region rebinding, polymorphic call sites, mid-region throw in `try`).
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/closure.rs
Original file line number Diff line number Diff line change
Expand Up @@ -841,6 +841,7 @@ pub(super) fn compile_closure(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/entry.rs
Original file line number Diff line number Diff line change
Expand Up @@ -754,6 +754,7 @@ pub(super) fn compile_module_entry(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down Expand Up @@ -1353,6 +1354,7 @@ pub(super) fn compile_module_entry(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/codegen/function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,6 +549,7 @@ pub(super) fn compile_function(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/codegen/method.rs
Original file line number Diff line number Diff line change
Expand Up @@ -467,6 +467,7 @@ pub(super) fn compile_method(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down Expand Up @@ -1463,6 +1464,7 @@ pub(super) fn compile_static_method(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
i32_counter_slots: HashMap::new(),
i1_local_slots: HashMap::new(),
Expand Down
28 changes: 28 additions & 0 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1068,6 +1068,17 @@ pub(crate) fn lower_unknown_local_index_get_for_number_context(
let Expr::LocalGet(id) = object.as_ref() else {
return Ok(None);
};
// #6750 follow-up: an active masked-window fact wins over the guarded
// inline-TA probe — the fact's entry guard already proved storage + the
// whole index window, so the read needs no per-access cache probe at all.
if let Some(fact) = super::masked_window::masked_window_fact_for_index(ctx, *id, index.as_ref())
{
let arr_box = lower_expr(ctx, object)?;
let idx_i32 = lower_expr_as_i32(ctx, index)?;
return Ok(Some(super::masked_window::lower_masked_window_index_get(
ctx, *id, &arr_box, &idx_i32, &fact,
)));
}
let recv_unknown = matches!(
crate::type_analysis::static_type_of(ctx, object),
None | Some(HirType::Any) | Some(HirType::Unknown)
Expand Down Expand Up @@ -1529,6 +1540,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &s_handle), (DOUBLE, &idx_d)],
));
}
// #6750 follow-up: a masked-window fact (dense range-loop or
// straight-line region fast copy) covering this access means the
// entry guard already proved the receiver's storage layout and
// the whole static index window — the read is a bare inline load
// even though the STATIC type is erased (`any` parameter). Must
// run before the unknown-receiver `js_dyn_index_get` route below.
if let Expr::LocalGet(arr_id) = object.as_ref() {
if let Some(fact) =
super::masked_window::masked_window_fact_for_index(ctx, *arr_id, index.as_ref())
{
let arr_box = lower_expr(ctx, object)?;
let idx_i32 = lower_expr_as_i32(ctx, index)?;
return Ok(super::masked_window::lower_masked_window_index_get(
ctx, *arr_id, &arr_box, &idx_i32, &fact,
));
}
}
// Issue #514: when the receiver's static type is genuinely
// unknown (`Type::Any` / `Type::Unknown`) and the index is
// numeric, route through the runtime tag-aware dispatcher.
Expand Down
162 changes: 134 additions & 28 deletions crates/perry-codegen/src/expr/masked_window.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,10 +13,15 @@ use anyhow::Result;
use perry_hir::Expr;

use crate::nanbox::POINTER_MASK_I64;
use crate::native_value::{BoundsState, BufferAccessMode, LoweredValue, NativeRep, SemanticKind};
use crate::native_value::{
BoundsState, BufferAccessMode, LoweredValue, NativeFactUse, NativeRep, SemanticKind,
};
use crate::types::{DOUBLE, I32, I64};

use super::{lower_expr, lower_expr_as_i32, raw_f64_layout_fact, FnCtx, MaskedWindowArrayFact};
use super::{
array_kind_fact, lower_expr, lower_expr_as_i32, raw_f64_layout_fact, FnCtx,
MaskedWindowArrayFact, MaskedWindowElem,
};

/// Look up an active masked-window fact for `(arr, index-expr)`: the index's
/// static value window (`collectors::static_index_window` — the same function
Expand All @@ -38,7 +43,7 @@ pub(crate) fn masked_window_fact_for_index(
.cloned()
}

/// Emit the raw in-window f64 element load shared by both tiers:
/// Emit the raw in-window f64 element load of the plain-array tiers:
/// `header + 8 + idx * 8` on the pointer-masked array handle.
fn emit_raw_window_load(ctx: &mut FnCtx<'_>, arr_box: &str, idx_i32: &str) -> String {
let blk = ctx.block();
Expand All @@ -52,24 +57,114 @@ fn emit_raw_window_load(ctx: &mut FnCtx<'_>, arr_box: &str, idx_i32: &str) -> St
blk.load(DOUBLE, &element_ptr)
}

/// Emit the raw in-window element load for a masked-window fact: the dense
/// range guard already proved a plain raw-f64 numeric array with every slot
/// in `[min_idx, max_idx_exclusive)` an in-bounds number (no holes), so the
/// load is a bare f64 read — no guard call, no hole check, no side exit.
/// Emit the raw in-window typed-array element load of the TA tiers:
/// `data_ptr + idx << shift`, where `data_ptr` is the element-0 address the
/// preheader probe hoisted (stable — the fast copy is call-free).
fn emit_ta_window_load(
ctx: &mut FnCtx<'_>,
data_ptr: &str,
idx_i32: &str,
shift: &str,
elem_ty: crate::types::LlvmType,
) -> String {
let blk = ctx.block();
let idx_i64 = blk.zext(I32, idx_i32, I64);
let byte_offset = blk.shl(I64, &idx_i64, shift);
let element_addr = blk.add(I64, data_ptr, &byte_offset);
let element_ptr = blk.inttoptr(I64, &element_addr);
blk.load(elem_ty, &element_ptr)
}

/// Emit the in-window element load for `fact`, materialized as a DOUBLE
/// (number semantics): plain raw-f64 and Float64Array slots load directly;
/// Int32Array loads sign-extend (`sitofp`), Uint32Array loads are UNSIGNED
/// (`uitofp` — elements may exceed `i32::MAX`).
fn emit_window_load_f64(
ctx: &mut FnCtx<'_>,
arr_box: &str,
idx_i32: &str,
fact: &MaskedWindowArrayFact,
) -> String {
match &fact.elem {
MaskedWindowElem::PlainF64 => emit_raw_window_load(ctx, arr_box, idx_i32),
MaskedWindowElem::TaI32 { data_ptr } => {
let data_ptr = data_ptr.clone();
let raw = emit_ta_window_load(ctx, &data_ptr, idx_i32, "2", I32);
ctx.block().sitofp(I32, &raw, DOUBLE)
}
MaskedWindowElem::TaU32 { data_ptr } => {
let data_ptr = data_ptr.clone();
let raw = emit_ta_window_load(ctx, &data_ptr, idx_i32, "2", I32);
ctx.block().uitofp(I32, &raw, DOUBLE)
}
MaskedWindowElem::TaF64 { data_ptr } => {
let data_ptr = data_ptr.clone();
emit_ta_window_load(ctx, &data_ptr, idx_i32, "3", DOUBLE)
}
}
}

/// Storage-layout audit facts + note for `fact`'s tier.
fn window_layout_facts(fact: &MaskedWindowArrayFact, arr_id: u32) -> (Vec<NativeFactUse>, String) {
match &fact.elem {
MaskedWindowElem::PlainF64 => (
vec![raw_f64_layout_fact(
Some(arr_id),
"consumed",
&fact.guard_id,
None,
)],
"storage_layout=raw_f64_numeric_slots".to_string(),
),
MaskedWindowElem::TaI32 { .. } => (
vec![array_kind_fact(
Some(arr_id),
"consumed",
&fact.guard_id,
None,
)],
"storage_layout=typed_array_i32_slots".to_string(),
),
MaskedWindowElem::TaU32 { .. } => (
vec![array_kind_fact(
Some(arr_id),
"consumed",
&fact.guard_id,
None,
)],
"storage_layout=typed_array_u32_slots".to_string(),
),
MaskedWindowElem::TaF64 { .. } => (
vec![array_kind_fact(
Some(arr_id),
"consumed",
&fact.guard_id,
None,
)],
"storage_layout=typed_array_f64_slots".to_string(),
),
}
}

/// Emit the in-window element load for a masked-window fact: the entry guard
/// already proved a numeric array with every slot in
/// `[min_idx, max_idx_exclusive)` an in-bounds number (no holes), so the load
/// is a bare width-correct read — no guard call, no hole check, no side exit.
pub(crate) fn lower_masked_window_index_get(
ctx: &mut FnCtx<'_>,
arr_id: u32,
arr_box: &str,
idx_i32: &str,
fact: &MaskedWindowArrayFact,
) -> String {
let value = emit_raw_window_load(ctx, arr_box, idx_i32);
let value = emit_window_load_f64(ctx, arr_box, idx_i32, fact);
let lowered = LoweredValue {
semantic: SemanticKind::JsNumber,
rep: NativeRep::F64,
llvm_ty: DOUBLE,
value: value.clone(),
};
let (layout_facts, layout_note) = window_layout_facts(fact, arr_id);
ctx.record_lowered_value_with_access_mode_and_facts(
"NumericArrayIndexGet",
Some(arr_id),
Expand All @@ -83,19 +178,14 @@ pub(crate) fn lower_masked_window_index_get(
None,
None,
None,
vec![raw_f64_layout_fact(
Some(arr_id),
"consumed",
&fact.guard_id,
None,
)],
layout_facts,
Vec::new(),
false,
false,
vec![
"index_range=static_window_guarded".to_string(),
"length_range=guarded_i32".to_string(),
"storage_layout=raw_f64_numeric_slots".to_string(),
layout_note,
],
);
value
Expand All @@ -115,9 +205,12 @@ pub(crate) fn masked_window_i32_load_is_provable(
masked_window_fact_for_index(ctx, *arr_id, index).is_some_and(|fact| fact.values_i32)
}

/// i32-tier masked-window load: raw in-window f64 element load + bare
/// `fptosi` (exact — the dense-i32 guard proved the value is an i32 integer).
/// Returns `None` when no i32-tier fact covers the access.
/// i32-tier masked-window load. Plain tier: raw in-window f64 element load +
/// bare `fptosi` (exact — the dense-i32 guard proved the value is an i32
/// integer). Int32Array tier: a direct `load i32` from the hoisted data
/// pointer — no float round-trip at all. Returns `None` when no i32-tier
/// fact covers the access (`values_i32` is never set for the Uint32Array /
/// Float64Array tiers, whose elements are not i32-representable).
pub(crate) fn lower_masked_window_index_get_i32(
ctx: &mut FnCtx<'_>,
object: &Expr,
Expand All @@ -133,14 +226,32 @@ pub(crate) fn lower_masked_window_index_get_i32(
};
let arr_box = lower_expr(ctx, object)?;
let idx_i32 = lower_expr_as_i32(ctx, index)?;
let raw_f64 = emit_raw_window_load(ctx, &arr_box, &idx_i32);
let value = ctx.block().fptosi(DOUBLE, &raw_f64, I32);
let (value, materialization_note) = match &fact.elem {
MaskedWindowElem::PlainF64 => {
let raw_f64 = emit_raw_window_load(ctx, &arr_box, &idx_i32);
(
ctx.block().fptosi(DOUBLE, &raw_f64, I32),
"integer_materialization=fptosi_guarded_dense_i32",
)
}
MaskedWindowElem::TaI32 { data_ptr } => {
let data_ptr = data_ptr.clone();
(
emit_ta_window_load(ctx, &data_ptr, &idx_i32, "2", I32),
"integer_materialization=direct_i32_load_ta",
)
}
MaskedWindowElem::TaU32 { .. } | MaskedWindowElem::TaF64 { .. } => {
unreachable!("values_i32 fact with non-i32 element kind")
}
};
let lowered = LoweredValue {
semantic: SemanticKind::JsNumber,
rep: NativeRep::I32,
llvm_ty: I32,
value: value.clone(),
};
let (layout_facts, layout_note) = window_layout_facts(&fact, *arr_id);
ctx.record_lowered_value_with_access_mode_and_facts(
"NumericArrayIndexGet",
Some(*arr_id),
Expand All @@ -154,20 +265,15 @@ pub(crate) fn lower_masked_window_index_get_i32(
None,
None,
None,
vec![raw_f64_layout_fact(
Some(*arr_id),
"consumed",
&fact.guard_id,
None,
)],
layout_facts,
Vec::new(),
false,
false,
vec![
"index_range=static_window_guarded".to_string(),
"length_range=guarded_i32".to_string(),
"storage_layout=raw_f64_numeric_slots".to_string(),
"integer_materialization=fptosi_guarded_dense_i32".to_string(),
layout_note,
materialization_note.to_string(),
],
);
Ok(Some(value))
Expand Down
Loading
Loading