use std::collections::{HashMap, HashSet};
use shape_vm::bytecode::{BytecodeProgram, Constant, OpCode, Operand};
pub const MAX_SCALAR_ARRAY_ELEMENTS: usize = 8;
#[derive(Debug, Clone)]
pub struct ScalarArrayEntry {
pub local_slot: u16,
pub element_count: usize,
pub get_sites: HashMap<usize, usize>,
pub set_sites: HashMap<usize, usize>,
}
#[derive(Debug, Clone, Default)]
pub struct EscapeAnalysisPlan {
pub scalar_arrays: HashMap<usize, ScalarArrayEntry>,
}
impl EscapeAnalysisPlan {
#[cfg(test)]
pub fn has_candidates(&self) -> bool {
!self.scalar_arrays.is_empty()
}
}
struct ArrayCandidate {
new_array_idx: usize,
local_slot: u16,
element_count: usize,
get_sites: HashMap<usize, usize>,
set_sites: HashMap<usize, usize>,
escaped: bool,
}
fn is_block_boundary(op: OpCode) -> bool {
matches!(
op,
OpCode::Jump
| OpCode::JumpIfFalse
| OpCode::JumpIfFalseTrusted
| OpCode::JumpIfTrue
| OpCode::LoopStart
| OpCode::LoopEnd
| OpCode::Break
| OpCode::Continue
| OpCode::Return
| OpCode::ReturnValue
| OpCode::Halt
| OpCode::SetupTry
| OpCode::PopHandler
| OpCode::Throw
)
}
fn is_escaping_call(op: OpCode) -> bool {
matches!(
op,
OpCode::Call
| OpCode::CallValue
| OpCode::CallClosure
| OpCode::CallFunctionIndirect
| OpCode::CallMethod
| OpCode::BuiltinCall
| OpCode::DynMethodCall
| OpCode::CallForeign
| OpCode::DropCall
| OpCode::DropCallAsync
)
}
fn resolve_constant_index(program: &BytecodeProgram, const_idx: u16) -> Option<usize> {
match program.constants.get(const_idx as usize)? {
Constant::Int(v) if *v >= 0 => Some(*v as usize),
Constant::UInt(v) => Some(*v as usize),
Constant::Number(v) if *v >= 0.0 && *v == (*v as usize as f64) => Some(*v as usize),
_ => None,
}
}
pub fn analyze_escape(program: &BytecodeProgram) -> EscapeAnalysisPlan {
let mut plan = EscapeAnalysisPlan::default();
let instructions = &program.instructions;
if instructions.is_empty() {
return plan;
}
let mut candidates: Vec<ArrayCandidate> = Vec::new();
let mut slot_to_candidate: HashMap<u16, usize> = HashMap::new();
for i in 0..instructions.len().saturating_sub(1) {
let instr = &instructions[i];
if instr.opcode != OpCode::NewArray {
continue;
}
let count = match &instr.operand {
Some(Operand::Count(c)) => *c as usize,
_ => continue,
};
if count > MAX_SCALAR_ARRAY_ELEMENTS {
continue;
}
let next = &instructions[i + 1];
let local_slot = match (next.opcode, &next.operand) {
(OpCode::StoreLocal, Some(Operand::Local(slot))) => *slot,
(OpCode::StoreLocalTyped, Some(Operand::TypedLocal(slot, _))) => *slot,
_ => continue,
};
if let Some(&old_idx) = slot_to_candidate.get(&local_slot) {
candidates[old_idx].escaped = true;
}
let cand_idx = candidates.len();
candidates.push(ArrayCandidate {
new_array_idx: i,
local_slot,
element_count: count,
get_sites: HashMap::new(),
set_sites: HashMap::new(),
escaped: false,
});
slot_to_candidate.insert(local_slot, cand_idx);
}
if candidates.is_empty() {
return plan;
}
let mut active_slots: HashSet<u16> = HashSet::new();
let mut activated: HashSet<usize> = HashSet::new();
let mut jump_targets: HashSet<usize> = HashSet::new();
for instr in instructions.iter() {
if let Some(Operand::Offset(off)) = &instr.operand {
match instr.opcode {
OpCode::Jump
| OpCode::JumpIfFalse
| OpCode::JumpIfFalseTrusted
| OpCode::JumpIfTrue => {
}
_ => {}
}
let _ = off; }
}
for (i, instr) in instructions.iter().enumerate() {
if let Some(Operand::Offset(off)) = &instr.operand {
match instr.opcode {
OpCode::Jump
| OpCode::JumpIfFalse
| OpCode::JumpIfFalseTrusted
| OpCode::JumpIfTrue => {
let target = (i as i64 + *off as i64 + 1) as usize;
if target < instructions.len() {
jump_targets.insert(target);
}
}
_ => {}
}
}
}
for i in 0..instructions.len() {
let instr = &instructions[i];
if is_block_boundary(instr.opcode) || jump_targets.contains(&i) {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
if activated.contains(&cand_idx) {
candidates[cand_idx].escaped = true;
}
}
}
active_slots.clear();
}
if i > 0 && instructions[i - 1].opcode == OpCode::NewArray {
match (instr.opcode, &instr.operand) {
(OpCode::StoreLocal, Some(Operand::Local(slot)))
| (OpCode::StoreLocalTyped, Some(Operand::TypedLocal(slot, _))) => {
if let Some(&cand_idx) = slot_to_candidate.get(slot) {
if candidates[cand_idx].new_array_idx == i - 1 && !candidates[cand_idx].escaped
{
activated.insert(cand_idx);
active_slots.insert(*slot);
}
}
}
_ => {}
}
}
match (instr.opcode, &instr.operand) {
(OpCode::LoadLocal | OpCode::LoadLocalTrusted, Some(Operand::Local(slot))) => {
if let Some(&cand_idx) = slot_to_candidate.get(slot) {
if activated.contains(&cand_idx) && !candidates[cand_idx].escaped {
if i + 2 < instructions.len() {
let next1 = &instructions[i + 1];
let next2 = &instructions[i + 2];
if next2.opcode == OpCode::GetProp && next2.operand.is_none() {
if let (
OpCode::PushConst,
Some(Operand::Const(const_idx)),
) = (next1.opcode, &next1.operand)
{
if let Some(elem_idx) =
resolve_constant_index(program, *const_idx)
{
if elem_idx < candidates[cand_idx].element_count {
candidates[cand_idx]
.get_sites
.insert(i + 2, elem_idx);
continue;
}
}
}
}
}
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::SetLocalIndex, Some(Operand::Local(slot))) => {
if let Some(&cand_idx) = slot_to_candidate.get(slot) {
if activated.contains(&cand_idx) && !candidates[cand_idx].escaped {
if let Some(const_index) =
find_constant_index_for_set(program, i)
{
if const_index < candidates[cand_idx].element_count {
candidates[cand_idx].set_sites.insert(i, const_index);
continue;
}
}
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::StoreLocal, Some(Operand::Local(slot)))
| (OpCode::StoreLocalTyped, Some(Operand::TypedLocal(slot, _))) => {
if let Some(&cand_idx) = slot_to_candidate.get(slot) {
if activated.contains(&cand_idx)
&& candidates[cand_idx].new_array_idx + 1 != i
{
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::MakeRef, Some(Operand::Local(slot))) => {
if let Some(&cand_idx) = slot_to_candidate.get(slot) {
if activated.contains(&cand_idx) {
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::SetIndexRef | OpCode::MakeFieldRef | OpCode::MakeIndexRef
| OpCode::DerefLoad | OpCode::DerefStore, _) => {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
candidates[cand_idx].escaped = true;
}
}
}
_ if is_escaping_call(instr.opcode) => {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::Return | OpCode::ReturnValue, _) => {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::ArrayPush | OpCode::ArrayPushLocal | OpCode::ArrayPop | OpCode::Length | OpCode::SliceAccess, _) => {
if let Some(Operand::Local(slot)) = &instr.operand {
if let Some(&cand_idx) = slot_to_candidate.get(slot) {
if activated.contains(&cand_idx) {
candidates[cand_idx].escaped = true;
}
}
}
if matches!(instr.opcode, OpCode::ArrayPush | OpCode::ArrayPop | OpCode::Length | OpCode::SliceAccess) {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
candidates[cand_idx].escaped = true;
}
}
}
}
(OpCode::SetProp, _) => {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
candidates[cand_idx].escaped = true;
}
}
}
(OpCode::MakeClosure, _) => {
for &slot in &active_slots {
if let Some(&cand_idx) = slot_to_candidate.get(&slot) {
candidates[cand_idx].escaped = true;
}
}
}
_ => {}
}
}
for candidate in candidates {
if candidate.escaped {
continue;
}
if candidate.get_sites.is_empty() && candidate.set_sites.is_empty() {
continue;
}
plan.scalar_arrays.insert(
candidate.new_array_idx,
ScalarArrayEntry {
local_slot: candidate.local_slot,
element_count: candidate.element_count,
get_sites: candidate.get_sites,
set_sites: candidate.set_sites,
},
);
}
plan
}
fn find_constant_index_for_set(
program: &BytecodeProgram,
set_idx: usize,
) -> Option<usize> {
let mut depth_from_top: i32 = 1; for j in (0..set_idx).rev() {
let instr = &program.instructions[j];
let op = instr.opcode;
if is_block_boundary(op) || is_escaping_call(op) {
return None;
}
let (pops, pushes) = stack_effect_simple(op)?;
if depth_from_top < pushes {
if op == OpCode::PushConst {
if let Some(Operand::Const(const_idx)) = &instr.operand {
return resolve_constant_index(program, *const_idx);
}
}
return None;
}
depth_from_top = depth_from_top - pushes + pops;
if depth_from_top < 0 {
return None;
}
}
None
}
fn stack_effect_simple(op: OpCode) -> Option<(i32, i32)> {
let eff = match op {
OpCode::LoadLocal
| OpCode::LoadLocalTrusted
| OpCode::LoadModuleBinding
| OpCode::LoadClosure
| OpCode::PushConst
| OpCode::PushNull
| OpCode::DerefLoad => (0, 1),
OpCode::IntToNumber
| OpCode::NumberToInt
| OpCode::CastWidth
| OpCode::NegInt
| OpCode::NegNumber
| OpCode::IsNull
| OpCode::Not
| OpCode::Length => (1, 1),
OpCode::AddInt
| OpCode::SubInt
| OpCode::MulInt
| OpCode::DivInt
| OpCode::ModInt
| OpCode::PowInt
| OpCode::AddNumber
| OpCode::SubNumber
| OpCode::MulNumber
| OpCode::DivNumber
| OpCode::ModNumber
| OpCode::PowNumber
| OpCode::GtInt
| OpCode::LtInt
| OpCode::GteInt
| OpCode::LteInt
| OpCode::GtNumber
| OpCode::LtNumber
| OpCode::GteNumber
| OpCode::LteNumber
| OpCode::EqInt
| OpCode::EqNumber
| OpCode::NeqInt
| OpCode::NeqNumber
| OpCode::EqString
| OpCode::GtString
| OpCode::LtString
| OpCode::GteString
| OpCode::LteString
| OpCode::EqDecimal
| OpCode::GetProp
| OpCode::And
| OpCode::Or => (2, 1),
OpCode::Dup => (1, 2),
OpCode::Swap => (2, 2),
OpCode::Pop
| OpCode::StoreLocal
| OpCode::StoreLocalTyped
| OpCode::StoreModuleBinding
| OpCode::StoreModuleBindingTyped
| OpCode::StoreClosure
| OpCode::DerefStore
| OpCode::DropCall
| OpCode::DropCallAsync => (1, 0),
OpCode::NewArray => (0, 1), _ => return None,
};
Some(eff)
}
#[cfg(test)]
mod tests {
use super::*;
use shape_vm::bytecode::{DebugInfo, Instruction};
fn make_instr(opcode: OpCode, operand: Option<Operand>) -> Instruction {
Instruction { opcode, operand }
}
fn make_program(instrs: Vec<Instruction>, constants: Vec<Constant>) -> BytecodeProgram {
BytecodeProgram {
instructions: instrs,
constants,
strings: vec![],
functions: vec![],
debug_info: DebugInfo::default(),
data_schema: None,
module_binding_names: vec![],
top_level_locals_count: 0,
top_level_local_storage_hints: vec![],
type_schema_registry: Default::default(),
module_binding_storage_hints: vec![],
function_local_storage_hints: vec![],
compiled_annotations: Default::default(),
trait_method_symbols: Default::default(),
expanded_function_defs: Default::default(),
string_index: Default::default(),
foreign_functions: vec![],
native_struct_layouts: vec![],
content_addressed: None,
function_blob_hashes: vec![],
top_level_frame: None,
..Default::default()
}
}
#[test]
fn simple_scalar_replacement_candidate() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(2))), make_instr(OpCode::StoreLocal, Some(Operand::Local(0))), make_instr(OpCode::PushConst, Some(Operand::Const(0))), make_instr(OpCode::PushConst, Some(Operand::Const(2))), make_instr(OpCode::SetLocalIndex, Some(Operand::Local(0))), make_instr(OpCode::LoadLocal, Some(Operand::Local(0))), make_instr(OpCode::PushConst, Some(Operand::Const(1))), make_instr(OpCode::GetProp, None), make_instr(OpCode::Pop, None), ],
vec![
Constant::Int(0), Constant::Int(1), Constant::Int(42), ],
);
let plan = analyze_escape(&program);
assert!(plan.has_candidates());
let entry = plan.scalar_arrays.get(&0).expect("should have candidate at idx 0");
assert_eq!(entry.local_slot, 0);
assert_eq!(entry.element_count, 2);
assert_eq!(entry.set_sites.get(&4), Some(&0)); assert_eq!(entry.get_sites.get(&7), Some(&1)); }
#[test]
fn array_escapes_via_call() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(2))), make_instr(OpCode::StoreLocal, Some(Operand::Local(0))), make_instr(OpCode::LoadLocal, Some(Operand::Local(0))), make_instr(OpCode::Call, Some(Operand::Count(1))), ],
vec![],
);
let plan = analyze_escape(&program);
assert!(!plan.has_candidates());
}
#[test]
fn array_too_large_rejected() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(9))), make_instr(OpCode::StoreLocal, Some(Operand::Local(0))),
make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
make_instr(OpCode::PushConst, Some(Operand::Const(0))),
make_instr(OpCode::GetProp, None),
make_instr(OpCode::Pop, None),
],
vec![Constant::Int(0)],
);
let plan = analyze_escape(&program);
assert!(!plan.has_candidates());
}
#[test]
fn array_escapes_via_return() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(2))),
make_instr(OpCode::StoreLocal, Some(Operand::Local(0))),
make_instr(OpCode::LoadLocal, Some(Operand::Local(0))),
make_instr(OpCode::PushConst, Some(Operand::Const(0))),
make_instr(OpCode::GetProp, None),
make_instr(OpCode::ReturnValue, None),
],
vec![Constant::Int(0)],
);
let plan = analyze_escape(&program);
assert!(!plan.has_candidates());
}
#[test]
fn array_escapes_at_block_boundary() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(2))),
make_instr(OpCode::StoreLocal, Some(Operand::Local(0))),
make_instr(OpCode::Jump, Some(Operand::Offset(0))), make_instr(OpCode::LoadLocal, Some(Operand::Local(0))), make_instr(OpCode::PushConst, Some(Operand::Const(0))),
make_instr(OpCode::GetProp, None),
make_instr(OpCode::Pop, None),
],
vec![Constant::Int(0)],
);
let plan = analyze_escape(&program);
assert!(!plan.has_candidates());
}
#[test]
fn no_uses_not_scalarized() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(2))),
make_instr(OpCode::StoreLocal, Some(Operand::Local(0))),
make_instr(OpCode::PushNull, None),
make_instr(OpCode::Pop, None),
],
vec![],
);
let plan = analyze_escape(&program);
assert!(!plan.has_candidates());
}
#[test]
fn array_escapes_via_array_push() {
let program = make_program(
vec![
make_instr(OpCode::NewArray, Some(Operand::Count(2))),
make_instr(OpCode::StoreLocal, Some(Operand::Local(0))),
make_instr(OpCode::PushConst, Some(Operand::Const(0))),
make_instr(OpCode::ArrayPushLocal, Some(Operand::Local(0))),
],
vec![Constant::Int(42)],
);
let plan = analyze_escape(&program);
assert!(!plan.has_candidates());
}
}