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
7 changes: 7 additions & 0 deletions changelog.d/8594-prefix-update-index-ranges.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Eliminated dynamic typed-array property lookups for bounded update-expression
indices such as `P[++i]`. Integer range analysis now models the distinct prefix
and postfix results and conservatively retains loop facts across updates. On
the Blowfish-shaped `typed_array` workload this reduced retired instructions
from 55.717 G to 14.647 G (-73.71%) and wall time from 4.063 s to 2.498 s
(-38.51%) on the quiet M1 mini, while the typed/untyped ratio stayed at 1.000
and peak RSS decreased 1.19%.
124 changes: 123 additions & 1 deletion crates/perry-codegen/src/codegen/ordinary_param_guard_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@

use crate::{compile_module, CompileOptions};
use perry_hir::types::{ObjectType, PropertyInfo, Type};
use perry_hir::{BinaryOp, CompareOp, Expr, Function, Module, Param, Stmt, TypeAlias};
use perry_hir::{BinaryOp, CompareOp, Expr, Function, Module, Param, Stmt, TypeAlias, UpdateOp};
use std::collections::HashMap;

fn function_ir<'a>(ir: &'a str, marker: &str) -> &'a str {
Expand Down Expand Up @@ -334,6 +334,128 @@ fn numeric_by_construction_local_drops_specialized_clone_root() {
);
}

#[test]
fn guarded_typed_array_clone_keeps_prefix_update_indices_native() {
let read = Function {
id: 1,
name: "read".to_string(),
type_params: Vec::new(),
params: vec![Param {
id: 10,
name: "table".to_string(),
ty: Type::Any,
default: None,
decorators: Vec::new(),
is_rest: false,
arguments_object: None,
}],
return_type: Type::Number,
body: vec![
Stmt::Let {
id: 11,
name: "i".to_string(),
ty: Type::Number,
mutable: true,
init: Some(Expr::Integer(0)),
},
Stmt::Let {
id: 12,
name: "sum".to_string(),
ty: Type::Number,
mutable: true,
init: Some(Expr::Integer(0)),
},
Stmt::While {
condition: Expr::Compare {
op: CompareOp::Lt,
left: Box::new(Expr::LocalGet(11)),
right: Box::new(Expr::Integer(2)),
},
body: vec![
Stmt::Expr(Expr::LocalSet(
12,
Box::new(Expr::Binary {
op: BinaryOp::BitXor,
left: Box::new(Expr::LocalGet(12)),
right: Box::new(Expr::IndexGet {
object: Box::new(Expr::LocalGet(10)),
index: Box::new(Expr::Update {
id: 11,
op: UpdateOp::Increment,
prefix: true,
}),
}),
}),
)),
Stmt::Expr(Expr::LocalSet(
12,
Box::new(Expr::Binary {
op: BinaryOp::BitXor,
left: Box::new(Expr::LocalGet(12)),
right: Box::new(Expr::IndexGet {
object: Box::new(Expr::LocalGet(10)),
index: Box::new(Expr::Update {
id: 11,
op: UpdateOp::Increment,
prefix: true,
}),
}),
}),
)),
],
},
Stmt::Return(Some(Expr::LocalGet(12))),
],
is_async: false,
is_generator: false,
is_strict: true,
is_exported: false,
captures: Vec::new(),
decorators: Vec::new(),
was_plain_async: false,
was_unrolled: false,
};
let mut module = Module::new("prefix_update_typed_array.ts");
module.functions.push(read);
module.init.extend([
Stmt::Let {
id: 20,
name: "table".to_string(),
ty: Type::Any,
mutable: false,
init: Some(Expr::TypedArrayNew {
kind: perry_hir::TYPED_ARRAY_KIND_INT32,
arg: Some(Box::new(Expr::Integer(4))),
}),
},
Stmt::Expr(Expr::Call {
callee: Box::new(Expr::FuncRef(1)),
args: vec![Expr::LocalGet(20)],
type_args: Vec::new(),
byte_offset: 0,
}),
]);

let opts = CompileOptions {
emit_ir_only: true,
output_type: "executable".to_string(),
..Default::default()
};
let ir = String::from_utf8(compile_module(&module, opts).expect("module compiles"))
.expect("LLVM IR is UTF-8");
let specialized = function_ir(&ir, "read$spec_ta4x4(");

assert!(
specialized.contains("load i32"),
"the guarded descriptor clone must load Int32Array elements natively:\n{specialized}"
);
assert!(
!specialized.contains("js_typed_array_index_get_dynamic")
&& !specialized.contains("js_typed_array_get"),
"both prefix-update reads must retain their proven element ranges:\n{specialized}"
);
}

