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
9 changes: 9 additions & 0 deletions changelog.d/9171-string-array-length.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Masked `string[]` length-accumulation loops now validate their receiver and
complete index window once, then load boxed string slots directly and read SSO
or heap-string lengths inline. The generic loop remains as the fallback for
erased annotation lies, holes, descriptors, prototype pollution, and
non-number accumulators.

On the issue-shaped `strings[i & 3].length` benchmark this lowers Perry from
5.58 to 1.14 ns/access on the same host (4.9x faster), reducing the gap to
Node from about 8.9x to 1.8x.
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 @@ -1187,6 +1187,7 @@ pub(super) fn compile_closure(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::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 @@ -875,6 +875,7 @@ pub(super) fn compile_module_entry(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
Expand Down Expand Up @@ -1598,6 +1599,7 @@ pub(super) fn compile_module_entry(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::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 @@ -1137,6 +1137,7 @@ pub(super) fn compile_function(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::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 @@ -545,6 +545,7 @@ pub(super) fn compile_method(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
Expand Down Expand Up @@ -1717,6 +1718,7 @@ pub(super) fn compile_static_method(
bounded_index_pairs: Vec::new(),
packed_f64_loop_facts: Vec::new(),
masked_window_array_facts: Vec::new(),
string_window_array_facts: Vec::new(),
masked_region_scalar_locals: std::collections::HashSet::new(),
suppressed_cleared_shadow_slots: std::collections::HashSet::new(),
class_field_loop_facts: Vec::new(),
Expand Down
3 changes: 3 additions & 0 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1445,6 +1445,9 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
))
});
}
if let Some(value) = super::string_window::try_lower_index_get(ctx, object, index)? {
return Ok(value);
}
// #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
Expand Down
21 changes: 21 additions & 0 deletions crates/perry-codegen/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -998,6 +998,11 @@ pub(crate) struct FnCtx<'a> {
/// `i` in bounds.
pub packed_f64_loop_facts: Vec<PackedF64LoopFact>,
pub masked_window_array_facts: Vec<MaskedWindowArrayFact>,
/// Scoped facts established by the string-array masked-window loop
/// versioner. The entry guard proves every slot in the window is an
/// in-bounds SSO-or-heap string, so reads may bypass ordinary array
/// dispatch and string `.length` needs no dynamic miss arm.
pub string_window_array_facts: Vec<StringWindowArrayFact>,
/// #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
Expand Down Expand Up @@ -2061,6 +2066,20 @@ pub(crate) struct MaskedWindowArrayFact {
pub allows_stores: bool,
}

/// Read-only masked-index window over a plain array of boxed strings.
///
/// The fast-loop preheader validates the receiver shape, bounds, and every
/// slot's string tag. Its body is call/store-free apart from the accumulator
/// update, so the proof remains true until the scoped clone exits.
#[derive(Debug, Clone)]
pub(crate) struct StringWindowArrayFact {
pub array_local_id: u32,
pub scope_id: u32,
pub min_idx: i64,
pub max_idx_exclusive: i64,
pub numeric_accumulator: u32,
}

/// #5093: one fact per (receiver, versioned loop). See
/// `FnCtx::class_field_loop_facts` for the safety argument.
#[derive(Debug, Clone)]
Expand Down Expand Up @@ -2700,6 +2719,8 @@ mod index_get_claim_tests;
pub(crate) mod masked_window;
#[cfg(test)]
mod null_default_numeric_add_tests;
mod string_length;
pub(crate) mod string_window;

mod ptr_numarray_access;
mod ta_param_f64_read;
Expand Down
80 changes: 2 additions & 78 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -421,84 +421,8 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
return Ok(ctx.block().load(DOUBLE, &slot));
}
}
// `.length` on a statically-string receiver (`string`-typed local,
// `string[]` element, string-returning expression). #7128: this
// arrived in Phase 3a but keys on `is_string_expr` — the receiver's
// static TYPE — and never on a canonical-`Str` selection, so it is
// on `PERRY_STATIC_STRING_LOWERING`, not on the `Str` knob.
// The receiver bits are freshly
// produced with no safepoint before the header read (no
// forwarding hazard — evacuation rewrites slots/returns before
// the mutator resumes), so the ~18-op generic tower below
// (GC-type byte, forwarding flag, handle-band checks) collapses
// to a 3-arm tag dispatch: SSO → inline length-byte extract
// (`lshr 40; and 0xFF`, matching `js_value_length_f64`'s SSO
// branch), heap STRING_TAG → `load i32` of `utf16_len` at
// offset 0, anything else (annotation lie, nullable-union
// receiver) → the property-semantic slow call used by the
// generic tower's slow arm.
{
if crate::expr::static_string_lowering_enabled()
&& is_string_expr(ctx, object)
&& !is_array_expr(ctx, object)
{
let recv_box = lower_expr(ctx, object)?;
let bits = ctx.block().bitcast_double_to_i64(&recv_box);
let tag = ctx.block().lshr(I64, &bits, "48");
let is_sso =
ctx.block()
.icmp_eq(I64, &tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64);
let sso_idx = ctx.new_block("strlen.sso");
let chk_idx = ctx.new_block("strlen.chk");
let heap_idx = ctx.new_block("strlen.heap");
let slow_idx = ctx.new_block("strlen.slow");
let merge_idx = ctx.new_block("strlen.merge");
let sso_label = ctx.block_label(sso_idx);
let chk_label = ctx.block_label(chk_idx);
let heap_label = ctx.block_label(heap_idx);
let slow_label = ctx.block_label(slow_idx);
let merge_label = ctx.block_label(merge_idx);
ctx.block().cond_br(&is_sso, &sso_label, &chk_label);

ctx.current_block = sso_idx;
let len_shifted = ctx.block().lshr(I64, &bits, "40");
let len_byte = ctx.block().and(I64, &len_shifted, "255");
let sso_len = ctx.block().uitofp(I64, &len_byte, DOUBLE);
let sso_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = chk_idx;
let is_heap =
ctx.block()
.icmp_eq(I64, &tag, crate::nanbox::STRING_TAG_TOP16_I64);
ctx.block().cond_br(&is_heap, &heap_label, &slow_label);

ctx.current_block = heap_idx;
let handle = ctx.block().and(I64, &bits, POINTER_MASK_I64);
let len_i32 = ctx.block().safe_load_i32_from_ptr(&handle);
let heap_len = ctx.block().uitofp(I32, &len_i32, DOUBLE);
let heap_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = slow_idx;
let slow_len = ctx.block().call(
DOUBLE,
"js_value_length_property_f64",
&[(DOUBLE, &recv_box)],
);
let slow_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = merge_idx;
return Ok(ctx.block().phi(
DOUBLE,
&[
(&sso_len, &sso_pred),
(&heap_len, &heap_pred),
(&slow_len, &slow_pred),
],
));
}
if let Some(length) = super::string_length::try_lower(ctx, object)? {
return Ok(length);
}
// Issue #73: validate the receiver before the inline load.
// The compile-time condition above fires for Array / String /
Expand Down
100 changes: 100 additions & 0 deletions crates/perry-codegen/src/expr/string_length.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
//! Inline `.length` lowering for statically classified string receivers.

use anyhow::Result;
use perry_hir::Expr;

use crate::nanbox::POINTER_MASK_I64;
use crate::type_analysis::{is_array_expr, is_string_expr, string_value_is_runtime_guaranteed};
use crate::types::{DOUBLE, I32, I64};

use super::{lower_expr, static_string_lowering_enabled, FnCtx};

/// Lower string `.length` as SSO-byte extraction or a heap-header load.
///
/// A declared type is only a dispatch candidate, so its miss retains ordinary
/// property semantics. A constructive proof (including the guarded string
/// window used by #9160) makes the receiver exactly SSO-or-heap-string and
/// removes both the second tag branch and the runtime helper from the clone.
pub(crate) fn try_lower(ctx: &mut FnCtx<'_>, object: &Expr) -> Result<Option<String>> {
if !static_string_lowering_enabled()
|| !is_string_expr(ctx, object)
|| is_array_expr(ctx, object)
{
return Ok(None);
}

let proven_string = string_value_is_runtime_guaranteed(ctx, object);
let recv_box = lower_expr(ctx, object)?;
let bits = ctx.block().bitcast_double_to_i64(&recv_box);
let tag = ctx.block().lshr(I64, &bits, "48");
let is_sso = ctx
.block()
.icmp_eq(I64, &tag, crate::nanbox::SHORT_STRING_TAG_TOP16_I64);
let sso_idx = ctx.new_block("strlen.sso");
let chk_idx = (!proven_string).then(|| ctx.new_block("strlen.chk"));
let heap_idx = ctx.new_block("strlen.heap");
let slow_idx = chk_idx.map(|_| ctx.new_block("strlen.slow"));
let merge_idx = ctx.new_block("strlen.merge");
let sso_label = ctx.block_label(sso_idx);
let heap_label = ctx.block_label(heap_idx);
let merge_label = ctx.block_label(merge_idx);
let non_sso_label = chk_idx
.map(|idx| ctx.block_label(idx))
.unwrap_or_else(|| heap_label.clone());
ctx.block().cond_br(&is_sso, &sso_label, &non_sso_label);

ctx.current_block = sso_idx;
let len_shifted = ctx.block().lshr(I64, &bits, "40");
let len_byte = ctx.block().and(I64, &len_shifted, "255");
let sso_len = ctx.block().uitofp(I64, &len_byte, DOUBLE);
let sso_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

if let (Some(chk_idx), Some(slow_idx)) = (chk_idx, slow_idx) {
ctx.current_block = chk_idx;
let is_heap = ctx
.block()
.icmp_eq(I64, &tag, crate::nanbox::STRING_TAG_TOP16_I64);
let slow_label = ctx.block_label(slow_idx);
ctx.block().cond_br(&is_heap, &heap_label, &slow_label);

ctx.current_block = slow_idx;
let slow_len = ctx.block().call(
DOUBLE,
"js_value_length_property_f64",
&[(DOUBLE, &recv_box)],
);
let slow_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = heap_idx;
let handle = ctx.block().and(I64, &bits, POINTER_MASK_I64);
let len_i32 = ctx.block().safe_load_i32_from_ptr(&handle);
let heap_len = ctx.block().uitofp(I32, &len_i32, DOUBLE);
let heap_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = merge_idx;
return Ok(Some(ctx.block().phi(
DOUBLE,
&[
(&sso_len, &sso_pred),
(&heap_len, &heap_pred),
(&slow_len, &slow_pred),
],
)));
}

ctx.current_block = heap_idx;
let handle = ctx.block().and(I64, &bits, POINTER_MASK_I64);
let len_i32 = ctx.block().safe_load_i32_from_ptr(&handle);
let heap_len = ctx.block().uitofp(I32, &len_i32, DOUBLE);
let heap_pred = ctx.block().label.clone();
ctx.block().br(&merge_label);

ctx.current_block = merge_idx;
Ok(Some(ctx.block().phi(
DOUBLE,
&[(&sso_len, &sso_pred), (&heap_len, &heap_pred)],
)))
}
63 changes: 63 additions & 0 deletions crates/perry-codegen/src/expr/string_window.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
//! Raw reads backed by a scoped, prevalidated string-array window.

use perry_hir::Expr;

use crate::types::{DOUBLE, I32, I64};

use super::{lower_expr, lower_expr_as_i32, FnCtx, StringWindowArrayFact};

/// Find the innermost active fact whose validated window covers `index`.
pub(crate) fn fact_for_index(
ctx: &FnCtx<'_>,
array_local_id: u32,
index: &Expr,
) -> Option<StringWindowArrayFact> {
let (lo, hi) = crate::collectors::static_index_window(index)?;
ctx.string_window_array_facts
.iter()
.rev()
.find(|fact| {
fact.array_local_id == array_local_id
&& lo >= fact.min_idx
&& hi < fact.max_idx_exclusive
})
.cloned()
}

/// Whether an `IndexGet` is covered by an active string-window proof.
pub(crate) fn proves_index_string(ctx: &FnCtx<'_>, expr: &Expr) -> bool {
let Expr::IndexGet { object, index } = expr else {
return false;
};
let Expr::LocalGet(array_local_id) = object.as_ref() else {
return false;
};
fact_for_index(ctx, *array_local_id, index).is_some()
}

/// Lower a covered `array[index]` to `load double` at
/// `ArrayHeader + index * sizeof(JSValue)`. Re-reading the binding at every
/// access observes any GC move while still bypassing the generic array
/// guard/descriptor/hole diamond.
pub(crate) fn try_lower_index_get(
ctx: &mut FnCtx<'_>,
object: &Expr,
index: &Expr,
) -> anyhow::Result<Option<String>> {
let Expr::LocalGet(array_local_id) = object else {
return Ok(None);
};
if fact_for_index(ctx, *array_local_id, index).is_none() {
return Ok(None);
}
let array_box = lower_expr(ctx, object)?;
let index_i32 = lower_expr_as_i32(ctx, index)?;
let handle = super::packed_receiver_handle_i64(ctx, Some(*array_local_id), &array_box);
let block = ctx.block();
let index_i64 = block.zext(I32, &index_i32, I64);
let byte_offset = block.shl(I64, &index_i64, "3");
let slot_offset = block.add(I64, &byte_offset, "8");
let slot_addr = block.add(I64, &handle, &slot_offset);
let slot_ptr = block.inttoptr(I64, &slot_addr);
Ok(Some(block.load(DOUBLE, &slot_ptr)))
}
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/objects.rs
Original file line number Diff line number Diff line change
Expand Up @@ -393,6 +393,7 @@ pub fn declare_phase_b_objects(module: &mut LlModule) {
I32,
&[I64, DOUBLE],
);
module.declare_function("js_string_array_range_loop_guard", I32, &[DOUBLE, I32, I32]);
// #6011: range-preguarded packed-f64 loop — validates a whole
// [min_idx, max_idx_exclusive) index window (hole-tolerant) at loop entry.
module.declare_function(
Expand Down
8 changes: 8 additions & 0 deletions crates/perry-codegen/src/stmt/loops.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5576,6 +5576,14 @@ pub(crate) fn lower_for(
lower_stmt(ctx, init_stmt)?;
}

// #9160: `sum += strings[maskedIndex].length`. A one-time receiver,
// window, element-tag, and accumulator check admits a clone whose array
// access is a raw boxed-slot load and whose length dispatch is SSO/heap
// only. The ordinary loop below remains the semantic fallback.
if super::string_length_loop::lower(ctx, init, condition, update, body)? {
return Ok(());
}

// #6809/#6812: validate a dense, same-shape object array once and run a
// bounded one-to-four-field numeric write nest without receiver/shape
// guards or runtime calls in either hot loop.
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/stmt/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ mod prealloc_module_global_tests;
pub(crate) mod stable_packed_accumulator;
pub(crate) mod stable_packed_loop;
mod stable_packed_typed_array;
mod string_length_loop;
mod switch_stmt;
mod try_stmt;
mod unused_expr;
Expand Down
Loading
Loading