From 1d99efabecf41536fce92101c601dca0aa4dfbff Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Wed, 17 Jun 2026 18:54:25 +0000 Subject: [PATCH 1/2] Inline Uint8Array access for const local lengths --- .../perry-codegen/src/collectors/hir_facts.rs | 95 +++++++++++++++---- .../tests/native_proof_buffer_views.rs | 46 +++++++++ 2 files changed, 125 insertions(+), 16 deletions(-) diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index 3328b6fa5..af9d7bde7 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -314,11 +314,25 @@ pub(crate) fn collect_hir_facts( fn collect_known_noalias_buffer_locals(stmts: &[Stmt]) -> HashSet { let mut out = HashSet::new(); - collect_owned_buffer_lets(stmts, &mut out); + let mut known_length_locals = HashSet::new(); + collect_owned_buffer_lets(stmts, &mut out, &mut known_length_locals); out } -fn collect_owned_buffer_lets(stmts: &[Stmt], out: &mut HashSet) { +fn collect_owned_buffer_lets_child_scope( + stmts: &[Stmt], + out: &mut HashSet, + known_length_locals: &HashSet, +) { + let mut child_length_locals = known_length_locals.clone(); + collect_owned_buffer_lets(stmts, out, &mut child_length_locals); +} + +fn collect_owned_buffer_lets( + stmts: &[Stmt], + out: &mut HashSet, + known_length_locals: &mut HashSet, +) { for stmt in stmts { match stmt { Stmt::Let { @@ -327,48 +341,62 @@ fn collect_owned_buffer_lets(stmts: &[Stmt], out: &mut HashSet) { init: Some(init), .. } => { - if !*mutable && is_owned_u8_buffer_alloc(init) { + if !*mutable && is_owned_u8_buffer_alloc(init, known_length_locals) { out.insert(*id); } + if !*mutable && is_fresh_uint8array_length_expr(init, known_length_locals) { + known_length_locals.insert(*id); + } else { + known_length_locals.remove(id); + } } Stmt::If { then_branch, else_branch, .. } => { - collect_owned_buffer_lets(then_branch, out); + collect_owned_buffer_lets_child_scope(then_branch, out, known_length_locals); if let Some(else_branch) = else_branch { - collect_owned_buffer_lets(else_branch, out); + collect_owned_buffer_lets_child_scope(else_branch, out, known_length_locals); } } Stmt::While { body, .. } | Stmt::DoWhile { body, .. } => { - collect_owned_buffer_lets(body, out); + collect_owned_buffer_lets_child_scope(body, out, known_length_locals); } Stmt::For { init, body, .. } => { + let mut loop_length_locals = known_length_locals.clone(); if let Some(init) = init { - collect_owned_buffer_lets(std::slice::from_ref(init.as_ref()), out); + collect_owned_buffer_lets( + std::slice::from_ref(init.as_ref()), + out, + &mut loop_length_locals, + ); } - collect_owned_buffer_lets(body, out); + collect_owned_buffer_lets(body, out, &mut loop_length_locals); } Stmt::Labeled { body, .. } => { - collect_owned_buffer_lets(std::slice::from_ref(body.as_ref()), out); + collect_owned_buffer_lets_child_scope( + std::slice::from_ref(body.as_ref()), + out, + known_length_locals, + ); } Stmt::Try { body, catch, finally, } => { - collect_owned_buffer_lets(body, out); + collect_owned_buffer_lets_child_scope(body, out, known_length_locals); if let Some(catch) = catch { - collect_owned_buffer_lets(&catch.body, out); + collect_owned_buffer_lets_child_scope(&catch.body, out, known_length_locals); } if let Some(finally) = finally { - collect_owned_buffer_lets(finally, out); + collect_owned_buffer_lets_child_scope(finally, out, known_length_locals); } } Stmt::Switch { cases, .. } => { for case in cases { - collect_owned_buffer_lets(&case.body, out); + collect_owned_buffer_lets_child_scope(&case.body, out, known_length_locals); } } Stmt::Let { init: None, .. } @@ -384,15 +412,17 @@ fn collect_owned_buffer_lets(stmts: &[Stmt], out: &mut HashSet) { } } -fn is_owned_u8_buffer_alloc(expr: &Expr) -> bool { +fn is_owned_u8_buffer_alloc(expr: &Expr, known_length_locals: &HashSet) -> bool { match expr { Expr::BufferAlloc { .. } | Expr::BufferAllocUnsafe(_) => true, Expr::Uint8ArrayNew(None) => true, - Expr::Uint8ArrayNew(Some(size)) => is_fresh_uint8array_length_literal(size), + Expr::Uint8ArrayNew(Some(size)) => { + is_fresh_uint8array_length_expr(size, known_length_locals) + } Expr::TypedArrayNew { arg: None, .. } => true, Expr::TypedArrayNew { arg: Some(size), .. - } => is_fresh_uint8array_length_literal(size), + } => is_fresh_uint8array_length_expr(size, known_length_locals), Expr::NativeMethodCall { module, method, @@ -404,6 +434,13 @@ fn is_owned_u8_buffer_alloc(expr: &Expr) -> bool { } } +fn is_fresh_uint8array_length_expr(expr: &Expr, known_length_locals: &HashSet) -> bool { + match expr { + Expr::LocalGet(id) => known_length_locals.contains(id), + _ => is_fresh_uint8array_length_literal(expr), + } +} + fn is_fresh_uint8array_length_literal(expr: &Expr) -> bool { match expr { Expr::Integer(n) => *n >= 0 && *n < i32::MAX as i64, @@ -428,6 +465,16 @@ mod tests { } } + fn const_number_let(id: u32, init: Expr) -> Stmt { + Stmt::Let { + id, + name: format!("v{}", id), + ty: Type::Number, + mutable: false, + init: Some(init), + } + } + fn known_ids(stmts: Vec) -> HashSet { collect_known_noalias_buffer_locals(&stmts) } @@ -463,6 +510,20 @@ mod tests { assert!(ids.contains(&3)); } + #[test] + fn uint8array_const_local_lengths_are_known_noalias_sources() { + let ids = known_ids(vec![ + const_number_let(10, Expr::Integer(8)), + const_let(1, Expr::Uint8ArrayNew(Some(Box::new(Expr::LocalGet(10))))), + const_number_let(11, Expr::Number(16.0)), + const_number_let(12, Expr::LocalGet(11)), + const_let(2, Expr::Uint8ArrayNew(Some(Box::new(Expr::LocalGet(12))))), + ]); + + assert!(ids.contains(&1)); + assert!(ids.contains(&2)); + } + #[test] fn uint8array_non_literal_or_alias_possible_sources_are_not_noalias() { let ids = known_ids(vec![ @@ -474,6 +535,8 @@ mod tests { 5, Expr::Uint8ArrayNew(Some(Box::new(Expr::Number(i32::MAX as f64)))), ), + mutable_number_let(6, Expr::Integer(8)), + const_let(7, Expr::Uint8ArrayNew(Some(Box::new(Expr::LocalGet(6))))), ]); assert!(ids.is_empty(), "unexpected noalias ids: {ids:?}"); diff --git a/crates/perry-codegen/tests/native_proof_buffer_views.rs b/crates/perry-codegen/tests/native_proof_buffer_views.rs index 0aa3e18cc..53dce67ce 100644 --- a/crates/perry-codegen/tests/native_proof_buffer_views.rs +++ b/crates/perry-codegen/tests/native_proof_buffer_views.rs @@ -1584,6 +1584,52 @@ fn native_owned_uint8array_set_fallback_uses_uint8array_helper() { ); } +#[test] +fn uint8array_const_local_length_uses_inline_byte_get_set() { + let ir = compile_ir( + "uint8array_const_local_length_inline_byte_access.ts", + vec![ + number_let(1, "size", false, int(16)), + Stmt::Let { + id: 2, + name: "buf".to_string(), + ty: Type::Named("Uint8Array".to_string()), + mutable: false, + init: Some(Expr::Uint8ArrayNew(Some(Box::new(local(1))))), + }, + for_loop( + 3, + local(1), + vec![Stmt::Expr(Expr::Uint8ArraySet { + array: Box::new(local(2)), + index: Box::new(local(3)), + value: Box::new(local(3)), + })], + ), + Stmt::Return(Some(Expr::Uint8ArrayGet { + array: Box::new(local(2)), + index: Box::new(int(0)), + })), + ], + ); + assert!( + ir.contains("store i8"), + "bounded Uint8Array set should lower to an inline byte store:\n{ir}" + ); + assert!( + ir.contains("load i8"), + "bounded Uint8Array get should lower to an inline byte load:\n{ir}" + ); + assert!( + !ir.contains("call void @js_uint8array_set"), + "inline Uint8Array set should not call the runtime helper:\n{ir}" + ); + assert!( + !ir.contains("call i32 @js_uint8array_get"), + "inline Uint8Array get should not call the runtime helper:\n{ir}" + ); +} + #[test] fn native_owned_typed_array_fallback_reasons_are_explicit() { let disposed = compile_artifact_json( From 25194e1da53096268e296bc7fa99b4621f1f70f4 Mon Sep 17 00:00:00 2001 From: Andrew DiZenzo Date: Wed, 17 Jun 2026 23:21:37 +0000 Subject: [PATCH 2/2] Skip pure unused scalar literal initializers --- crates/perry-codegen/src/codegen/closure.rs | 4 + crates/perry-codegen/src/codegen/entry.rs | 12 ++ crates/perry-codegen/src/codegen/function.rs | 4 + crates/perry-codegen/src/codegen/method.rs | 8 + .../src/collectors/escape_arrays.rs | 141 +++++++++++++- .../src/collectors/escape_objects.rs | 183 +++++++++++++++++- .../perry-codegen/src/collectors/hir_facts.rs | 19 ++ crates/perry-codegen/src/expr/mod.rs | 4 + crates/perry-codegen/src/stmt/let_stmt.rs | 110 +++++++++++ .../tests/typed_shape_descriptors.rs | 146 ++++++++++++++ 10 files changed, 629 insertions(+), 2 deletions(-) diff --git a/crates/perry-codegen/src/codegen/closure.rs b/crates/perry-codegen/src/codegen/closure.rs index 4de67dc93..e27b02d9e 100644 --- a/crates/perry-codegen/src/codegen/closure.rs +++ b/crates/perry-codegen/src/codegen/closure.rs @@ -345,7 +345,11 @@ pub(super) fn compile_closure( non_escaping_news: native_facts.non_escaping_news().clone(), non_escaping_new_used_fields: native_facts.non_escaping_new_used_fields().clone(), non_escaping_arrays: native_facts.non_escaping_arrays().clone(), + non_escaping_array_used_indices: native_facts.non_escaping_array_used_indices().clone(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), + non_escaping_object_literal_used_fields: native_facts + .non_escaping_object_literal_used_fields() + .clone(), flat_const_arrays: &cross_module.flat_const_arrays, array_row_aliases: HashMap::new(), clamp3_functions: &cross_module.clamp3_functions, diff --git a/crates/perry-codegen/src/codegen/entry.rs b/crates/perry-codegen/src/codegen/entry.rs index 862b23e10..97ee5260b 100644 --- a/crates/perry-codegen/src/codegen/entry.rs +++ b/crates/perry-codegen/src/codegen/entry.rs @@ -447,7 +447,13 @@ pub(super) fn compile_module_entry( non_escaping_news: main_native_facts.non_escaping_news().clone(), non_escaping_new_used_fields: main_native_facts.non_escaping_new_used_fields().clone(), non_escaping_arrays: main_native_facts.non_escaping_arrays().clone(), + non_escaping_array_used_indices: main_native_facts + .non_escaping_array_used_indices() + .clone(), non_escaping_object_literals: main_native_facts.non_escaping_object_literals().clone(), + non_escaping_object_literal_used_fields: main_native_facts + .non_escaping_object_literal_used_fields() + .clone(), flat_const_arrays: &cross_module.flat_const_arrays, array_row_aliases: HashMap::new(), clamp3_functions: &cross_module.clamp3_functions, @@ -887,7 +893,13 @@ pub(super) fn compile_module_entry( non_escaping_news: init_native_facts.non_escaping_news().clone(), non_escaping_new_used_fields: init_native_facts.non_escaping_new_used_fields().clone(), non_escaping_arrays: init_native_facts.non_escaping_arrays().clone(), + non_escaping_array_used_indices: init_native_facts + .non_escaping_array_used_indices() + .clone(), non_escaping_object_literals: init_native_facts.non_escaping_object_literals().clone(), + non_escaping_object_literal_used_fields: init_native_facts + .non_escaping_object_literal_used_fields() + .clone(), flat_const_arrays: &cross_module.flat_const_arrays, array_row_aliases: HashMap::new(), clamp3_functions: &cross_module.clamp3_functions, diff --git a/crates/perry-codegen/src/codegen/function.rs b/crates/perry-codegen/src/codegen/function.rs index 2c2b1c7ea..6c0f8fb3b 100644 --- a/crates/perry-codegen/src/codegen/function.rs +++ b/crates/perry-codegen/src/codegen/function.rs @@ -247,7 +247,11 @@ pub(super) fn compile_function( non_escaping_news: native_facts.non_escaping_news().clone(), non_escaping_new_used_fields: native_facts.non_escaping_new_used_fields().clone(), non_escaping_arrays: native_facts.non_escaping_arrays().clone(), + non_escaping_array_used_indices: native_facts.non_escaping_array_used_indices().clone(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), + non_escaping_object_literal_used_fields: native_facts + .non_escaping_object_literal_used_fields() + .clone(), flat_const_arrays: &cross_module.flat_const_arrays, array_row_aliases: HashMap::new(), clamp3_functions: &cross_module.clamp3_functions, diff --git a/crates/perry-codegen/src/codegen/method.rs b/crates/perry-codegen/src/codegen/method.rs index 006b1e486..da08e3e0a 100644 --- a/crates/perry-codegen/src/codegen/method.rs +++ b/crates/perry-codegen/src/codegen/method.rs @@ -234,7 +234,11 @@ pub(super) fn compile_method( non_escaping_news: native_facts.non_escaping_news().clone(), non_escaping_new_used_fields: native_facts.non_escaping_new_used_fields().clone(), non_escaping_arrays: native_facts.non_escaping_arrays().clone(), + non_escaping_array_used_indices: native_facts.non_escaping_array_used_indices().clone(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), + non_escaping_object_literal_used_fields: native_facts + .non_escaping_object_literal_used_fields() + .clone(), flat_const_arrays: &cross_module.flat_const_arrays, array_row_aliases: HashMap::new(), clamp3_functions: &cross_module.clamp3_functions, @@ -771,7 +775,11 @@ pub(super) fn compile_static_method( non_escaping_news: native_facts.non_escaping_news().clone(), non_escaping_new_used_fields: native_facts.non_escaping_new_used_fields().clone(), non_escaping_arrays: native_facts.non_escaping_arrays().clone(), + non_escaping_array_used_indices: native_facts.non_escaping_array_used_indices().clone(), non_escaping_object_literals: native_facts.non_escaping_object_literals().clone(), + non_escaping_object_literal_used_fields: native_facts + .non_escaping_object_literal_used_fields() + .clone(), flat_const_arrays: &cross_module.flat_const_arrays, array_row_aliases: HashMap::new(), clamp3_functions: &cross_module.clamp3_functions, diff --git a/crates/perry-codegen/src/collectors/escape_arrays.rs b/crates/perry-codegen/src/collectors/escape_arrays.rs index 9b18ddc6c..5cb90d8a1 100644 --- a/crates/perry-codegen/src/collectors/escape_arrays.rs +++ b/crates/perry-codegen/src/collectors/escape_arrays.rs @@ -1,5 +1,5 @@ use perry_hir::{BinaryOp, Expr, Function, Stmt}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use super::*; @@ -22,6 +22,145 @@ pub fn collect_non_escaping_arrays( candidates } +pub fn collect_non_escaping_array_used_indices( + stmts: &[perry_hir::Stmt], + non_escaping_arrays: &HashMap, +) -> HashMap> { + let mut used = HashMap::new(); + if non_escaping_arrays.is_empty() { + return used; + } + collect_used_array_indices_in_stmts(stmts, non_escaping_arrays, &mut used); + used +} + +fn collect_used_array_indices_in_stmts( + stmts: &[perry_hir::Stmt], + non_escaping_arrays: &HashMap, + used: &mut HashMap>, +) { + use perry_hir::Stmt; + for stmt in stmts { + match stmt { + Stmt::Let { init, .. } => { + if let Some(expr) = init { + collect_used_array_indices_in_expr(expr, non_escaping_arrays, used); + } + } + Stmt::Expr(expr) | Stmt::Throw(expr) => { + collect_used_array_indices_in_expr(expr, non_escaping_arrays, used); + } + Stmt::Return(expr) => { + if let Some(expr) = expr { + collect_used_array_indices_in_expr(expr, non_escaping_arrays, used); + } + } + Stmt::If { + condition, + then_branch, + else_branch, + } => { + collect_used_array_indices_in_expr(condition, non_escaping_arrays, used); + collect_used_array_indices_in_stmts(then_branch, non_escaping_arrays, used); + if let Some(else_branch) = else_branch { + collect_used_array_indices_in_stmts(else_branch, non_escaping_arrays, used); + } + } + Stmt::While { condition, body } => { + collect_used_array_indices_in_expr(condition, non_escaping_arrays, used); + collect_used_array_indices_in_stmts(body, non_escaping_arrays, used); + } + Stmt::DoWhile { body, condition } => { + collect_used_array_indices_in_stmts(body, non_escaping_arrays, used); + collect_used_array_indices_in_expr(condition, non_escaping_arrays, used); + } + Stmt::For { + init, + condition, + update, + body, + } => { + if let Some(init_stmt) = init { + collect_used_array_indices_in_stmts( + std::slice::from_ref(init_stmt.as_ref()), + non_escaping_arrays, + used, + ); + } + if let Some(condition) = condition { + collect_used_array_indices_in_expr(condition, non_escaping_arrays, used); + } + if let Some(update) = update { + collect_used_array_indices_in_expr(update, non_escaping_arrays, used); + } + collect_used_array_indices_in_stmts(body, non_escaping_arrays, used); + } + Stmt::Labeled { body, .. } => { + collect_used_array_indices_in_stmts( + std::slice::from_ref(body.as_ref()), + non_escaping_arrays, + used, + ); + } + Stmt::Try { + body, + catch, + finally, + } => { + collect_used_array_indices_in_stmts(body, non_escaping_arrays, used); + if let Some(catch) = catch { + collect_used_array_indices_in_stmts(&catch.body, non_escaping_arrays, used); + } + if let Some(finally) = finally { + collect_used_array_indices_in_stmts(finally, non_escaping_arrays, used); + } + } + Stmt::Switch { + discriminant, + cases, + } => { + collect_used_array_indices_in_expr(discriminant, non_escaping_arrays, used); + for case in cases { + if let Some(test) = &case.test { + collect_used_array_indices_in_expr(test, non_escaping_arrays, used); + } + collect_used_array_indices_in_stmts(&case.body, non_escaping_arrays, used); + } + } + Stmt::Break + | Stmt::Continue + | Stmt::LabeledBreak(_) + | Stmt::LabeledContinue(_) + | Stmt::PreallocateBoxes(_) => {} + } + } +} + +fn collect_used_array_indices_in_expr( + expr: &perry_hir::Expr, + non_escaping_arrays: &HashMap, + used: &mut HashMap>, +) { + use perry_hir::Expr; + if let Expr::IndexGet { object, index } = expr { + if let Expr::LocalGet(id) = object.as_ref() { + if let Some(&len) = non_escaping_arrays.get(id) { + if let Some(k) = const_index(index) { + if k < len { + used.entry(*id).or_default().insert(k); + } + } + } + } + } + if let Expr::Closure { body, .. } = expr { + collect_used_array_indices_in_stmts(body, non_escaping_arrays, used); + } + perry_hir::walker::walk_expr_children(expr, &mut |child| { + collect_used_array_indices_in_expr(child, non_escaping_arrays, used); + }); +} + pub fn find_array_candidates( stmts: &[perry_hir::Stmt], boxed_vars: &HashSet, diff --git a/crates/perry-codegen/src/collectors/escape_objects.rs b/crates/perry-codegen/src/collectors/escape_objects.rs index 6272329c9..c6d20fbfb 100644 --- a/crates/perry-codegen/src/collectors/escape_objects.rs +++ b/crates/perry-codegen/src/collectors/escape_objects.rs @@ -1,5 +1,5 @@ use perry_hir::{BinaryOp, Expr, Function, Stmt}; -use std::collections::HashSet; +use std::collections::{HashMap, HashSet}; use super::*; @@ -23,6 +23,187 @@ pub fn collect_non_escaping_object_literals( candidates } +pub fn collect_non_escaping_object_literal_used_fields( + stmts: &[perry_hir::Stmt], + non_escaping_object_literals: &HashMap>, +) -> HashMap> { + let mut used = HashMap::new(); + if non_escaping_object_literals.is_empty() { + return used; + } + collect_used_object_fields_in_stmts(stmts, non_escaping_object_literals, &mut used); + used +} + +fn collect_used_object_fields_in_stmts( + stmts: &[perry_hir::Stmt], + non_escaping_object_literals: &HashMap>, + used: &mut HashMap>, +) { + use perry_hir::Stmt; + for stmt in stmts { + match stmt { + Stmt::Let { init, .. } => { + if let Some(expr) = init { + collect_used_object_fields_in_expr(expr, non_escaping_object_literals, used); + } + } + Stmt::Expr(expr) | Stmt::Throw(expr) => { + collect_used_object_fields_in_expr(expr, non_escaping_object_literals, used); + } + Stmt::Return(expr) => { + if let Some(expr) = expr { + collect_used_object_fields_in_expr(expr, non_escaping_object_literals, used); + } + } + Stmt::If { + condition, + then_branch, + else_branch, + } => { + collect_used_object_fields_in_expr(condition, non_escaping_object_literals, used); + collect_used_object_fields_in_stmts( + then_branch, + non_escaping_object_literals, + used, + ); + if let Some(else_branch) = else_branch { + collect_used_object_fields_in_stmts( + else_branch, + non_escaping_object_literals, + used, + ); + } + } + Stmt::While { condition, body } => { + collect_used_object_fields_in_expr(condition, non_escaping_object_literals, used); + collect_used_object_fields_in_stmts(body, non_escaping_object_literals, used); + } + Stmt::DoWhile { body, condition } => { + collect_used_object_fields_in_stmts(body, non_escaping_object_literals, used); + collect_used_object_fields_in_expr(condition, non_escaping_object_literals, used); + } + Stmt::For { + init, + condition, + update, + body, + } => { + if let Some(init_stmt) = init { + collect_used_object_fields_in_stmts( + std::slice::from_ref(init_stmt.as_ref()), + non_escaping_object_literals, + used, + ); + } + if let Some(condition) = condition { + collect_used_object_fields_in_expr( + condition, + non_escaping_object_literals, + used, + ); + } + if let Some(update) = update { + collect_used_object_fields_in_expr(update, non_escaping_object_literals, used); + } + collect_used_object_fields_in_stmts(body, non_escaping_object_literals, used); + } + Stmt::Labeled { body, .. } => { + collect_used_object_fields_in_stmts( + std::slice::from_ref(body.as_ref()), + non_escaping_object_literals, + used, + ); + } + Stmt::Try { + body, + catch, + finally, + } => { + collect_used_object_fields_in_stmts(body, non_escaping_object_literals, used); + if let Some(catch) = catch { + collect_used_object_fields_in_stmts( + &catch.body, + non_escaping_object_literals, + used, + ); + } + if let Some(finally) = finally { + collect_used_object_fields_in_stmts( + finally, + non_escaping_object_literals, + used, + ); + } + } + Stmt::Switch { + discriminant, + cases, + } => { + collect_used_object_fields_in_expr( + discriminant, + non_escaping_object_literals, + used, + ); + for case in cases { + if let Some(test) = &case.test { + collect_used_object_fields_in_expr( + test, + non_escaping_object_literals, + used, + ); + } + collect_used_object_fields_in_stmts( + &case.body, + non_escaping_object_literals, + used, + ); + } + } + Stmt::Break + | Stmt::Continue + | Stmt::LabeledBreak(_) + | Stmt::LabeledContinue(_) + | Stmt::PreallocateBoxes(_) => {} + } + } +} + +fn collect_used_object_fields_in_expr( + expr: &perry_hir::Expr, + non_escaping_object_literals: &HashMap>, + used: &mut HashMap>, +) { + use perry_hir::Expr; + if let Expr::PropertyGet { object, property } = expr { + if let Expr::LocalGet(id) = object.as_ref() { + if let Some(fields) = non_escaping_object_literals.get(id) { + if fields.iter().any(|field| field == property) { + used.entry(*id).or_default().insert(property.clone()); + } + } + } + } + if let Expr::PropertyUpdate { + object, property, .. + } = expr + { + if let Expr::LocalGet(id) = object.as_ref() { + if let Some(fields) = non_escaping_object_literals.get(id) { + if fields.iter().any(|field| field == property) { + used.entry(*id).or_default().insert(property.clone()); + } + } + } + } + if let Expr::Closure { body, .. } = expr { + collect_used_object_fields_in_stmts(body, non_escaping_object_literals, used); + } + perry_hir::walker::walk_expr_children(expr, &mut |child| { + collect_used_object_fields_in_expr(child, non_escaping_object_literals, used); + }); +} + pub fn find_object_literal_candidates( stmts: &[perry_hir::Stmt], boxed_vars: &HashSet, diff --git a/crates/perry-codegen/src/collectors/hir_facts.rs b/crates/perry-codegen/src/collectors/hir_facts.rs index af9d7bde7..01360446c 100644 --- a/crates/perry-codegen/src/collectors/hir_facts.rs +++ b/crates/perry-codegen/src/collectors/hir_facts.rs @@ -56,7 +56,9 @@ pub(crate) struct EscapeFacts { pub non_escaping_news: HashMap, pub non_escaping_new_used_fields: HashMap>, pub non_escaping_arrays: HashMap, + pub non_escaping_array_used_indices: HashMap>, pub non_escaping_object_literals: HashMap>, + pub non_escaping_object_literal_used_fields: HashMap>, } #[derive(Debug, Clone, Default)] @@ -127,10 +129,18 @@ impl TypeFacts { &self.escape.non_escaping_arrays } + pub(crate) fn non_escaping_array_used_indices(&self) -> &HashMap> { + &self.escape.non_escaping_array_used_indices + } + pub(crate) fn non_escaping_object_literals(&self) -> &HashMap> { &self.escape.non_escaping_object_literals } + pub(crate) fn non_escaping_object_literal_used_fields(&self) -> &HashMap> { + &self.escape.non_escaping_object_literal_used_fields + } + pub(crate) fn materialization_hazard_locals(&self) -> &HashSet { &self.materialization_hazards.initially_known_hazard_locals } @@ -220,11 +230,18 @@ pub(crate) fn collect_type_facts( super::escape_news::collect_non_escaping_new_used_fields(stmts, &non_escaping_news); let non_escaping_arrays = super::escape_arrays::collect_non_escaping_arrays(stmts, boxed_vars, module_globals); + let non_escaping_array_used_indices = + super::escape_arrays::collect_non_escaping_array_used_indices(stmts, &non_escaping_arrays); let non_escaping_object_literals = super::escape_objects::collect_non_escaping_object_literals( stmts, boxed_vars, module_globals, ); + let non_escaping_object_literal_used_fields = + super::escape_objects::collect_non_escaping_object_literal_used_fields( + stmts, + &non_escaping_object_literals, + ); let scalar_replaceable_object_locals = non_escaping_news .keys() .chain(non_escaping_object_literals.keys()) @@ -249,7 +266,9 @@ pub(crate) fn collect_type_facts( non_escaping_news, non_escaping_new_used_fields, non_escaping_arrays, + non_escaping_array_used_indices, non_escaping_object_literals, + non_escaping_object_literal_used_fields, }, purity: PurityFacts { pure_helper_function_ids: clamp_fn_ids.clone(), diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index ddf9e9652..ae3902532 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -795,6 +795,8 @@ pub(crate) struct FnCtx<'a> { /// `let arr = [a, b, c]` and emit per-index allocas instead of a /// heap array, and by `.length` reads to fold to the constant. pub non_escaping_arrays: std::collections::HashMap, + pub non_escaping_array_used_indices: + std::collections::HashMap>, /// Non-escaping object literals identified by escape analysis. Maps /// local_id → field names (declaration order, deduplicated). Used by @@ -803,6 +805,8 @@ pub(crate) struct FnCtx<'a> { /// already resolve through `scalar_replaced`, so no separate read path /// is required. pub non_escaping_object_literals: std::collections::HashMap>, + pub non_escaping_object_literal_used_fields: + std::collections::HashMap>, /// (Issue #50) Module-level const 2D int arrays folded into a flat /// `[N x i32]` LLVM constant. Maps local_id → (flat_global_name, rows, diff --git a/crates/perry-codegen/src/stmt/let_stmt.rs b/crates/perry-codegen/src/stmt/let_stmt.rs index e0ec3144f..cdecdbfd8 100644 --- a/crates/perry-codegen/src/stmt/let_stmt.rs +++ b/crates/perry-codegen/src/stmt/let_stmt.rs @@ -389,7 +389,16 @@ pub(crate) fn lower_let( // result into its slot. Order matches source, so any // side effects stay observable in the same sequence the // heap-allocating path would have produced. + let used_indices = ctx + .non_escaping_array_used_indices + .get(&id) + .cloned() + .unwrap_or_default(); for (i, elem) in elements.iter().enumerate() { + let index_is_observed = used_indices.contains(&(i as u32)); + if !index_is_observed && lower_unused_expr(ctx, elem)? { + continue; + } let v = lower_expr(ctx, elem)?; ctx.block().store(DOUBLE, &v, &slots[i]); let lowered = LoweredValue { @@ -446,7 +455,15 @@ pub(crate) fn lower_let( // order — duplicate keys naturally do last-write-wins // because they share a slot. Side effects of each value // expression stay observable in declaration order. + let used_fields = ctx + .non_escaping_object_literal_used_fields + .get(&id) + .cloned() + .unwrap_or_default(); for (key, value_expr) in props { + if !used_fields.contains(key) && lower_unused_expr(ctx, value_expr)? { + continue; + } let v = lower_expr(ctx, value_expr)?; if let Some(slot) = field_slots.get(key).cloned() { ctx.block().store(DOUBLE, &v, &slot); @@ -1477,6 +1494,9 @@ pub(crate) fn collect_scalar_class_data( } fn lower_unused_expr(ctx: &mut FnCtx<'_>, expr: &perry_hir::Expr) -> Result { + if unused_expr_is_pure_nonthrowing(ctx, expr) { + return Ok(true); + } match expr { perry_hir::Expr::New { class_name, args, .. @@ -1522,6 +1542,96 @@ fn lower_unused_expr(ctx: &mut FnCtx<'_>, expr: &perry_hir::Expr) -> Result, expr: &perry_hir::Expr) -> bool { + match expr { + perry_hir::Expr::Undefined + | perry_hir::Expr::Null + | perry_hir::Expr::Bool(_) + | perry_hir::Expr::Number(_) + | perry_hir::Expr::Integer(_) + | perry_hir::Expr::String(_) + | perry_hir::Expr::WtfString(_) + | perry_hir::Expr::LocalGet(_) => true, + perry_hir::Expr::Unary { operand, .. } => { + crate::type_analysis::is_numeric_expr(ctx, operand) + && unused_expr_is_pure_nonthrowing(ctx, operand) + } + perry_hir::Expr::Binary { op, left, right } => { + unused_binary_is_pure_nonthrowing(ctx, op, left, right) + && unused_expr_is_pure_nonthrowing(ctx, left) + && unused_expr_is_pure_nonthrowing(ctx, right) + } + perry_hir::Expr::Compare { left, right, .. } => { + unused_primitive_expr_is_nonthrowing(ctx, left) + && unused_primitive_expr_is_nonthrowing(ctx, right) + && unused_expr_is_pure_nonthrowing(ctx, left) + && unused_expr_is_pure_nonthrowing(ctx, right) + } + perry_hir::Expr::Conditional { + condition, + then_expr, + else_expr, + } => { + unused_expr_is_pure_nonthrowing(ctx, condition) + && unused_expr_is_pure_nonthrowing(ctx, then_expr) + && unused_expr_is_pure_nonthrowing(ctx, else_expr) + } + _ => false, + } +} + +fn unused_binary_is_pure_nonthrowing( + ctx: &FnCtx<'_>, + op: &perry_hir::BinaryOp, + left: &perry_hir::Expr, + right: &perry_hir::Expr, +) -> bool { + match op { + perry_hir::BinaryOp::Add => { + let l_num = crate::type_analysis::is_numeric_expr(ctx, left); + let r_num = crate::type_analysis::is_numeric_expr(ctx, right); + if l_num && r_num { + return true; + } + let l_str = crate::type_analysis::is_definitely_string_expr(ctx, left); + let r_str = crate::type_analysis::is_definitely_string_expr(ctx, right); + (l_str || r_str) + && unused_primitive_expr_is_nonthrowing(ctx, left) + && unused_primitive_expr_is_nonthrowing(ctx, right) + } + perry_hir::BinaryOp::Sub + | perry_hir::BinaryOp::Mul + | perry_hir::BinaryOp::Div + | perry_hir::BinaryOp::Mod + | perry_hir::BinaryOp::BitAnd + | perry_hir::BinaryOp::BitOr + | perry_hir::BinaryOp::BitXor + | perry_hir::BinaryOp::Shl + | perry_hir::BinaryOp::Shr + | perry_hir::BinaryOp::UShr => { + crate::type_analysis::is_numeric_expr(ctx, left) + && crate::type_analysis::is_numeric_expr(ctx, right) + } + _ => false, + } +} + +fn unused_primitive_expr_is_nonthrowing(ctx: &FnCtx<'_>, expr: &perry_hir::Expr) -> bool { + crate::type_analysis::is_numeric_expr(ctx, expr) + || crate::type_analysis::is_definitely_string_expr(ctx, expr) + || crate::type_analysis::is_bool_expr(ctx, expr) + || matches!( + expr, + perry_hir::Expr::Undefined + | perry_hir::Expr::Null + | perry_hir::Expr::String(_) + | perry_hir::Expr::WtfString(_) + | perry_hir::Expr::Number(_) + | perry_hir::Expr::Integer(_) + | perry_hir::Expr::Bool(_) + ) +} + fn record_pod_rejection(ctx: &mut FnCtx<'_>, id: u32, reason: String) { let undef = crate::nanbox::double_literal(f64::from_bits(crate::nanbox::TAG_UNDEFINED)); let lowered = LoweredValue::js_value(undef); diff --git a/crates/perry-codegen/tests/typed_shape_descriptors.rs b/crates/perry-codegen/tests/typed_shape_descriptors.rs index 98518e111..3206ebba0 100644 --- a/crates/perry-codegen/tests/typed_shape_descriptors.rs +++ b/crates/perry-codegen/tests/typed_shape_descriptors.rs @@ -167,6 +167,152 @@ fn block_between<'a>(ir: &'a str, start: &str, end: &str) -> &'a str { &block_ir[..end_pos] } +#[test] +fn scalar_object_literal_skips_pure_unobserved_initializers() { + let module = base_module( + "scalar_object_literal_used_fields.ts", + vec![ + Stmt::Let { + id: 1, + name: "obj".to_string(), + ty: Type::Any, + mutable: false, + init: Some(Expr::Object(vec![ + ("used".to_string(), Expr::Integer(7)), + ( + "unused".to_string(), + Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::String("drop_".to_string())), + right: Box::new(Expr::Integer(1)), + }, + ), + ])), + }, + Stmt::Return(Some(Expr::PropertyGet { + object: Box::new(Expr::LocalGet(1)), + property: "used".to_string(), + })), + ], + Vec::new(), + ); + + let ir = ir_for(module); + assert!( + !ir.contains("call i64 @js_object_alloc"), + "non-escaping object literal should stay scalar-replaced" + ); + assert!( + !ir.contains("call i64 @js_string_concat"), + "pure unobserved field initializer should not be lowered" + ); +} + +#[test] +fn scalar_array_literal_skips_pure_unobserved_initializers() { + let module = base_module( + "scalar_array_literal_used_indices.ts", + vec![ + Stmt::Let { + id: 1, + name: "arr".to_string(), + ty: Type::Any, + mutable: false, + init: Some(Expr::Array(vec![ + Expr::Integer(7), + Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::String("drop_".to_string())), + right: Box::new(Expr::Integer(1)), + }, + Expr::Integer(9), + ])), + }, + Stmt::Return(Some(Expr::IndexGet { + object: Box::new(Expr::LocalGet(1)), + index: Box::new(Expr::Integer(0)), + })), + ], + Vec::new(), + ); + + let ir = ir_for(module); + assert!( + !ir.contains("call i64 @js_array_alloc"), + "non-escaping array literal should stay scalar-replaced" + ); + assert!( + !ir.contains("call i64 @js_string_concat"), + "pure unobserved array initializer should not be lowered" + ); +} + +#[test] +fn scalar_object_literal_keeps_observable_unobserved_initializers() { + let module = base_module( + "scalar_object_literal_observable_unused.ts", + vec![ + Stmt::Let { + id: 1, + name: "obj".to_string(), + ty: Type::Any, + mutable: false, + init: Some(Expr::Object(vec![ + ("used".to_string(), Expr::Integer(7)), + ("unused".to_string(), Expr::DateNow), + ])), + }, + Stmt::Return(Some(Expr::PropertyGet { + object: Box::new(Expr::LocalGet(1)), + property: "used".to_string(), + })), + ], + Vec::new(), + ); + + let ir = ir_for(module); + assert!( + ir.contains("call double @js_date_now"), + "unobserved initializers that are not proven discardable must still be lowered" + ); +} + +#[test] +fn scalar_object_literal_keeps_initializers_read_by_update() { + let module = base_module( + "scalar_object_literal_update_read.ts", + vec![ + Stmt::Let { + id: 1, + name: "obj".to_string(), + ty: Type::Any, + mutable: false, + init: Some(Expr::Object(vec![( + "count".to_string(), + Expr::Binary { + op: BinaryOp::Add, + left: Box::new(Expr::String("4".to_string())), + right: Box::new(Expr::Integer(1)), + }, + )])), + }, + Stmt::Return(Some(Expr::PropertyUpdate { + object: Box::new(Expr::LocalGet(1)), + property: "count".to_string(), + op: BinaryOp::Add, + prefix: false, + })), + ], + Vec::new(), + ); + + let ir = ir_for(module); + assert!( + ir.contains("call i64 @js_string_concat"), + "field initializer read by obj.field++ must still be lowered" + ); +} + fn assert_typed_feedback_setter_after(ir: &str, start_pos: usize, context: &str) { let after_start = &ir[start_pos..]; assert!(