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
1 change: 1 addition & 0 deletions changelog.d/13459-jst6-string-slice4.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
String building is cheaper again: split births each piece without a per-piece root and reload when the allocation provably cannot collect, join walks its pieces with a heap-string-first dispatch and inline small copies, string-array length loops are admitted as one call-free region, and concatenations of two to five operands call count-specialized entries. Allocation order, GC schedule and memory use are unchanged.
10 changes: 5 additions & 5 deletions crates/perry-codegen/src/codegen/declared_string_add_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -295,7 +295,7 @@ fn a_string_method_name_on_an_unproven_receiver_is_guarded() {
/// entry that carries literals as constant data (JST slice 3).
fn has_nway_fold(ir: &str) -> bool {
ir.contains("call i64 @js_string_concat_chain(")
|| ir.contains("call i64 @js_string_concat_parts(")
|| ir.contains("call i64 @js_string_concat_parts")
}

#[test]
Expand All @@ -313,7 +313,7 @@ fn a_chain_whose_head_pair_is_all_declared_does_not_fold() {
);
assert!(
!ir.contains("call i64 @js_string_concat_chain(")
&& !ir.contains("call i64 @js_string_concat_parts("),
&& !ir.contains("call i64 @js_string_concat_parts"),
"the head pair carries no proof, so the N-way fold is unsound \
here:\n{ir}"
);
Expand Down Expand Up @@ -385,7 +385,7 @@ fn a_self_append_chain_fuses_the_accumulator_and_suffix() {
);
assert_eq!(
ir.matches("call i64 @js_string_concat_chain(").count()
+ ir.matches("call i64 @js_string_concat_parts(").count(),
+ ir.matches("call i64 @js_string_concat_parts").count(),
0,
"the suffix must not be allocated before it is appended:\n{ir}"
);
Expand Down Expand Up @@ -814,7 +814,7 @@ fn literal_parts_get_no_symbol_check_and_the_value_part_is_tested_inline() {
// check each have exactly one site, for the one value part, and each sits
// in a block reached only through the inline test of the live bits.
let ir = literal_chain(Expr::LocalGet(1), vec![param(1, "n", Type::Number)]);
assert!(ir.contains("call i64 @js_string_concat_parts("), "{ir}");
assert!(ir.contains("call i64 @js_string_concat_parts"), "{ir}");
assert_eq!(
ir.matches("call void @js_add_throw_if_symbol(").count(),
1,
Expand Down Expand Up @@ -879,7 +879,7 @@ fn an_operand_that_cannot_be_a_symbol_carries_no_check_at_all() {
Expr::String("z".to_string()),
),
);
assert!(ir.contains("call i64 @js_string_concat_parts("), "{ir}");
assert!(ir.contains("call i64 @js_string_concat_parts"), "{ir}");
assert_eq!(
ir.matches("call void @js_add_throw_if_symbol(").count(),
1,
Expand Down
17 changes: 17 additions & 0 deletions crates/perry-codegen/src/expr/array_proto_guard.rs
Original file line number Diff line number Diff line change
Expand Up @@ -97,3 +97,20 @@ pub(crate) fn emit_array_element_descriptors(blk: &mut LlBlock, arr_addr: &str)
);
blk.icmp_ne(crate::types::I32, &kind, "0")
}

/// Emit the i1 "an ordinary, unforwarded `GC_TYPE_ARRAY` with no element
/// descriptors" test for the live heap handle `handle` (an i64 address the
/// caller has already placed in the heap-pointer band): the receiver guard of
/// every inline element read. A call-free region reads the dense slots of an
/// array admitted by it.
pub(crate) fn emit_ordinary_array_admission(blk: &mut LlBlock, handle: &str) -> String {
let word = super::index_get::guarded_array::emit_array_guard_word(blk, handle);
super::index_get::guarded_array::emit_array_guard_word_ok(blk, &word, handle)
}

/// `bits - POINTER_TAG - 2 MiB`, compared unsigned against this span, holds
/// exactly for a heap-pointer-tagged value above the runtime-id band: the
/// precondition of reading the header behind it.
pub(crate) const HEAP_POINTER_BAND_BASE_I64: &str = super::receiver_range::RECEIVER_BIAS_LITERAL;
/// The span of the band that [`HEAP_POINTER_BAND_BASE_I64`] biases into.
pub(crate) const HEAP_POINTER_BAND_SPAN_I64: &str = super::receiver_range::RECEIVER_SPAN_LITERAL;
6 changes: 5 additions & 1 deletion crates/perry-codegen/src/expr/index_get/guarded_array.rs
Original file line number Diff line number Diff line change
Expand Up @@ -221,7 +221,11 @@ pub(in crate::expr) fn emit_array_guard_word(

/// `(word & MASK) == EXPECT` and a null named layout: an ordinary array, not
/// forwarded, no element descriptors.
fn emit_array_guard_word_ok(blk: &mut crate::block::LlBlock, word: &str, handle: &str) -> String {
pub(in crate::expr) fn emit_array_guard_word_ok(
blk: &mut crate::block::LlBlock,
word: &str,
handle: &str,
) -> String {
let masked = blk.and(I32, word, ARRAY_READ_GUARD_MASK_I32);
let word_ok = blk.icmp_eq(I32, &masked, ARRAY_READ_GUARD_EXPECT_I32);
emit_and_no_named_layout(blk, &word_ok, handle)
Expand Down
20 changes: 15 additions & 5 deletions crates/perry-codegen/src/lower_string_concat.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1178,11 +1178,21 @@ pub(crate) fn emit_string_concat_parts(ctx: &mut FnCtx<'_>, parts: &[ConcatPartA
}
let base_i64 = blk.next_reg();
blk.emit_raw(format!("{} = ptrtoint ptr {} to i64", base_i64, buf_reg));
let result_handle = blk.call(
I64,
"js_string_concat_parts",
&[(I64, &base_i64), (I32, &n.to_string())],
);
// A count the runtime has a specialized entry for takes it (the count is
// then a constant of the callee); any other count takes the generic entry.
let result_handle = if (2..=5).contains(&n) {
blk.call(
I64,
&format!("js_string_concat_parts_{n}"),
&[(I64, &base_i64)],
)
} else {
blk.call(
I64,
"js_string_concat_parts",
&[(I64, &base_i64), (I32, &n.to_string())],
)
};
nanbox_string_inline(blk, &result_handle)
}

Expand Down
4 changes: 4 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,10 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_string_concat_chain", I64, &[I64, I32]);
// Literal operands as constant data (`string/concat_parts.rs`).
module.declare_function("js_string_concat_parts", I64, &[I64, I32]);
// The same operation for a part count the compiler knows (2..=5).
for n in 2..=5 {
module.declare_function(&format!("js_string_concat_parts_{n}"), I64, &[I64]);
}
module.declare_function("js_string_concat_lit_value_box", DOUBLE, &[I64, I64, DOUBLE]);
module.declare_function("js_to_primitive_default_for_add", DOUBLE, &[DOUBLE]);
module.declare_function("js_add_throw_if_symbol", VOID, &[DOUBLE]);
Expand Down
55 changes: 51 additions & 4 deletions crates/perry-codegen/src/stmt/indexed_owner_loop.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,16 @@ use std::collections::{BTreeMap, BTreeSet};
#[path = "indexed_owner_string.rs"]
mod string;
const STRING_KIND: u8 = 12;
#[path = "indexed_owner_sarray.rs"]
mod sarray;
const SARRAY_KIND: u8 = 13;

#[derive(Default)]
struct Plan {
counter: u32,
arrays: BTreeMap<u32, u8>,
strings: BTreeSet<u32>,
sarrays: BTreeSet<u32>,
aliases: BTreeMap<u32, u32>,
defs: BTreeMap<u32, Expr>,
inputs: BTreeSet<u32>,
Expand Down Expand Up @@ -63,7 +67,7 @@ impl Plan {
.and_then(crate::expr::typed_element::type_kind)
})
.or_else(|| crate::expr::ta_element_read::receiver_kind(ctx, &Expr::LocalGet(root)))?;
if matches!(kind, 9 | 10) || self.strings.contains(&root) {
if matches!(kind, 9 | 10) || self.strings.contains(&root) || self.sarrays.contains(&root) {
return None;
}
self.arrays.insert(root, kind);
Expand All @@ -85,13 +89,33 @@ impl Plan {
if self.body_bindings.contains(&id)
|| !plain_binding(ctx, id)
|| self.arrays.contains_key(&id)
|| self.sarrays.contains(&id)
{
return None;
}
self.strings.insert(id);
Some((id, index))
}

/// `array[i].length` of a declared `string[]` binding.
fn sarray_read<'a>(&mut self, ctx: &FnCtx<'_>, e: &'a Expr) -> Option<(u32, &'a Expr)> {
let (id, index) = sarray::read(e)?;
if !self.sarray_binding(ctx, id) {
return None;
}
self.sarrays.insert(id);
Some((id, index))
}

fn sarray_binding(&self, ctx: &FnCtx<'_>, id: u32) -> bool {
!self.body_bindings.contains(&id)
&& !self.aliases.contains_key(&id)
&& plain_binding(ctx, id)
&& !self.arrays.contains_key(&id)
&& !self.strings.contains(&id)
&& sarray::declared(ctx, id)
}

fn condition(&mut self, ctx: &FnCtx<'_>, e: &Expr) -> bool {
match e {
Expr::Bool(_) => true,
Expand All @@ -110,6 +134,13 @@ impl Plan {
self.indices.push((root, index.clone()));
return true;
}
if let Some((root, index)) = self.sarray_read(ctx, e) {
if !self.numeric(ctx, index) {
return false;
}
self.indices.push((root, index.clone()));
return true;
}
if let Some((root, index)) = self.read(ctx, e) {
if !self.numeric(ctx, index) {
return false;
Expand Down Expand Up @@ -891,6 +922,11 @@ fn numeric(
values: &mut BTreeMap<u32, Number>,
storage: &BTreeMap<u32, Storage>,
) -> Number {
if let Some((id, index)) = sarray::read(e) {
let s = &storage[&id];
let index = index_value(ctx, p, index, values, storage);
return Number::U(sarray::element_length(ctx, s, &index));
}
if let Some((id, index)) = string::read(e) {
let s = &storage[&id];
let index = index_value(ctx, p, index, values, storage);
Expand Down Expand Up @@ -1403,7 +1439,10 @@ pub(super) fn lower(
object, property, ..
} if property == "length" => {
if let Expr::LocalGet(id) = object.as_ref() {
if p.strings.contains(id) {
if p.strings.contains(id) || p.sarrays.contains(id) {
Some(*id)
} else if p.sarray_binding(ctx, *id) {
p.sarrays.insert(*id);
Some(*id)
} else {
p.receiver(ctx, object)
Expand Down Expand Up @@ -1443,11 +1482,12 @@ pub(super) fn lower(
// captured cells. The raw binding and any i32 shadow are flushed on exit.
if p.strings
.iter()
.chain(p.sarrays.iter())
.any(|id| p.inputs.contains(id) || p.writes.contains(id))
{
return Ok(false);
}
if !p.strings.is_empty() {
if !p.strings.is_empty() || !p.sarrays.is_empty() {
// The original condition/body can drain work deferred by a preceding
// allocation even though this admitted region cannot allocate. Keep
// that entry safepoint before loading any owner or raw string bytes;
Expand Down Expand Up @@ -1486,13 +1526,17 @@ pub(super) fn lower(
for id in &p.strings {
storage.insert(*id, string::admit(ctx, *id, &slow_l));
}
for id in &p.sarrays {
storage.insert(*id, sarray::admit(ctx, *id, &slow_l));
}
// Read-only regions require no alias separation. For writes, allocation
// identities can prove separation before falling back to live spans.
if !p.stores.is_empty() {
disjoint_storage(ctx, &mut storage, &slow_l);
}
if let Some(id) = length_receiver.filter(|id| {
storage[id].kind != STRING_KIND
&& storage[id].kind != SARRAY_KIND
&& !(ctx.wpfacts.indexed_metadata_pristine
&& owner_proof(ctx, *id, storage[id].kind).is_some())
}) {
Expand Down Expand Up @@ -1703,7 +1747,10 @@ pub(super) fn lower(
false,
false,
vec![
format!("owners={:?};ascii_strings={:?}", p.arrays, p.strings),
format!(
"owners={:?};ascii_strings={:?};string_arrays={:?}",
p.arrays, p.strings, p.sarrays
),
format!("index_windows={}", indices.len()),
format!(
"dynamic_entry_guards={dynamic_entry_guards};nested_loops={}",
Expand Down
91 changes: 89 additions & 2 deletions crates/perry-codegen/src/stmt/indexed_owner_loop_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,14 @@ fn ir(kind: &str, body: Vec<Stmt>) -> String {
ir_with_facts(kind, body, Default::default())
}
fn ir_with_facts(kind: &str, body: Vec<Stmt>, facts: crate::codegen::ElisionFacts) -> String {
ir_with_param_type(Type::Named(kind.into()), body, facts)
}
fn ir_with_param_type(
receiver: Type,
body: Vec<Stmt>,
facts: crate::codegen::ElisionFacts,
) -> String {
let kind = receiver;
let mut m = Module::new("indexed_owner_contract");
let param = |id, name: &str, ty| Param {
id,
Expand All @@ -46,9 +54,9 @@ fn ir_with_facts(kind: &str, body: Vec<Stmt>, facts: crate::codegen::ElisionFact
name: "operate".into(),
type_params: vec![],
params: vec![
param(1, "a", Type::Named(kind.into())),
param(1, "a", kind.clone()),
param(2, "scale", Type::Any),
param(7, "b", Type::Named(kind.into())),
param(7, "b", kind.clone()),
],
return_type: Type::Any,
body: vec![
Expand Down Expand Up @@ -475,3 +483,82 @@ fn string_region_rejects_rebinding_and_calls_before_any_body_effect() {
assert!(!ir("string", body).contains("indexed.string.storage"));
}
}

fn string_array_length(index: Expr) -> Expr {
Expr::PropertyGet {
object: Box::new(index_get_of(1, index)),
property: "length".into(),
byte_offset: 0,
byte_end: 0,
}
}
fn index_get_of(a: u32, i: Expr) -> Expr {
index(a, i)
}
fn string_array() -> Type {
Type::Array(Box::new(Type::String))
}

// JST slice 4 (F): `lenSum += pieces[i].length` over a declared `string[]`.
#[test]
fn string_array_length_region_scans_the_slots_once_and_stays_call_free() {
let body = vec![Stmt::Expr(Expr::LocalSet(
2,
Box::new(bin(BinaryOp::Add, get(2), string_array_length(get(3)))),
))];
let ir = ir_with_param_type(string_array(), body, Default::default());
assert!(ir.contains("indexed.sarray.scan"), "{ir}");
assert!(ir.contains("indexed.owner.slow"), "{ir}");
let admission = ir.find("indexed.sarray.scan").unwrap();
let first_poll = ir
.find("call void @js_gc_loop_safepoint()")
.expect("pending GC work must be serviced before the array is read");
assert!(
first_poll < admission,
"the entry poll must precede the slot scan: {ir}"
);
let fast = ir
.split("\nindexed.owner.fast.")
.nth(1)
.unwrap()
.split("\nindexed.owner.exit.")
.next()
.unwrap();
assert!(
!fast.contains("call "),
"the string-array region stays call-free: {fast}"
);
// The per-element read is one slot load and one header load.
assert!(fast.contains("lshr i64"), "slot tag test: {fast}");
}

#[test]
fn string_array_length_region_rejects_stores_and_calls_before_any_body_effect() {
for body in [
// The array is rebound in the body.
vec![
Stmt::Expr(Expr::LocalSet(1, Box::new(Expr::Integer(4)))),
Stmt::Expr(string_array_length(get(3))),
],
// A call can run user code between two element reads.
vec![
Stmt::Expr(string_array_length(get(3))),
Stmt::Expr(Expr::Call {
byte_end: 0,
callee: Box::new(get(2)),
args: vec![],
type_args: vec![],
byte_offset: 0,
}),
],
// An element store.
vec![Stmt::Expr(Expr::IndexSet {
object: Box::new(get(1)),
index: Box::new(get(3)),
value: Box::new(Expr::String("x".into())),
})],
] {
let ir = ir_with_param_type(string_array(), body, Default::default());
assert!(!ir.contains("indexed.sarray.scan"), "{ir}");
}
}
Loading
Loading