#[test]
fn nonsuspending_async_function_needs_no_direct_call_site_for_its_guarded_clone() {
// An async body with no `await` runs to completion synchronously, so the
Expand Down
52 changes: 46 additions & 6 deletions crates/perry-codegen/src/expr/range_facts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -339,6 +339,22 @@ fn int_range_expr_inner(
| Expr::PodLayoutAlignOf { .. }
| Expr::PodLayoutOffsetOf { .. } => pod_layout_constant_i64(ctx, expr).map(IntRange::exact),
Expr::LocalGet(id) => int_range_for_local(ctx, *id, seen),
// An update expression is both a read and a write, but its returned
// value still has an exact range relative to the value on entry:
// postfix returns the old range, prefix returns the stepped range.
// Keeping that distinction lets a proven loop counter remain an
// integer element key in `table[++i]` without truncating a fractional
// or out-of-range dynamic property key into an i32 index.
Expr::Update { id, op, prefix } => {
let old = int_range_for_local(ctx, *id, seen)?;
if !prefix {
return Some(old);
}
match op {
UpdateOp::Increment => checked_range_add(old, IntRange::exact(1)),
UpdateOp::Decrement => checked_range_sub(old, IntRange::exact(1)),
}
}
Expr::IndexGet { object, index } => int_typed_array_load_range(ctx, object, index, seen),
Expr::Binary { op, left, right } => {
// Result-shape rules that need no range on one (or either)
Expand Down Expand Up @@ -570,12 +586,36 @@ pub(crate) fn invalidate_local_write_facts(ctx: &mut FnCtx<'_>, id: u32) {

pub(crate) fn record_int_facts_for_update(ctx: &mut FnCtx<'_>, id: u32, op: UpdateOp) {
ctx.int_range_aliases.remove(&id);
let remains_nonnegative = match op {
UpdateOp::Increment => ctx.nonnegative_integer_locals.contains(&id),
UpdateOp::Decrement => int_range_for_local(ctx, id, &mut std::collections::HashSet::new())
.is_some_and(|range| range.min >= 1),
};
ctx.int_range_facts.retain(|fact| fact.local_id != id);
let was_nonnegative = ctx.nonnegative_integer_locals.contains(&id);

// A dominating loop/branch fact remains useful after ++/--, but the write
// must not make it narrower. Dropping the fact made the second `P[++i]` in
// a Blowfish round opaque even after the first one had proved `[1, 16]`.
// Widen each active fact to cover both the old and stepped intervals. The
// union is conservative even when an update is conditional: lowering one
// branch cannot make the fact assume that the other branch updated too.
ctx.int_range_facts.retain_mut(|fact| {
if fact.local_id != id {
return true;
}
let shifted = match op {
UpdateOp::Increment => checked_range_add(fact.range, IntRange::exact(1)),
UpdateOp::Decrement => checked_range_sub(fact.range, IntRange::exact(1)),
};
if let Some(range) = shifted {
fact.range = IntRange {
min: fact.range.min.min(range.min),
max: fact.range.max.max(range.max),
};
true
} else {
false
}
});

let active_range = int_range_for_local(ctx, id, &mut std::collections::HashSet::new());
let remains_nonnegative = active_range.is_some_and(IntRange::is_nonnegative)
|| matches!(op, UpdateOp::Increment) && was_nonnegative;
if remains_nonnegative {
ctx.nonnegative_integer_locals.insert(id);
} else {
Expand Down
Loading