diff --git a/changelog.d/6794-untyped-param-masked-window.md b/changelog.d/6794-untyped-param-masked-window.md new file mode 100644 index 0000000000..bd224bcde2 --- /dev/null +++ b/changelog.d/6794-untyped-param-masked-window.md @@ -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) + +#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`). diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 111b5b62e4..60b6f31115 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -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(), diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 133f506045..6d74b68974 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -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(), @@ -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(), diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 9b2fb8b773..28c3368f15 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -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(), diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 9f2e51bfaf..adce436b1e 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -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(), @@ -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(), diff --git a/crates/perry-codegen/src/expr/index_get.rs b/crates/perry-codegen/src/expr/index_get.rs index e034146767..e59cc1193e 100644 --- a/crates/perry-codegen/src/expr/index_get.rs +++ b/crates/perry-codegen/src/expr/index_get.rs @@ -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) @@ -1529,6 +1540,23 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { &[(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. diff --git a/crates/perry-codegen/src/expr/masked_window.rs b/crates/perry-codegen/src/expr/masked_window.rs index 0c2998f866..69064fc29d 100644 --- a/crates/perry-codegen/src/expr/masked_window.rs +++ b/crates/perry-codegen/src/expr/masked_window.rs @@ -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 @@ -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(); @@ -52,10 +57,99 @@ 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, 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, @@ -63,13 +157,14 @@ pub(crate) fn lower_masked_window_index_get( 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), @@ -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 @@ -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, @@ -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), @@ -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)) diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 35ebb05eb6..152dd75073 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -663,6 +663,11 @@ pub(crate) struct FnCtx<'a> { /// `i` in bounds. pub packed_f64_loop_facts: Vec, pub masked_window_array_facts: Vec, + /// #6750 follow-up: locals currently flow-refined to Number inside a + /// masked-window region fast copy — their shadow slots were cleared at + /// the refinement point and per-statement shadow updates are suppressed + /// until the refinement is dropped (`expr::shadow_slot`). + pub masked_region_scalar_locals: std::collections::HashSet, /// #5093: scoped loop-versioning facts for monomorphic class-field loops. /// Pushed only around the FAST clone of `lower_class_field_versioned_for` @@ -1201,13 +1206,35 @@ pub(crate) struct PackedF64LoopFact { pub window_validated: bool, } +/// Element storage a masked-window fact's entry guard proved (#6750 +/// follow-up). The plain tier keeps deriving the slot address from the boxed +/// array handle; the typed-array tiers load through the data pointer the +/// preheader probe hoisted (`js_typed_array_masked_window_data_ptr` — stable +/// for the whole call-free fast copy). +#[derive(Debug, Clone, PartialEq)] +pub(crate) enum MaskedWindowElem { + /// Plain array with raw-f64 numeric slots: `handle + 8 + idx * 8`. + PlainF64, + /// Int32Array: `load i32` at `data_ptr + idx * 4`; every element is an + /// exact i32 by construction. + TaI32 { data_ptr: String }, + /// Uint32Array: `load i32` at `data_ptr + idx * 4`, materialized + /// UNSIGNED (`uitofp`) — elements may exceed `i32::MAX`, so the fact + /// never sets `values_i32`. + TaU32 { data_ptr: String }, + /// Float64Array: `load double` at `data_ptr + idx * 8`. + TaF64 { data_ptr: String }, +} + /// Read-only masked-index window fact for the dense packed-f64 range loop: -/// the entry guard (`js_typed_feedback_packed_f64_range_loop_guard_dense`) -/// proved `array_local_id` is a plain raw-f64 numeric array whose +/// the entry guard (`js_typed_feedback_packed_f64_range_loop_guard_dense` +/// for the plain tiers, `js_typed_feedback_masked_window_ta_kind` for the +/// typed-array tiers) proved `array_local_id` is a numeric array whose /// `[min_idx, max_idx_exclusive)` slots are all in-bounds numbers (no holes). /// Any read whose index has a static value window inside this range (e.g. /// `S[x & 1023]`, `S[256 + ((x >>> 16) & 0xff)]` — see -/// `collectors::static_index_window`) lowers to a bare raw-f64 element load. +/// `collectors::static_index_window`) lowers to a bare element load whose +/// width/signedness follows `elem`. #[derive(Debug, Clone)] pub(crate) struct MaskedWindowArrayFact { pub array_local_id: u32, @@ -1215,10 +1242,13 @@ pub(crate) struct MaskedWindowArrayFact { pub guard_id: String, pub min_idx: i64, pub max_idx_exclusive: i64, - /// True in the i32-tier fast copy: the guard additionally proved every - /// window slot holds an i32-representable integer, so loads may - /// materialize elements as `i32` with a bare exact `fptosi`. + /// True in the i32-tier fast copies: the guard proved every window slot + /// holds an i32-representable integer (plain dense-i32 tier), or the + /// element type is exactly i32 (Int32Array tier), so loads may + /// materialize elements as native `i32`. pub values_i32: bool, + /// Storage layout the guard proved — selects the inline load shape. + pub elem: MaskedWindowElem, } /// #5093: one fact per (receiver, versioned loop). See @@ -1338,6 +1368,7 @@ mod fs_await; mod index_get; mod masked_window; pub(crate) use index_get::packed_f64_loop_index_parts; +pub(crate) use masked_window::masked_window_fact_for_index; mod index_set; mod instance_misc1; pub(crate) use instance_misc1::builtin_parent_reserved_class_id; diff --git a/crates/perry-codegen/src/expr/shadow_slot.rs b/crates/perry-codegen/src/expr/shadow_slot.rs index 8f74509de1..6045041616 100644 --- a/crates/perry-codegen/src/expr/shadow_slot.rs +++ b/crates/perry-codegen/src/expr/shadow_slot.rs @@ -33,6 +33,14 @@ pub(crate) fn expr_is_known_non_pointer_shadow_value(ctx: &FnCtx<'_>, expr: &Exp Expr::Compare { .. } | Expr::Void(_) => true, Expr::Unary { .. } => true, Expr::Binary { op, .. } => !matches!(op, BinaryOp::Add), + // #6750 follow-up: a masked-index read covered by an ACTIVE + // masked-window fact is a guard-proven numeric element load — never + // a pointer — even when the receiver's static type is erased. + Expr::IndexGet { object, index } => matches!( + object.as_ref(), + Expr::LocalGet(arr_id) + if super::masked_window_fact_for_index(ctx, *arr_id, index).is_some() + ), Expr::Conditional { then_expr, else_expr, @@ -74,6 +82,14 @@ pub(crate) fn emit_shadow_slot_update_for_expr( value_reg: &str, rhs: &Expr, ) { + // #6750 follow-up: inside a masked-window region fast copy, a local + // flow-refined to Number had its slot cleared at the refinement point + // and every subsequent region write stores a proven number — no + // per-statement shadow traffic needed until the refinement is dropped + // (see `stmt::masked_window_region`). + if ctx.masked_region_scalar_locals.contains(&local_id) { + return; + } let Some(slot_idx) = ctx.shadow_slot_map.get(&local_id).copied() else { return; }; diff --git a/crates/perry-codegen/src/expr/typed_feedback.rs b/crates/perry-codegen/src/expr/typed_feedback.rs index 0e7f0d86e1..146c6bf517 100644 --- a/crates/perry-codegen/src/expr/typed_feedback.rs +++ b/crates/perry-codegen/src/expr/typed_feedback.rs @@ -86,6 +86,12 @@ impl TypedFeedbackContract { Self::new("packed_f64_array_loop_guard", "generic_jsvalue_loop") } + // #6750 follow-up: masked-window typed-array tier probe for untyped + // loop-invariant array bindings. + pub(crate) const fn masked_window_ta_probe() -> Self { + Self::new("masked_window_ta_kind_probe", "generic_jsvalue_loop") + } + pub(crate) const fn packed_i32_array_loop() -> Self { Self::new("packed_i32_array_loop_guard", "generic_jsvalue_loop") } diff --git a/crates/perry-codegen/src/runtime_decls/objects.rs b/crates/perry-codegen/src/runtime_decls/objects.rs index aae5ae91fa..179190e53c 100644 --- a/crates/perry-codegen/src/runtime_decls/objects.rs +++ b/crates/perry-codegen/src/runtime_decls/objects.rs @@ -354,6 +354,15 @@ pub fn declare_phase_b_objects(module: &mut LlModule) { I32, &[I64, DOUBLE, I32, I32], ); + // #6750 follow-up: masked-window typed-array tier probe (returns the + // MASKED_WINDOW_TA_KIND_* code) and the preheader element-0 data-pointer + // hoist consumed by the guarded typed-array fast copies. + module.declare_function( + "js_typed_feedback_masked_window_ta_kind", + I32, + &[I64, DOUBLE, I32, I32], + ); + module.declare_function("js_typed_array_masked_window_data_ptr", I64, &[DOUBLE]); module.declare_function( "js_typed_feedback_packed_u32_array_loop_guard", I32, diff --git a/crates/perry-codegen/src/stmt/loops.rs b/crates/perry-codegen/src/stmt/loops.rs index 9ed7179a50..9412f62506 100644 --- a/crates/perry-codegen/src/stmt/loops.rs +++ b/crates/perry-codegen/src/stmt/loops.rs @@ -410,16 +410,16 @@ enum PackedF64RangeLoopBound { } #[derive(Clone, Copy)] -struct PackedF64RangeArrayAccess { - array_id: u32, +pub(super) struct PackedF64RangeArrayAccess { + pub(super) array_id: u32, /// Counter-relative accesses: smallest / largest constant offset `c` over /// all `arr[i ± c]` accesses. - counter: Option<(i32, i32)>, + pub(super) counter: Option<(i32, i32)>, /// Merged static index windows `(lo, hi)` over masked accesses /// (`arr[e & K]`, `arr[K1 + (e >>> k & K2)]`, … — see /// `collectors::static_index_window`). Dense mode only. - stat: Option<(i64, i64)>, - written: bool, + pub(super) stat: Option<(i64, i64)>, + pub(super) written: bool, } struct PackedF64RangeLoop { @@ -615,7 +615,16 @@ fn match_packed_f64_range_loop( { return None; } - } else if !local_is_number_array(ctx, arr_id) { + } else if !local_is_number_array(ctx, arr_id) + && !(dense && local_is_untyped_candidate(ctx, arr_id)) + { + // #6750 follow-up: read-only DENSE accesses also admit bindings + // with no usable static type (`any` function parameters — the + // bcryptjs S-box shape). The entry guards/probes re-validate the + // ACTUAL runtime value, so a wrong hint costs one failed guard → + // slow loop, never correctness. Known non-array static types stay + // excluded so ordinary object/string index loops don't grow dead + // guard chains. return None; } } @@ -869,7 +878,7 @@ fn packed_f64_range_loop_dense_body_collect( /// with an unrecognized receiver/index shape bails the whole match. /// `allow_static` (dense mode) additionally admits reads whose index carries a /// static value window (`a[e & K]`, `a[K1 + (e >>> k & K2)]`, …). -fn packed_f64_range_loop_pure_expr_collect( +pub(super) fn packed_f64_range_loop_pure_expr_collect( expr: &perry_hir::Expr, counter_id: u32, allow_static: bool, @@ -1080,9 +1089,125 @@ fn push_packed_f64_range_facts( min_idx: lo, max_idx_exclusive: hi + 1, values_i32, + elem: crate::expr::MaskedWindowElem::PlainF64, + }); + } + } +} + +/// #6750 follow-up: one `js_typed_feedback_masked_window_ta_kind` probe call +/// per accessed array (O(1) each: registry lookup + length compare). Returns +/// the first array's kind code plus an i1 "every array probed to the same +/// code" (None for a single array). The caller branches into the matching +/// typed-array fast copy only when all arrays agree on a non-NONE code — +/// heterogeneous mixes fall through to the plain-array guard tiers. +fn emit_masked_window_ta_probes( + ctx: &mut FnCtx<'_>, + matched: &PackedF64RangeLoop, +) -> Result<(String, Option)> { + let mut first_kind: Option = None; + let mut all_same: Option = None; + for access in &matched.arrays { + let arr_box = lower_expr(ctx, &perry_hir::Expr::LocalGet(access.array_id))?; + let feedback_site_id = emit_typed_feedback_register_site( + ctx, + TypedFeedbackKind::ArrayElement, + "array[masked_window_ta_probe]", + TypedFeedbackContract::masked_window_ta_probe(), + ); + let (lo, hi) = access + .stat + .expect("TA tier probes require static-window accesses"); + let min_idx = lo.to_string(); + let max_idx = (hi + 1).to_string(); + let kind = ctx.block().call( + I32, + "js_typed_feedback_masked_window_ta_kind", + &[ + (I64, &feedback_site_id), + (DOUBLE, &arr_box), + (I32, &min_idx), + (I32, &max_idx), + ], + ); + match &first_kind { + None => first_kind = Some(kind), + Some(first) => { + let first = first.clone(); + let same = ctx.block().icmp_eq(I32, &kind, &first); + all_same = Some(match all_same.take() { + None => same, + Some(prev) => ctx.block().and(I1, &prev, &same), }); + } } } + Ok(( + first_kind.expect("range loop matcher requires >= 1 array"), + all_same, + )) +} + +/// Lower one masked-window typed-array fast copy: hoist each array's element-0 +/// data pointer (`js_typed_array_masked_window_data_ptr` — stable for the +/// call-free copy), push per-array facts carrying the tier's element kind, and +/// emit the loop with the shared i32 bound. +#[allow(clippy::too_many_arguments)] +fn lower_masked_window_ta_tier( + ctx: &mut FnCtx<'_>, + matched: &PackedF64RangeLoop, + init: Option<&Stmt>, + condition: Option<&perry_hir::Expr>, + update: Option<&perry_hir::Expr>, + body: &[Stmt], + guard_id: &str, + loop_label: &str, + values_i32: bool, + make_elem: fn(String) -> crate::expr::MaskedWindowElem, + bound_i32: &str, + merge_label: &str, +) -> Result<()> { + let mut hoisted: Vec<(u32, crate::expr::MaskedWindowElem)> = Vec::new(); + for access in &matched.arrays { + let arr_box = lower_expr(ctx, &perry_hir::Expr::LocalGet(access.array_id))?; + let data_ptr = ctx.block().call( + I64, + "js_typed_array_masked_window_data_ptr", + &[(DOUBLE, &arr_box)], + ); + hoisted.push((access.array_id, make_elem(data_ptr))); + } + let scope_id = ctx.next_loop_proof_scope_id(); + for (access, (arr_id, elem)) in matched.arrays.iter().zip(hoisted) { + let (lo, hi) = access + .stat + .expect("TA tiers require static-window accesses"); + ctx.masked_window_array_facts + .push(crate::expr::MaskedWindowArrayFact { + array_local_id: arr_id, + scope_id, + guard_id: guard_id.to_string(), + min_idx: lo, + max_idx_exclusive: hi + 1, + values_i32, + elem, + }); + } + lower_for_after_init_with_i32_bound( + ctx, + init, + condition, + update, + body, + loop_label, + Some((matched.counter_id, bound_i32.to_string())), + )?; + ctx.masked_window_array_facts + .retain(|fact| fact.scope_id != scope_id); + if !ctx.block().is_terminated() { + ctx.block().br(merge_label); + } + Ok(()) } fn lower_packed_f64_range_versioned_for( @@ -1200,6 +1325,110 @@ fn lower_packed_f64_range_versioned_for( }; if matched.dense { + // #6750 follow-up: typed-array tiers ahead of the plain-array guard + // chain, for loops whose accessed bindings include at least one with + // no usable static type (an `any` parameter — the bcryptjs shape; a + // declared `number[]` loop keeps exactly the previous tier chain and + // never pays a probe). One O(1) probe per array classifies the actual + // runtime receiver; when every array agrees on Int32Array / Uint32Array + // / Float64Array the matching fast copy loads elements inline through + // the hoisted data pointer (width-correct, no per-access call). Any + // disagreement or non-TA receiver falls through to the plain tiers, + // whose runtime guards reject typed arrays. Counter-offset accesses + // keep assuming plain raw-f64 storage, so the TA tiers require every + // access to carry a static window. + let ta_tiers_apply = matched + .arrays + .iter() + .all(|access| access.counter.is_none() && access.stat.is_some()) + && matched + .arrays + .iter() + .any(|access| !local_is_number_array(ctx, access.array_id)); + if ta_tiers_apply { + let ta_i32_pre_idx = ctx.new_block("packed_f64_range.loop.ta_i32.preheader"); + let ta_u32_pre_idx = ctx.new_block("packed_f64_range.loop.ta_u32.preheader"); + let ta_f64_pre_idx = ctx.new_block("packed_f64_range.loop.ta_f64.preheader"); + let ta_try_u32_idx = ctx.new_block("packed_f64_range.ta.try_u32"); + let ta_try_f64_idx = ctx.new_block("packed_f64_range.ta.try_f64"); + let ta_plain_idx = ctx.new_block("packed_f64_range.ta.plain"); + let ta_i32_pre_label = ctx.block_label(ta_i32_pre_idx); + let ta_u32_pre_label = ctx.block_label(ta_u32_pre_idx); + let ta_f64_pre_label = ctx.block_label(ta_f64_pre_idx); + let ta_try_u32_label = ctx.block_label(ta_try_u32_idx); + let ta_try_f64_label = ctx.block_label(ta_try_f64_idx); + let ta_plain_label = ctx.block_label(ta_plain_idx); + + let (kind0, all_same) = emit_masked_window_ta_probes(ctx, &matched)?; + // Kind codes: keep in sync with MASKED_WINDOW_TA_KIND_* in + // perry-runtime/src/typed_feedback.rs. + let tier_select = |ctx: &mut FnCtx<'_>, code: &str| { + let is_code = ctx.block().icmp_eq(I32, &kind0, code); + match &all_same { + Some(same) => ctx.block().and(I1, same, &is_code), + None => is_code, + } + }; + let is_i32 = tier_select(ctx, "1"); + ctx.block() + .cond_br(&is_i32, &ta_i32_pre_label, &ta_try_u32_label); + ctx.current_block = ta_try_u32_idx; + let is_u32 = tier_select(ctx, "2"); + ctx.block() + .cond_br(&is_u32, &ta_u32_pre_label, &ta_try_f64_label); + ctx.current_block = ta_try_f64_idx; + let is_f64 = tier_select(ctx, "3"); + ctx.block() + .cond_br(&is_f64, &ta_f64_pre_label, &ta_plain_label); + + ctx.current_block = ta_i32_pre_idx; + lower_masked_window_ta_tier( + ctx, + &matched, + init, + condition, + update, + body, + "masked_window_ta_i32", + "for.packed_f64_range_fast_ta_i32", + true, + |data_ptr| crate::expr::MaskedWindowElem::TaI32 { data_ptr }, + &bound_i32, + &merge_label, + )?; + ctx.current_block = ta_u32_pre_idx; + lower_masked_window_ta_tier( + ctx, + &matched, + init, + condition, + update, + body, + "masked_window_ta_u32", + "for.packed_f64_range_fast_ta_u32", + false, + |data_ptr| crate::expr::MaskedWindowElem::TaU32 { data_ptr }, + &bound_i32, + &merge_label, + )?; + ctx.current_block = ta_f64_pre_idx; + lower_masked_window_ta_tier( + ctx, + &matched, + init, + condition, + update, + body, + "masked_window_ta_f64", + "for.packed_f64_range_fast_ta_f64", + false, + |data_ptr| crate::expr::MaskedWindowElem::TaF64 { data_ptr }, + &bound_i32, + &merge_label, + )?; + ctx.current_block = ta_plain_idx; + } + // Read-only dense mode: two guard tiers. The i32 tier additionally // proves every window value is an i32-representable integer, so its // fast copy materializes loads with a bare exact `fptosi` (bit-mixing @@ -2141,7 +2370,10 @@ fn packed_loop_array_binding_is_eligible(ctx: &FnCtx<'_>, arr_id: u32) -> bool { /// The storage half of [`packed_loop_array_binding_is_eligible`]: the binding /// read is a plain load (stack alloca or `@perry_global_*`), not a capture /// slot or box. -fn packed_loop_array_binding_storage_is_addressable(ctx: &FnCtx<'_>, arr_id: u32) -> bool { +pub(super) fn packed_loop_array_binding_storage_is_addressable( + ctx: &FnCtx<'_>, + arr_id: u32, +) -> bool { if ctx.closure_captures.contains_key(&arr_id) { false } else if ctx.locals.contains_key(&arr_id) { @@ -2151,7 +2383,7 @@ fn packed_loop_array_binding_storage_is_addressable(ctx: &FnCtx<'_>, arr_id: u32 } } -fn local_is_number_array(ctx: &FnCtx<'_>, local_id: u32) -> bool { +pub(super) fn local_is_number_array(ctx: &FnCtx<'_>, local_id: u32) -> bool { matches!( local_array_element_type(ctx, local_id), Some(perry_types::Type::Number | perry_types::Type::Int32) @@ -2161,6 +2393,22 @@ fn local_is_number_array(ctx: &FnCtx<'_>, local_id: u32) -> bool { ) } +/// #6750 follow-up: a binding whose static type gives the compiler nothing to +/// key on — an `any`/`unknown` function parameter (the bcryptjs S-box shape) +/// or a local with no recorded type at all. These are candidates for the +/// runtime-probed dense masked-window tiers: the loop-entry probes/guards +/// classify the ACTUAL runtime value (typed array kind, plain raw-f64 +/// packedness, window bounds), so the missing static type only means we must +/// version the loop instead of proving anything at compile time. Known +/// non-array static types (string, object, declared non-number arrays) stay +/// ineligible — their guard chains would be dead weight. +pub(super) fn local_is_untyped_candidate(ctx: &FnCtx<'_>, local_id: u32) -> bool { + matches!( + ctx.local_types.get(&local_id), + None | Some(perry_types::Type::Any | perry_types::Type::Unknown) + ) +} + fn local_allows_packed_f64_loop_store(ctx: &FnCtx<'_>, local_id: u32) -> bool { matches!( local_array_element_type(ctx, local_id), diff --git a/crates/perry-codegen/src/stmt/masked_window_region.rs b/crates/perry-codegen/src/stmt/masked_window_region.rs new file mode 100644 index 0000000000..c9ac3e7bfd --- /dev/null +++ b/crates/perry-codegen/src/stmt/masked_window_region.rs @@ -0,0 +1,706 @@ +//! #6750 follow-up: masked-window versioning for STRAIGHT-LINE statement runs. +//! +//! The dense range-loop tiers (`loops.rs`) hoist per-access array-read guards +//! to the loop preheader — but bcryptjs ships `_encipher` fully UNROLLED: 16 +//! Feistel rounds of `S[l >>> 24]` / `S[0x100 | ((l >> 16) & 0xff)]` / +//! `P[k]` reads as ~130 consecutive scalar statements with no loop to +//! version. This module applies the same speculation to a maximal run of +//! region-safe statements: probe the accessed arrays once at region entry, +//! branch into a fast copy whose masked reads are bare inline loads (via the +//! shared [`MaskedWindowArrayFact`] machinery), or fall through to the +//! ordinary per-access lowering. +//! +//! A region-safe statement is `Stmt::Expr` of a scalar `LocalSet` / `Update` +//! / pure expression — the same effect-free walk the dense loop matcher uses +//! (`packed_f64_range_loop_pure_expr_collect`): no calls, closures, awaits, +//! stores, or `Stmt::Let` (a Let lowered once per copy would leave post-region +//! reads pointing at only the last copy's alloca). Reads on ineligible +//! receivers (dynamic indices like `lr[off]`, non-array bindings) don't stop +//! the region — they simply lower per-access in every copy. An array binding +//! REASSIGNED inside the region is dropped from the eligible set, so its +//! reads keep full JS semantics. +//! +//! Tier chain (each copy duplicates the region, so only the two tiers that +//! matter are emitted — rarer shapes keep the per-access path): +//! 1. `ta_i32` — every eligible array probes as an Int32Array whose length +//! covers the merged window (`js_typed_feedback_masked_window_ta_kind`, +//! O(1)); loads are `load i32` through the hoisted data pointer. +//! 2. `plain_f64` — every eligible array passes the dense plain-array +//! window guard (O(1) once the RawF64 layout flag is set; the dense-i32 +//! plain tier is deliberately NOT emitted here — its per-entry window +//! scan is O(window), which a hot small function would pay on every +//! call). +//! 3. slow — the untouched per-access lowering. +//! +//! Safety mirrors the dense-loop fast copies: the fast copies' statements +//! cannot write memory (no stores/calls admitted), typed-array storage never +//! moves and view backings are thread-lifetime allocations, and plain-array +//! loads re-derive the element base from the binding's slot at every access, +//! so a GC triggered by an allocating scalar op (string concat) cannot leave +//! a stale pointer behind. + +use anyhow::Result; +use perry_hir::{Expr, Stmt}; + +use super::loops::{ + local_is_number_array, local_is_untyped_candidate, packed_f64_range_loop_pure_expr_collect, + packed_loop_array_binding_storage_is_addressable, PackedF64RangeArrayAccess, +}; +use super::{emit_shadow_clears_after_stmt, lower_stmt}; +use crate::expr::{ + emit_typed_feedback_register_site, lower_expr, FnCtx, MaskedWindowArrayFact, MaskedWindowElem, + TypedFeedbackContract, TypedFeedbackKind, +}; +use crate::types::{DOUBLE, I1, I32, I64}; + +/// Minimum number of masked static-window reads on eligible arrays a region +/// must contain before the probe call + region duplication pays for itself. +/// `_encipher` has ~130; hand-rolled crypto/codec rounds have ≥ 16. +const REGION_MIN_TRACKED_READS: usize = 8; + +/// Counter-id sentinel for the shared pure-expression walk: no HIR local uses +/// `u32::MAX`, so the walk's counter-relative arm never fires and every +/// tracked read must carry a static index window. +const REGION_NO_COUNTER: u32 = u32::MAX; + +pub(super) struct MaskedWindowRegionArray { + pub array_id: u32, + /// Merged static window over every tracked read of this array. + pub lo: i64, + pub hi: i64, +} + +/// One scheduled fast-copy type refinement: after lowering the statement at +/// `stmt_offset`, override (or restore) `local_id`'s static type. +pub(super) struct RegionRefinement { + pub stmt_offset: usize, + pub local_id: u32, + /// `true` → set `Type::Number`; `false` → restore the original type (the + /// local was reassigned a value we can no longer prove numeric). + pub set_number: bool, +} + +pub(super) struct MaskedWindowRegion { + /// Number of consecutive statements the region consumes. + pub len: usize, + /// Eligible arrays (static-window reads only, never written in-region, + /// addressable number-array or untyped bindings). + pub arrays: Vec, + /// Flow-ordered type refinements applied ONLY inside the fast copies: + /// an untyped local written a provably-numeric value (a fact-covered + /// read, or any ToNumber/ToInt32-producing operator) is `Type::Number` + /// from that statement on, so downstream scalar ops lower numerically + /// (inline coercion towers) instead of through the `js_dynamic_*` + /// dispatch calls. The slow copy sees the original types — full dynamic + /// semantics — and the fast copies compute identical VALUES for numeric + /// inputs, which the entry guards established. + pub refinements: Vec, +} + +/// True when `stmt` contains at least one `LocalGet`-received read with a +/// static index window — the cheap pre-filter that keeps the quadratic-ish +/// region scan off plain arithmetic runs. +fn stmt_has_masked_read(stmt: &Stmt) -> bool { + fn expr_has(expr: &Expr) -> bool { + if let Expr::IndexGet { object, index } = expr { + if matches!(object.as_ref(), Expr::LocalGet(_)) + && crate::collectors::static_index_window(index).is_some() + { + return true; + } + } + let mut found = false; + perry_hir::walker::walk_expr_children(expr, &mut |child| { + found = found || expr_has(child); + }); + found + } + matches!(stmt, Stmt::Expr(expr) if expr_has(expr)) +} + +/// Count masked static-window reads on `eligible` arrays inside `expr`. +fn count_masked_reads(expr: &Expr, eligible: &std::collections::HashSet) -> usize { + let mut count = 0; + if let Expr::IndexGet { object, index } = expr { + if let Expr::LocalGet(id) = object.as_ref() { + if eligible.contains(id) && crate::collectors::static_index_window(index).is_some() { + count += 1; + } + } + } + perry_hir::walker::walk_expr_children(expr, &mut |child| { + count += count_masked_reads(child, eligible); + }); + count +} + +/// True when `expr` provably evaluates to a JS number in a fast copy, under +/// `refined` (locals already proven number at this program point) and +/// `eligible` (arrays whose static-window reads the entry guard proved +/// numeric). +/// +/// BigInt is the trap here: `1n * 1n`, `-1n`, `~1n`, `1n << 1n` are all +/// BigInts, so arithmetic/bitwise operators do NOT unconditionally produce +/// numbers. What IS sound is the mixed-type rule: when at least one operand +/// is a proven number, `-`/`*`/`/`/`%`/`**`/`&`/`|`/`^`/`<<`/`>>` either +/// produce a Number or THROW a TypeError ("cannot mix BigInt") — and a +/// statement that throws never completes, so its scheduled refinement is +/// unobservable. `>>>` and unary `+` throw on ANY BigInt operand, so they +/// are unconditionally number-or-throw; `+` (Add) needs BOTH sides proven +/// (string concatenation); `-x`/`~x` need the operand proven. +fn expr_is_number_under( + ctx: &FnCtx<'_>, + refined: &std::collections::HashSet, + eligible: &std::collections::HashSet, + expr: &Expr, +) -> bool { + use perry_hir::{BinaryOp, UnaryOp}; + match expr { + Expr::Number(_) | Expr::Integer(_) | Expr::NumberCoerce(_) => true, + Expr::LocalGet(id) => { + refined.contains(id) + || matches!( + ctx.local_types.get(id), + Some(perry_types::Type::Number | perry_types::Type::Int32) + ) + } + Expr::IndexGet { object, index } => { + matches!(object.as_ref(), Expr::LocalGet(id) if eligible.contains(id)) + && crate::collectors::static_index_window(index).is_some() + } + Expr::Binary { op, left, right } => match op { + // ToUint32 has no BigInt form — `1n >>> 0n` throws — so the + // result, when the statement completes, is always a Number. + BinaryOp::UShr => true, + // Number-or-throw when one side is a proven number: the BigInt + // forms of these ops require BOTH operands BigInt (mixing + // throws), and every non-BigInt primitive coerces to Number. + BinaryOp::BitAnd + | BinaryOp::BitOr + | BinaryOp::BitXor + | BinaryOp::Shl + | BinaryOp::Shr + | BinaryOp::Sub + | BinaryOp::Mul + | BinaryOp::Div + | BinaryOp::Mod + | BinaryOp::Pow => { + expr_is_number_under(ctx, refined, eligible, left) + || expr_is_number_under(ctx, refined, eligible, right) + } + BinaryOp::Add => { + expr_is_number_under(ctx, refined, eligible, left) + && expr_is_number_under(ctx, refined, eligible, right) + } + }, + Expr::Unary { op, operand } => match op { + // Unary `+` is ToNumber, which throws on BigInt. + UnaryOp::Pos => true, + // `-x` / `~x` on a BigInt yield BigInts — need the operand proven. + UnaryOp::Neg | UnaryOp::BitNot => expr_is_number_under(ctx, refined, eligible, operand), + UnaryOp::Not => false, + }, + Expr::Conditional { + condition: _, + then_expr, + else_expr, + } => { + expr_is_number_under(ctx, refined, eligible, then_expr) + && expr_is_number_under(ctx, refined, eligible, else_expr) + } + Expr::Logical { left, right, .. } => { + expr_is_number_under(ctx, refined, eligible, left) + && expr_is_number_under(ctx, refined, eligible, right) + } + Expr::MathImul(_, _) + | Expr::MathPow(_, _) + | Expr::MathMin(_) + | Expr::MathMax(_) + | Expr::MathAbs(_) + | Expr::MathSqrt(_) + | Expr::MathFloor(_) + | Expr::MathCeil(_) + | Expr::MathRound(_) + | Expr::MathTrunc(_) + | Expr::MathSign(_) + | Expr::MathF16round(_) => true, + _ => false, + } +} + +/// Match a masked-window region starting at `stmts[0]`. Returns `None` when +/// the run is too short, tracks no eligible array, or carries fewer than +/// [`REGION_MIN_TRACKED_READS`] tracked reads. +pub(super) fn try_match_masked_window_region( + ctx: &FnCtx<'_>, + stmts: &[Stmt], +) -> Option { + if !stmts.first().is_some_and(stmt_has_masked_read) { + return None; + } + let mut accesses: std::collections::BTreeMap = + std::collections::BTreeMap::new(); + let mut written: std::collections::HashSet = std::collections::HashSet::new(); + let mut len = 0usize; + for stmt in stmts { + let ok = match stmt { + Stmt::Expr(Expr::LocalSet(id, value)) => { + let mut trial = accesses.clone(); + if packed_f64_range_loop_pure_expr_collect( + value, + REGION_NO_COUNTER, + true, + &mut trial, + ) { + accesses = trial; + written.insert(*id); + true + } else { + false + } + } + Stmt::Expr(Expr::Update { id, .. }) => { + written.insert(*id); + true + } + Stmt::Expr(expr) => { + let mut trial = accesses.clone(); + if packed_f64_range_loop_pure_expr_collect( + expr, + REGION_NO_COUNTER, + true, + &mut trial, + ) { + accesses = trial; + true + } else { + false + } + } + _ => false, + }; + if !ok { + break; + } + len += 1; + } + if len == 0 || accesses.is_empty() { + return None; + } + + let mut arrays = Vec::new(); + for access in accesses.values() { + // A binding written anywhere in the region (`S = T` rebinding, or a + // tracked store) is dropped from the eligible set — its reads keep + // the ordinary per-access lowering in every copy. + if access.written || written.contains(&access.array_id) { + continue; + } + if access.counter.is_some() { + continue; + } + let Some((lo, hi)) = access.stat else { + continue; + }; + if lo < 0 || hi >= i64::from(i32::MAX) { + continue; + } + if !packed_loop_array_binding_storage_is_addressable(ctx, access.array_id) + || ctx.scalar_replaced_arrays.contains_key(&access.array_id) + { + continue; + } + // Already covered by an active fact (this run sits inside a dense + // range-loop fast copy) — its reads inline through that fact; a + // second, nested versioning would only add per-iteration probes. + if ctx + .masked_window_array_facts + .iter() + .any(|fact| fact.array_local_id == access.array_id) + { + continue; + } + if !local_is_number_array(ctx, access.array_id) + && !local_is_untyped_candidate(ctx, access.array_id) + { + continue; + } + arrays.push(MaskedWindowRegionArray { + array_id: access.array_id, + lo, + hi, + }); + } + if arrays.is_empty() { + return None; + } + + let eligible: std::collections::HashSet = + arrays.iter().map(|array| array.array_id).collect(); + let mut reads = 0usize; + for stmt in &stmts[..len] { + if let Stmt::Expr(expr) = stmt { + reads += count_masked_reads(expr, &eligible); + } + } + if reads < REGION_MIN_TRACKED_READS { + return None; + } + + // Flow-ordered fast-copy type refinements. A refinement lands strictly + // AFTER its statement: the statement's own RHS may read the local's + // pre-write (possibly non-number) value and must keep coercing + // semantics; every later statement may assume Number. A subsequent + // write we cannot prove numeric restores the original type. + let mut refinements = Vec::new(); + let mut refined: std::collections::HashSet = std::collections::HashSet::new(); + // Only plain stack locals whose static type is not already numeric are + // worth refining (boxed/captured storage keeps its own access lowering). + let refinable = |ctx: &FnCtx<'_>, id: u32| { + ctx.locals.contains_key(&id) + && !ctx.boxed_vars.contains(&id) + && !ctx.closure_captures.contains_key(&id) + && !matches!( + ctx.local_types.get(&id), + Some(perry_types::Type::Number | perry_types::Type::Int32) + ) + }; + for (offset, stmt) in stmts[..len].iter().enumerate() { + match stmt { + Stmt::Expr(Expr::LocalSet(id, value)) => { + if expr_is_number_under(ctx, &refined, &eligible, value) { + if refinable(ctx, *id) && refined.insert(*id) { + refinements.push(RegionRefinement { + stmt_offset: offset, + local_id: *id, + set_number: true, + }); + } + } else if refined.remove(id) { + refinements.push(RegionRefinement { + stmt_offset: offset, + local_id: *id, + set_number: false, + }); + } + } + // `x++` on a BigInt yields a BigInt (ToNumeric, not ToNumber) — + // an Update proves nothing about the local's type. If the local + // was previously refined, the refinement stays valid (++ on a + // number is a number); an unrefined local stays unrefined. + Stmt::Expr(Expr::Update { .. }) => {} + _ => {} + } + } + + Some(MaskedWindowRegion { + len, + arrays, + refinements, + }) +} + +/// Lower one copy of the region, mirroring `lower_stmts_inner`'s per-statement +/// bookkeeping (shadow-slot clears at the original statement indices). Fast +/// copies pass the region's flow-ordered type `refinements`; each lands +/// strictly AFTER its statement (the statement's own RHS may read the +/// pre-write, possibly non-number value) and every original type is restored +/// before returning, so the next copy — and everything after the region — +/// sees the untouched static types. +fn lower_region_copy( + ctx: &mut FnCtx<'_>, + region_stmts: &[Stmt], + base_idx: usize, + emit_shadow_clears: bool, + refinements: &[RegionRefinement], + privatize: bool, +) -> Result<()> { + // Locals refined to Number and never un-refined for the rest of the + // region. When `privatize` holds (no enclosing `try` — an exception + // unwinds the whole frame, so a stale original slot is unobservable), + // the fast copy moves each such local into a FRESH entry alloca at its + // refinement point and copies the value back at region end. The original + // slot's address escaped through `js_shadow_slot_bind`, which blocks + // LLVM from promoting it to a register; the private slot never escapes, + // so the whole call-free region SROAs into register-resident bit-mixing + // chains (the bcryptjs `_encipher` win). + let unset_ids: std::collections::HashSet = refinements + .iter() + .filter(|refinement| !refinement.set_number) + .map(|refinement| refinement.local_id) + .collect(); + let mut privatized: Vec<(u32, String)> = Vec::new(); + let mut saved: Vec<(u32, Option)> = Vec::new(); + let mut saved_ids: std::collections::HashSet = std::collections::HashSet::new(); + let mut result = Ok(()); + 'stmts: for (offset, stmt) in region_stmts.iter().enumerate() { + result = lower_stmt(ctx, stmt); + if result.is_err() || ctx.block().is_terminated() { + break; + } + if emit_shadow_clears { + emit_shadow_clears_after_stmt(ctx, base_idx + offset); + if ctx.block().is_terminated() { + break 'stmts; + } + } + for r in 0..refinements.len() { + if refinements[r].stmt_offset != offset { + continue; + } + let id = refinements[r].local_id; + let set_number = refinements[r].set_number; + if saved_ids.insert(id) { + saved.push((id, ctx.local_types.get(&id).cloned())); + } + if set_number { + ctx.local_types.insert(id, perry_types::Type::Number); + // The local now provably holds a number for the rest of the + // copy (or until an unset): clear its shadow slot once and + // suppress the per-statement shadow updates — numbers need + // no GC root, and the region admits no statement that could + // store a pointer while suppressed. When the statement's own + // shadow update already emitted a clear (its RHS was a known + // non-pointer shape), don't emit a second one. + if let Some(slot_idx) = ctx.shadow_slot_map.get(&id).copied() { + if ctx.masked_region_scalar_locals.insert(id) { + let already_cleared = matches!( + stmt, + Stmt::Expr(Expr::LocalSet(_, rhs)) + if crate::expr::expr_is_known_non_pointer_shadow_value(ctx, rhs) + ); + if !already_cleared { + crate::expr::emit_shadow_slot_clear(ctx, slot_idx); + } + } + } + if privatize && !unset_ids.contains(&id) { + if let Some(original_slot) = ctx.locals.get(&id).cloned() { + let private_slot = ctx.func.alloca_entry(DOUBLE); + let current = ctx.block().load(DOUBLE, &original_slot); + ctx.block().store(DOUBLE, ¤t, &private_slot); + ctx.locals.insert(id, private_slot); + privatized.push((id, original_slot)); + } + } + } else { + // Restore the pre-region type for the rest of this copy. + match saved.iter().find(|(saved_id, _)| *saved_id == id) { + Some((_, Some(original))) => { + ctx.local_types.insert(id, original.clone()); + } + _ => { + ctx.local_types.remove(&id); + } + } + // The statement just lowered stored a value we can no longer + // prove numeric while its shadow update was suppressed — + // re-bind the slot from the local's current value so GC sees + // it again. + if ctx.masked_region_scalar_locals.remove(&id) { + if let Some(slot_idx) = ctx.shadow_slot_map.get(&id).copied() { + if let Some(local_slot) = ctx.locals.get(&id).cloned() { + crate::expr::emit_shadow_slot_bind_for_local(ctx, id); + let current = ctx.block().load(DOUBLE, &local_slot); + let bits = ctx.block().bitcast_double_to_i64(¤t); + ctx.block().call_void( + "js_shadow_slot_set", + &[(I32, &slot_idx.to_string()), (I64, &bits)], + ); + } + } + } + } + } + } + // Copy privatized values back into the original (shadow-visible) slots + // and restore the binding map — post-region code reads the originals. + for (id, original_slot) in &privatized { + if result.is_ok() && !ctx.block().is_terminated() { + if let Some(private_slot) = ctx.locals.get(id).cloned() { + let value = ctx.block().load(DOUBLE, &private_slot); + ctx.block().store(DOUBLE, &value, original_slot); + } + } + ctx.locals.insert(*id, original_slot.clone()); + } + // Drop any still-active suppressions before leaving the copy — the slow + // copy and post-region code use the ordinary shadow protocol. + for (id, _) in &saved { + ctx.masked_region_scalar_locals.remove(id); + } + for (id, original) in saved { + match original { + Some(original) => { + ctx.local_types.insert(id, original); + } + None => { + ctx.local_types.remove(&id); + } + } + } + result +} + +/// Emit the versioned region: TA probe chain → `ta_i32` fast copy, plain +/// dense-window guard chain → `plain_f64` fast copy, else the slow copy. +pub(super) fn lower_masked_window_region( + ctx: &mut FnCtx<'_>, + region_stmts: &[Stmt], + base_idx: usize, + emit_shadow_clears: bool, + region: &MaskedWindowRegion, +) -> Result<()> { + let ta_pre_idx = ctx.new_block("masked_region.ta_i32.preheader"); + let try_plain_idx = ctx.new_block("masked_region.try_plain"); + let plain_pre_idx = ctx.new_block("masked_region.plain_f64.preheader"); + let slow_pre_idx = ctx.new_block("masked_region.slow"); + let merge_idx = ctx.new_block("masked_region.merge"); + let ta_pre_label = ctx.block_label(ta_pre_idx); + let try_plain_label = ctx.block_label(try_plain_idx); + let plain_pre_label = ctx.block_label(plain_pre_idx); + let slow_pre_label = ctx.block_label(slow_pre_idx); + let merge_label = ctx.block_label(merge_idx); + + // TA tier probe: every eligible array must classify as an Int32Array + // covering its window. Kind code 1 = MASKED_WINDOW_TA_KIND_I32 (see + // perry-runtime/src/typed_feedback.rs). + let mut all_i32: Option = None; + for array in ®ion.arrays { + let arr_box = lower_expr(ctx, &Expr::LocalGet(array.array_id))?; + let feedback_site_id = emit_typed_feedback_register_site( + ctx, + TypedFeedbackKind::ArrayElement, + "array[masked_region_ta_probe]", + TypedFeedbackContract::masked_window_ta_probe(), + ); + let kind = ctx.block().call( + I32, + "js_typed_feedback_masked_window_ta_kind", + &[ + (I64, &feedback_site_id), + (DOUBLE, &arr_box), + (I32, &array.lo.to_string()), + (I32, &(array.hi + 1).to_string()), + ], + ); + let is_i32 = ctx.block().icmp_eq(I32, &kind, "1"); + all_i32 = Some(match all_i32 { + None => is_i32, + Some(prev) => ctx.block().and(I1, &prev, &is_i32), + }); + } + let all_i32 = all_i32.expect("region matcher requires >= 1 eligible array"); + ctx.block() + .cond_br(&all_i32, &ta_pre_label, &try_plain_label); + + // Plain tier: the dense window guard (hole-free, raw-f64) — O(1) once the + // RawF64 layout flag is set. + ctx.current_block = try_plain_idx; + let mut all_plain: Option = None; + for array in ®ion.arrays { + let arr_box = lower_expr(ctx, &Expr::LocalGet(array.array_id))?; + let feedback_site_id = emit_typed_feedback_register_site( + ctx, + TypedFeedbackKind::ArrayElement, + "array[masked_region_plain]", + TypedFeedbackContract::packed_f64_array_loop(), + ); + let guard_i32 = ctx.block().call( + I32, + "js_typed_feedback_packed_f64_range_loop_guard_dense", + &[ + (I64, &feedback_site_id), + (DOUBLE, &arr_box), + (I32, &array.lo.to_string()), + (I32, &(array.hi + 1).to_string()), + ], + ); + let guard_ok = ctx.block().icmp_ne(I32, &guard_i32, "0"); + all_plain = Some(match all_plain { + None => guard_ok, + Some(prev) => ctx.block().and(I1, &prev, &guard_ok), + }); + } + let all_plain = all_plain.expect("region matcher requires >= 1 eligible array"); + ctx.block() + .cond_br(&all_plain, &plain_pre_label, &slow_pre_label); + + // ta_i32 fast copy: hoist each array's element-0 pointer, then bare + // `load i32` element reads (values_i32 keeps bit-mixing chains in i32). + ctx.current_block = ta_pre_idx; + let mut hoisted: Vec<(u32, String)> = Vec::new(); + for array in ®ion.arrays { + let arr_box = lower_expr(ctx, &Expr::LocalGet(array.array_id))?; + let data_ptr = ctx.block().call( + I64, + "js_typed_array_masked_window_data_ptr", + &[(DOUBLE, &arr_box)], + ); + hoisted.push((array.array_id, data_ptr)); + } + let ta_scope_id = ctx.next_loop_proof_scope_id(); + for (array, (arr_id, data_ptr)) in region.arrays.iter().zip(hoisted) { + ctx.masked_window_array_facts.push(MaskedWindowArrayFact { + array_local_id: arr_id, + scope_id: ta_scope_id, + guard_id: "masked_region_ta_i32".to_string(), + min_idx: array.lo, + max_idx_exclusive: array.hi + 1, + values_i32: true, + elem: MaskedWindowElem::TaI32 { data_ptr }, + }); + } + let privatize = ctx.try_depth == 0; + lower_region_copy( + ctx, + region_stmts, + base_idx, + emit_shadow_clears, + ®ion.refinements, + privatize, + )?; + ctx.masked_window_array_facts + .retain(|fact| fact.scope_id != ta_scope_id); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + // plain_f64 fast copy: bare raw-f64 window loads on the boxed handle. + ctx.current_block = plain_pre_idx; + let plain_scope_id = ctx.next_loop_proof_scope_id(); + for array in ®ion.arrays { + ctx.masked_window_array_facts.push(MaskedWindowArrayFact { + array_local_id: array.array_id, + scope_id: plain_scope_id, + guard_id: "masked_region_plain_f64".to_string(), + min_idx: array.lo, + max_idx_exclusive: array.hi + 1, + values_i32: false, + elem: MaskedWindowElem::PlainF64, + }); + } + lower_region_copy( + ctx, + region_stmts, + base_idx, + emit_shadow_clears, + ®ion.refinements, + privatize, + )?; + ctx.masked_window_array_facts + .retain(|fact| fact.scope_id != plain_scope_id); + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + // Slow copy: the untouched per-access lowering, original static types. + ctx.current_block = slow_pre_idx; + lower_region_copy(ctx, region_stmts, base_idx, emit_shadow_clears, &[], false)?; + if !ctx.block().is_terminated() { + ctx.block().br(&merge_label); + } + + ctx.current_block = merge_idx; + Ok(()) +} diff --git a/crates/perry-codegen/src/stmt/mod.rs b/crates/perry-codegen/src/stmt/mod.rs index ba6f4f1f18..81ad41ad6e 100644 --- a/crates/perry-codegen/src/stmt/mod.rs +++ b/crates/perry-codegen/src/stmt/mod.rs @@ -14,6 +14,7 @@ use crate::types::DOUBLE; mod if_stmt; mod let_stmt; mod loops; +mod masked_window_region; mod switch_stmt; mod try_stmt; mod unused_expr; @@ -179,6 +180,28 @@ fn lower_stmts_inner(ctx: &mut FnCtx<'_>, stmts: &[Stmt], emit_shadow_clears: bo } } } + // #6750 follow-up: masked-window versioning for straight-line runs + // of scalar statements (bcryptjs ships `_encipher` fully unrolled — + // ~130 consecutive `S[l >>> 24]`-shaped reads with no loop for the + // range-loop tiers to version). Probes the accessed arrays once at + // region entry and branches into a fast copy whose masked reads are + // bare inline loads; consumes the whole region on a match. + if let Some(region) = masked_window_region::try_match_masked_window_region(ctx, &stmts[i..]) + { + let end = i + region.len; + masked_window_region::lower_masked_window_region( + ctx, + &stmts[i..end], + i, + emit_shadow_clears, + ®ion, + )?; + i = end; + if ctx.block().is_terminated() { + break; + } + continue; + } lower_stmt(ctx, &stmts[i])?; // If an earlier statement already terminated the current block // (e.g. return in a straight-line sequence), any following statement diff --git a/crates/perry-codegen/src/type_analysis/numeric.rs b/crates/perry-codegen/src/type_analysis/numeric.rs index 69f84fb514..0f018e06c0 100644 --- a/crates/perry-codegen/src/type_analysis/numeric.rs +++ b/crates/perry-codegen/src/type_analysis/numeric.rs @@ -249,7 +249,7 @@ pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { // Without this, `sum + arr[i]` in a hot loop wraps the element // load in `js_number_coerce` which blocks LLVM's vectorizer // and adds a function call per iteration. - Expr::IndexGet { object, .. } => { + Expr::IndexGet { object, index } => { if receiver_class_name(ctx, object) .as_deref() .is_some_and(is_numeric_typed_array_class) @@ -259,6 +259,18 @@ pub(crate) fn is_numeric_expr(ctx: &FnCtx<'_>, e: &Expr) -> bool { let Expr::LocalGet(arr_id) = object.as_ref() else { return false; }; + // #6750 follow-up: a masked-index read covered by an ACTIVE + // masked-window fact (dense range-loop / straight-line-region + // fast copy) is a guard-proven numeric element load, even when + // the receiver's STATIC type is erased (`any` parameter). + // Without this, `n ^= S[x & 0xff]` inside a fast copy still + // routed through the BigInt-aware dynamic helpers. Facts are + // scope-managed by the versioned lowerings, so the answer is + // only `true` while a fast copy that proved the window is being + // lowered. + if crate::expr::masked_window_fact_for_index(ctx, *arr_id, index).is_some() { + return true; + } match ctx.local_types.get(arr_id) { Some(HirType::Array(elem)) => { matches!(**elem, HirType::Number | HirType::Int32) diff --git a/crates/perry-runtime/src/typed_feedback.rs b/crates/perry-runtime/src/typed_feedback.rs index 3d6a0b40fc..bd9f96769b 100644 --- a/crates/perry-runtime/src/typed_feedback.rs +++ b/crates/perry-runtime/src/typed_feedback.rs @@ -1382,6 +1382,80 @@ pub extern "C" fn js_typed_feedback_packed_f64_range_loop_guard_dense_i32( ) } +/// Kind codes returned by [`js_typed_feedback_masked_window_ta_kind`]. The +/// codegen tier dispatch branches on these exact values — keep in sync with +/// the masked-window TA tiers in `perry-codegen/src/stmt/loops.rs`. +pub const MASKED_WINDOW_TA_KIND_NONE: i32 = 0; +pub const MASKED_WINDOW_TA_KIND_I32: i32 = 1; +pub const MASKED_WINDOW_TA_KIND_U32: i32 = 2; +pub const MASKED_WINDOW_TA_KIND_F64: i32 = 3; + +/// Masked-window typed-array probe (#6750 follow-up): classify a receiver +/// whose static type the compiler could not prove (an `any` function +/// parameter — the bcryptjs S-box shape) as a typed array whose whole static +/// index window `[min_idx, max_idx_exclusive)` is in bounds. O(1): a registry +/// lookup plus a length compare — no window scan, so re-entering a short hot +/// loop (one probe per accessed array per entry) stays cheap. +/// +/// A view over a detached ArrayBuffer has `length == 0` +/// (`zero_views_of_detached_backing`), so the window check also rejects +/// detached backings. Kinds outside {Int32, Uint32, Float64} return NONE and +/// fall through to the plain-array guard tiers / the slow loop. +fn masked_window_ta_kind(addr: usize, min_idx: i32, max_idx_exclusive: i32) -> i32 { + if min_idx < 0 { + return MASKED_WINDOW_TA_KIND_NONE; + } + let Some(kind) = crate::typedarray::lookup_typed_array_kind(addr) else { + return MASKED_WINDOW_TA_KIND_NONE; + }; + let code = match kind { + crate::typedarray::KIND_INT32 => MASKED_WINDOW_TA_KIND_I32, + crate::typedarray::KIND_UINT32 => MASKED_WINDOW_TA_KIND_U32, + crate::typedarray::KIND_FLOAT64 => MASKED_WINDOW_TA_KIND_F64, + _ => return MASKED_WINDOW_TA_KIND_NONE, + }; + let len = unsafe { (*(addr as *const crate::typedarray::TypedArrayHeader)).length }; + if i64::from(max_idx_exclusive) > i64::from(len) { + return MASKED_WINDOW_TA_KIND_NONE; + } + code +} + +/// FFI wrapper for [`masked_window_ta_kind`] — the typed-array tier probe of +/// the read-only masked-index range loop. Returns the `MASKED_WINDOW_TA_KIND_*` +/// code; the codegen requires every accessed array to probe to the same +/// non-NONE code before entering the matching typed-array fast copy. +#[no_mangle] +pub extern "C" fn js_typed_feedback_masked_window_ta_kind( + site_id: u64, + receiver: f64, + min_idx: i32, + max_idx_exclusive: i32, +) -> i32 { + let raw_addr = normalize_raw_object_addr(receiver.to_bits()); + let code = masked_window_ta_kind(raw_addr, min_idx, max_idx_exclusive); + if typed_feedback_enabled() { + let (class_id, heap_type, aux, element_kind) = classify_array(raw_addr, None); + let observation = Observation { + source: ObservationSource::Array, + object_addr: 0, + shape_addr: 0, + key_hash: 0, + class_id, + heap_type, + aux, + value_tag: element_kind, + }; + guard_observe( + site_id, + TypedFeedbackSiteKind::ArrayElement, + observation, + code != MASKED_WINDOW_TA_KIND_NONE, + ); + } + code +} + fn packed_i32_array_loop_guard(arr: *const ArrayHeader) -> bool { if !packed_f64_array_loop_guard(arr) { return false; diff --git a/crates/perry-runtime/src/typedarray/mod.rs b/crates/perry-runtime/src/typedarray/mod.rs index f16af6502a..ab04256e01 100644 --- a/crates/perry-runtime/src/typedarray/mod.rs +++ b/crates/perry-runtime/src/typedarray/mod.rs @@ -398,6 +398,27 @@ pub(crate) fn data_ptr(ta: *const TypedArrayHeader) -> *const u8 { } } +/// #6750 follow-up: preheader data-pointer hoist for the masked-window +/// typed-array loop tiers. Returns element 0's address for a registered typed +/// array (owning inline storage, ArrayBuffer view, or native-arena view), or +/// 0 when the receiver is not a registered typed array. Sound to cache for a +/// guarded fast-loop copy: the copy's body is call-free (no allocation → no +/// GC), the typed array's own address is stable (raw alloc or tenured +/// old-gen — see `PERRY_TA_KIND_CACHE`), and view backings live for the +/// thread's lifetime (`typedarray_view::TYPED_ARRAY_VIEW_META`), so nothing +/// can invalidate the pointer between the loop-entry probe and the last +/// iteration. +#[no_mangle] +pub extern "C" fn js_typed_array_masked_window_data_ptr(receiver: f64) -> i64 { + let addr = strip_nanbox(receiver.to_bits()); + if !crate::value::addr_class::is_plausible_heap_addr(addr) + || lookup_typed_array_kind(addr).is_none() + { + return 0; + } + data_ptr(addr as *const TypedArrayHeader) as i64 +} + #[inline] pub(crate) fn data_ptr_mut(ta: *mut TypedArrayHeader) -> *mut u8 { unsafe { diff --git a/test-files/test_gap_untyped_param_masked_window.ts b/test-files/test_gap_untyped_param_masked_window.ts new file mode 100644 index 0000000000..64d8e2fe82 --- /dev/null +++ b/test-files/test_gap_untyped_param_masked_window.ts @@ -0,0 +1,210 @@ +// #6750 follow-up: masked-window array reads through UNTYPED (any) function +// parameters — the bcryptjs Blowfish S-box shape. The dense range-loop +// versioning probes the runtime receiver at loop entry (typed-array tiers + +// plain-array guard tiers); every shape below must produce byte-identical +// output to Node whether a fast copy fires or the loop deopts to the slow +// path. + +// The canonical hot shape: S[i & mask] reduction over an untyped param. +function sumMasked(S: any, n: number): number { + let s = 0 | 0; + for (let i = 0; i < n; i++) s = (s + S[i & 15]) | 0; + return s; +} + +// f64-context reads (no |0 on the load): exercises the unsigned/float +// materialization of the typed-array tiers. +function sumMaskedF64(S: any, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) s += S[i & 15]; + return s; +} + +// Two untyped params in one loop (bcryptjs reads P and S together). +function sumTwoArrays(P: any, S: any, n: number): number { + let s = 0 | 0; + for (let i = 0; i < n; i++) s = (s + P[i & 7] + S[i & 15]) | 0; + return s; +} + +// Reassigning the binding inside the loop must keep full JS semantics. +function sumWithReassign(S: any, T: any, n: number): number { + let s = 0; + for (let i = 0; i < n; i++) { + s += S[i & 15]; + if (i === 8) S = T; + } + return s; +} + +const i32 = new Int32Array(16); +const u32 = new Uint32Array(16); +const f64 = new Float64Array(16); +const u8 = new Uint8Array(16); +const plain: number[] = new Array(16); +for (let i = 0; i < 16; i++) { + i32[i] = (i * 2654435761) | 0; // mixed-sign int32 values + u32[i] = (i * 2654435761) >>> 0; // values above i32::MAX — must stay unsigned + f64[i] = i * 1.5 + 0.25; // fractional — width 8, no i32 tier + u8[i] = (i * 37) & 0xff; // unsupported probe kind — must deopt cleanly + plain[i] = (i * 2654435761) | 0; +} +const small = new Int32Array(8); // shorter than the [0,16) window — probe must reject +for (let i = 0; i < 8; i++) small[i] = i + 1; +const holey: number[] = new Array(16); // hole inside the window — dense guard must reject +for (let i = 0; i < 16; i++) if (i !== 5) holey[i] = i + 1; +const mixed: any[] = new Array(16); // non-number element — guard must reject +for (let i = 0; i < 16; i++) mixed[i] = i === 7 ? 'x' : i + 1; +const offsetView = new Int32Array(new ArrayBuffer(128), 32, 16); // byteOffset view +for (let i = 0; i < 16; i++) offsetView[i] = (i + 1) * 3; + +console.log('i32:', sumMasked(i32, 1000)); +console.log('u32:', sumMasked(u32, 1000)); +console.log('f64:', sumMasked(f64, 1000)); +console.log('u8:', sumMasked(u8, 1000)); +console.log('plain:', sumMasked(plain, 1000)); +console.log('i32 f64-ctx:', sumMaskedF64(i32, 1000)); +console.log('u32 f64-ctx:', sumMaskedF64(u32, 1000)); // unsigned values summed as doubles +console.log('f64 f64-ctx:', sumMaskedF64(f64, 1000)); +console.log('plain f64-ctx:', sumMaskedF64(plain, 1000)); +console.log('short window:', sumMasked(small, 1000)); // OOB reads -> undefined -> NaN|0 +console.log('short f64-ctx:', sumMaskedF64(small, 1000)); // NaN +console.log('holey:', sumMaskedF64(holey, 1000)); // hole -> undefined -> NaN +console.log('mixed elems:', sumMasked(mixed, 1000)); // 'x' + num coercions +console.log('offset view:', sumMasked(offsetView, 1000)); +console.log('two arrays i32/i32:', sumTwoArrays(i32, i32, 1000)); +console.log('two arrays plain/plain:', sumTwoArrays(plain, plain, 1000)); +console.log('two arrays MIXED plain/i32:', sumTwoArrays(plain, i32, 1000)); // heterogeneous -> deopt +console.log('two arrays MIXED i32/f64:', sumTwoArrays(i32, f64, 1000)); // TA kinds disagree -> deopt +console.log('reassign mid-loop:', sumWithReassign(i32, plain, 32)); + +// Polymorphic call site: the same loop re-probes per entry. +const receivers: any[] = [i32, plain, u32, f64, i32, 'abcdefghijklmnop', { 3: 41 }]; +for (const r of receivers) { + console.log('poly:', sumMaskedF64(r, 8)); +} + +// Detached backing: views over a transferred ArrayBuffer read undefined. +const buf = new ArrayBuffer(64); +const view = new Int32Array(buf, 0, 16); +for (let i = 0; i < 16; i++) view[i] = i + 1; +console.log('pre-detach:', sumMasked(view, 100)); +(buf as any).transfer(); +console.log('post-detach:', sumMaskedF64(view, 100)); // length 0 -> undefined reads -> NaN + +// ---- STRAIGHT-LINE region shapes (the unrolled bcryptjs _encipher form): +// >= 8 masked reads with no loop, exercising the region versioner. + +// The _encipher shape: untyped locals fed from an untyped array, then a +// run of masked reads mixed through bitwise ops. +function encipherish(lr: any, off: any, P: any, S: any): number { + let n = 0; + let l = lr[off]; + let r = lr[off + 1]; + l ^= P[0]; + n = S[l >>> 28]; + n += S[8 | ((l >> 16) & 7)]; + n ^= S[(l >> 8) & 15]; + n += S[l & 15]; + r ^= n ^ P[1]; + n = S[r >>> 28]; + n += S[8 | ((r >> 16) & 7)]; + n ^= S[(r >> 8) & 15]; + n += S[r & 15]; + l ^= n ^ P[2]; + return ((l | 0) + (r | 0) + (n | 0)) | 0; +} + +// Region with the binding REASSIGNED mid-run: T's reads must see T. +function regionReassign(S: any, T: any): number { + let a = 0; + a += S[1 & 15]; + a += S[2 & 15]; + a += S[3 & 15]; + a += S[4 & 15]; + S = T; + a += S[5 & 15]; + a += S[6 & 15]; + a += S[7 & 15]; + a += S[8 & 15]; + return a; +} + +// Region inside try/catch (privatization disabled): a mid-region throw via +// a valueOf that returns a non-number must leave the partial sums correct. +function regionInTry(S: any, poison: any): string { + let a = 0; + try { + a += S[1 & 15]; + a += S[2 & 15]; + a += S[3 & 15]; + a += S[4 & 15]; + a += S[5 & 15]; + a += S[6 & 15]; + a += S[7 & 15]; + a += S[8 & 15]; + a ^= poison; + } catch (e: any) { + return 'caught a=' + a + ' ' + e.message; + } + return 'ok a=' + a; +} + +// BigInt flowing through a region must NOT be refined to Number: `x * x`, +// `-x`, `~x` on BigInts yield BigInts (a pure-unknown operator proves +// nothing), and mixing a BigInt with a guard-proven numeric element read +// must throw TypeError in fast and slow copies alike. +function regionBigintSide(S: any, b: any): string { + let x = b; + let y = b; + let acc = 0; + acc += S[1 & 15]; + acc += S[2 & 15]; + acc += S[3 & 15]; + acc += S[4 & 15]; + x = x * x; + y = -y; + acc += S[5 & 15]; + acc += S[6 & 15]; + acc += S[7 & 15]; + acc += S[8 & 15]; + return acc + ' ' + x + ' ' + y; +} +function regionBigintMixThrow(S: any, b: any): string { + let x = b; + let acc = 0; + try { + acc += S[1 & 15]; + acc += S[2 & 15]; + acc += S[3 & 15]; + acc += S[4 & 15]; + acc += S[5 & 15]; + acc += S[6 & 15]; + acc += S[7 & 15]; + x = x * S[8 & 15]; + } catch (e: any) { + return 'caught acc=' + acc + ' x=' + x; + } + return 'no-throw acc=' + acc + ' x=' + x; +} + +const lrPlain = [11, 22]; +console.log('encipherish i32:', encipherish(lrPlain, 0, i32, i32)); +console.log('encipherish plain:', encipherish(lrPlain, 0, plain, plain)); +console.log('encipherish mixed:', encipherish(lrPlain, 0, plain, i32)); +console.log('encipherish u32:', encipherish(lrPlain, 0, u32, u32)); +console.log('encipherish f64:', encipherish(lrPlain, 0, f64, f64)); +console.log('encipherish holey:', encipherish(lrPlain, 0, holey, holey)); +console.log('encipherish short:', encipherish(lrPlain, 0, small, small)); +console.log('region reassign:', regionReassign(i32, plain), regionReassign(plain, u32)); +console.log('region try ok:', regionInTry(i32, 3)); +const thrower = { + valueOf() { + throw new Error('boom'); + }, +}; +console.log('region try throw:', regionInTry(i32, thrower)); +console.log('region bigint side:', regionBigintSide(i32, 3n)); +console.log('region bigint side plain:', regionBigintSide(plain, 5n)); +console.log('region bigint mix:', regionBigintMixThrow(i32, 7n)); +console.log('region bigint mix num:', regionBigintMixThrow(i32, 2)); // number path completes