use std::collections::{HashMap, HashSet};
use crate::op::Op;
use crate::program::Function;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Policy {
FrameScope,
RequestScope,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Slot {
Agg(u32),
AggSet(Vec<u32>),
Other,
}
impl Slot {
fn sites(&self) -> &[u32] {
match self {
Slot::Agg(p) => std::slice::from_ref(p),
Slot::AggSet(v) => v.as_slice(),
Slot::Other => &[],
}
}
fn from_sites(mut sites: Vec<u32>) -> Slot {
sites.sort_unstable();
sites.dedup();
match sites.len() {
0 => Slot::Other,
1 => Slot::Agg(sites[0]),
_ => Slot::AggSet(sites),
}
}
fn merge(self, other: Slot) -> Slot {
if self == other { return self; }
let mut combined: Vec<u32> = Vec::with_capacity(
self.sites().len() + other.sites().len());
combined.extend_from_slice(self.sites());
combined.extend_from_slice(other.sites());
Slot::from_sites(combined)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct State {
stack: Vec<Slot>,
locals: Vec<Slot>,
}
impl State {
fn entry(locals_count: usize, arity: usize) -> Self {
Self {
stack: Vec::new(),
locals: vec![Slot::Other; locals_count.max(arity)],
}
}
fn merge_with(&self, other: &State) -> (State, HashSet<u32>) {
let mut escaped = HashSet::new();
let stack_len = self.stack.len().min(other.stack.len());
let mut stack = Vec::with_capacity(stack_len);
for i in 0..stack_len {
stack.push(self.stack[i].clone().merge(other.stack[i].clone()));
}
for tail in self.stack.iter().skip(stack_len).chain(other.stack.iter().skip(stack_len)) {
for &p in tail.sites() { escaped.insert(p); }
}
let local_len = self.locals.len().max(other.locals.len());
let mut locals = Vec::with_capacity(local_len);
for i in 0..local_len {
let a = self.locals.get(i).cloned().unwrap_or(Slot::Other);
let b = other.locals.get(i).cloned().unwrap_or(Slot::Other);
locals.push(a.merge(b));
}
(State { stack, locals }, escaped)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct EscapeReport {
pub fn_name: String,
pub sites: Vec<EscapeSite>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum SiteKind {
Record,
Tuple,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EscapeSite {
pub pc: u32,
pub kind: SiteKind,
pub shape_idx: u32,
pub field_count: u16,
pub escapes: bool,
}
pub fn analyze_program(functions: &[Function]) -> Vec<EscapeReport> {
functions
.iter()
.filter_map(|f| {
let r = analyze_function(f);
(!r.sites.is_empty()).then_some(r)
})
.collect()
}
pub fn analyze_function(func: &Function) -> EscapeReport {
analyze_function_with_policy(func, Policy::FrameScope)
}
pub fn analyze_function_with_policy(func: &Function, policy: Policy) -> EscapeReport {
let sites: Vec<(u32, SiteKind, u32, u16)> = func
.code
.iter()
.enumerate()
.filter_map(|(pc, op)| match op {
Op::MakeRecord { shape_idx, field_count } => {
Some((pc as u32, SiteKind::Record, *shape_idx, *field_count))
}
Op::MakeTuple(arity) => {
Some((pc as u32, SiteKind::Tuple, 0, *arity))
}
_ => None,
})
.collect();
if sites.is_empty() {
return EscapeReport { fn_name: func.name.clone(), sites: vec![] };
}
let n = func.code.len();
let locals_count = func.locals_count as usize;
let arity = func.arity as usize;
let mut in_state: Vec<Option<State>> = vec![None; n];
let mut escaped: HashSet<u32> = HashSet::new();
let mut containment: HashMap<u32, HashSet<u32>> = HashMap::new();
let mut worklist: Vec<(usize, State)> = vec![(0, State::entry(locals_count, arity))];
while let Some((pc, incoming)) = worklist.pop() {
if pc >= n { continue; }
let (merged, new_escapes) = match &in_state[pc] {
Some(prev) => {
let (m, e) = prev.merge_with(&incoming);
if &m == prev && e.is_empty() {
continue;
}
(m, e)
}
None => (incoming, HashSet::new()),
};
escaped.extend(new_escapes);
in_state[pc] = Some(merged.clone());
let (out, succs, leaked) = step(pc, &func.code[pc], merged, policy, &mut containment);
escaped.extend(leaked);
for s in succs {
if s < n {
worklist.push((s, out.clone()));
}
}
}
let mut changed = true;
while changed {
changed = false;
let snapshot: Vec<u32> = escaped.iter().copied().collect();
for p in snapshot {
if let Some(contained) = containment.get(&p) {
for &c in contained {
if escaped.insert(c) { changed = true; }
}
}
}
}
let report_sites = sites
.into_iter()
.map(|(pc, kind, shape_idx, field_count)| EscapeSite {
pc,
kind,
shape_idx,
field_count,
escapes: escaped.contains(&pc),
})
.collect();
EscapeReport { fn_name: func.name.clone(), sites: report_sites }
}
fn step(
pc: usize,
op: &Op,
mut s: State,
policy: Policy,
containment: &mut HashMap<u32, HashSet<u32>>,
) -> (State, Vec<usize>, HashSet<u32>) {
let mut escapes: HashSet<u32> = HashSet::new();
let pop_n_contain = |state: &mut State,
n: usize,
parent: u32,
containment: &mut HashMap<u32, HashSet<u32>>| {
let entry = containment.entry(parent).or_default();
for _ in 0..n {
if let Some(top) = state.stack.pop() {
for &child in top.sites() {
if child != parent { entry.insert(child); }
}
}
}
};
let leak = |slot: &Slot, into: &mut HashSet<u32>| {
for &p in slot.sites() { into.insert(p); }
};
let pop_n_leak = |state: &mut State, n: usize, esc: &mut HashSet<u32>| {
for _ in 0..n {
if let Some(top) = state.stack.pop() { leak(&top, esc); }
}
};
let pop_n_drop = |state: &mut State, n: usize| {
for _ in 0..n { state.stack.pop(); }
};
match op {
Op::PushConst(_) => { s.stack.push(Slot::Other); }
Op::Pop => { s.stack.pop(); }
Op::Dup => {
if let Some(top) = s.stack.pop() {
leak(&top, &mut escapes);
s.stack.push(Slot::Other);
s.stack.push(Slot::Other);
}
}
Op::LoadLocal(i) => {
let slot = s.locals.get(*i as usize).cloned().unwrap_or(Slot::Other);
s.stack.push(slot);
}
Op::StoreLocal(i) => {
if let Some(top) = s.stack.pop() {
let i = *i as usize;
if i >= s.locals.len() { s.locals.resize(i + 1, Slot::Other); }
s.locals[i] = top;
}
}
Op::MakeRecord { field_count, .. } => {
pop_n_contain(&mut s, *field_count as usize, pc as u32, containment);
s.stack.push(Slot::Agg(pc as u32));
}
Op::AllocStackRecord { field_count, .. } => {
pop_n_contain(&mut s, *field_count as usize, pc as u32, containment);
s.stack.push(Slot::Agg(pc as u32));
}
Op::AllocArenaRecord { field_count, .. } => {
pop_n_contain(&mut s, *field_count as usize, pc as u32, containment);
s.stack.push(Slot::Agg(pc as u32));
}
Op::MakeTuple(n) => {
pop_n_contain(&mut s, *n as usize, pc as u32, containment);
s.stack.push(Slot::Agg(pc as u32));
}
Op::AllocStackTuple { arity } => {
pop_n_contain(&mut s, *arity as usize, pc as u32, containment);
s.stack.push(Slot::Agg(pc as u32));
}
Op::AllocArenaTuple { arity } => {
pop_n_contain(&mut s, *arity as usize, pc as u32, containment);
s.stack.push(Slot::Agg(pc as u32));
}
Op::MakeList(n) => {
pop_n_leak(&mut s, *n as usize, &mut escapes);
s.stack.push(Slot::Other);
}
Op::MakeVariant { arity, .. } => {
pop_n_leak(&mut s, *arity as usize, &mut escapes);
s.stack.push(Slot::Other);
}
Op::MakeClosure { capture_count, .. } => {
pop_n_leak(&mut s, *capture_count as usize, &mut escapes);
s.stack.push(Slot::Other);
}
Op::GetField { .. } => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::GetElem(_) => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::TestVariant(_) => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::GetVariant(_) => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::GetVariantArg(_) => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::GetListLen => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::GetListElem(_) => { s.stack.pop(); s.stack.push(Slot::Other); }
Op::GetListElemDyn => {
s.stack.pop(); s.stack.pop(); s.stack.push(Slot::Other);
}
Op::ListAppend => {
if let Some(value) = s.stack.pop() { leak(&value, &mut escapes); }
s.stack.pop(); s.stack.push(Slot::Other);
}
Op::Jump(off) => {
let target = (pc as i32 + 1 + off) as usize;
return (s, vec![target], escapes);
}
Op::JumpIf(off) | Op::JumpIfNot(off) => {
s.stack.pop(); let target = (pc as i32 + 1 + off) as usize;
return (s, vec![pc + 1, target], escapes);
}
Op::Return => {
let top = s.stack.pop();
if matches!(policy, Policy::FrameScope) {
if let Some(top) = top { leak(&top, &mut escapes); }
}
return (s, vec![], escapes);
}
Op::Panic(_) => {
return (s, vec![], escapes);
}
Op::TailCall { arity, .. } => {
pop_n_leak(&mut s, *arity as usize, &mut escapes);
return (s, vec![], escapes);
}
Op::Call { arity, .. } => {
pop_n_leak(&mut s, *arity as usize, &mut escapes);
s.stack.push(Slot::Other);
}
Op::CallClosure { arity, .. } => {
pop_n_leak(&mut s, *arity as usize + 1, &mut escapes);
s.stack.push(Slot::Other);
}
Op::SortByKey { .. } | Op::ParallelMap { .. }
| Op::ListMap { .. } | Op::ListFilter { .. } => {
pop_n_leak(&mut s, 2, &mut escapes);
s.stack.push(Slot::Other);
}
Op::ListFold { .. } => {
pop_n_leak(&mut s, 3, &mut escapes);
s.stack.push(Slot::Other);
}
Op::EffectCall { arity, .. } => {
pop_n_leak(&mut s, *arity as usize, &mut escapes);
s.stack.push(Slot::Other);
}
Op::IntAdd | Op::IntSub | Op::IntMul | Op::IntDiv | Op::IntMod
| Op::IntEq | Op::IntLt | Op::IntLe
| Op::FloatAdd | Op::FloatSub | Op::FloatMul | Op::FloatDiv
| Op::FloatEq | Op::FloatLt | Op::FloatLe
| Op::NumAdd | Op::NumSub | Op::NumMul | Op::NumDiv | Op::NumMod
| Op::NumEq | Op::NumLt | Op::NumLe
| Op::BoolAnd | Op::BoolOr
| Op::StrConcat | Op::StrEq | Op::BytesEq => {
pop_n_drop(&mut s, 2);
s.stack.push(Slot::Other);
}
Op::IntNeg | Op::FloatNeg | Op::NumNeg | Op::BoolNot
| Op::StrLen | Op::BytesLen => {
s.stack.pop();
s.stack.push(Slot::Other);
}
Op::LoadLocalAddIntConst { .. } => {
s.stack.push(Slot::Other);
}
Op::LoadLocalAddIntConstStoreLocal { dest, .. } => {
let i = *dest as usize;
if i >= s.locals.len() { s.locals.resize(i + 1, Slot::Other); }
s.locals[i] = Slot::Other;
return (s, vec![pc + 4], escapes);
}
Op::LoadLocalAddLocal { .. }
| Op::LoadLocalSubLocal { .. }
| Op::LoadLocalMulLocal { .. } => {
s.stack.push(Slot::Other);
return (s, vec![pc + 3], escapes);
}
Op::LoadLocalGetField { .. } => {
s.stack.push(Slot::Other);
return (s, vec![pc + 2], escapes);
}
Op::LoadLocalGetFieldAdd { .. }
| Op::LoadLocalGetFieldSub { .. }
| Op::LoadLocalGetFieldMul { .. } => {
s.stack.pop();
s.stack.push(Slot::Other);
return (s, vec![pc + 3], escapes);
}
Op::LoadLocalEqIntConstJumpIfNot { jump_offset, .. } => {
let target = (pc as i32 + 4 + jump_offset) as usize;
return (s, vec![pc + 4, target], escapes);
}
Op::LoadLocalStoreEqIntConstJumpIfNot { dst, jump_offset, .. } => {
let i = *dst as usize;
if i >= s.locals.len() { s.locals.resize(i + 1, Slot::Other); }
s.locals[i] = Slot::Other;
let target = (pc as i32 + 6 + jump_offset) as usize;
return (s, vec![pc + 6, target], escapes);
}
}
(s, vec![pc + 1], escapes)
}
pub fn build_escape_index(functions: &[Function]) -> HashMap<(String, u32), bool> {
let mut idx = HashMap::new();
for report in analyze_program(functions) {
for site in report.sites {
idx.insert((report.fn_name.clone(), site.pc), site.escapes);
}
}
idx
}
#[cfg(test)]
mod tests {
use super::*;
use crate::op::Op;
use crate::program::{Function, ZERO_BODY_HASH};
fn func(name: &str, locals_count: u16, arity: u16, code: Vec<Op>) -> Function {
Function {
name: name.into(),
arity,
locals_count,
code,
effects: vec![],
body_hash: ZERO_BODY_HASH,
refinements: vec![],
field_ic_sites: 0,
}
}
fn assert_escapes(report: &EscapeReport, expected: &[(u32, bool)]) {
let got: Vec<(u32, bool)> = report.sites.iter().map(|s| (s.pc, s.escapes)).collect();
assert_eq!(got, expected,
"escape report for `{}` differs from expected", report.fn_name);
}
#[test]
fn record_built_and_dropped_does_not_escape() {
let f = func("dropper", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, false)]);
}
#[test]
fn record_returned_escapes() {
let f = func("returner", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true)]);
}
#[test]
fn record_field_read_only_does_not_escape() {
let f = func("reader", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::GetField { name_idx: 0, site_idx: 0 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, false)]);
}
#[test]
fn record_round_tripped_through_local_does_not_escape() {
let f = func("roundtrip", 1, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::StoreLocal(0),
Op::LoadLocal(0),
Op::GetField { name_idx: 0, site_idx: 0 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, false)]);
}
#[test]
fn record_stored_into_outer_record_escapes() {
let f = func("nest", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 }, Op::PushConst(2),
Op::MakeRecord { shape_idx: 1, field_count: 2 }, Op::Return, ]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true), (4, true)]);
}
#[test]
fn record_passed_to_call_escapes() {
let f = func("passer", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::Call { fn_id: 1, arity: 1, node_id_idx: 0 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true)]);
}
#[test]
fn record_captured_in_closure_escapes() {
let f = func("capturer", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::MakeClosure { fn_id: 1, capture_count: 1 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true)]);
}
#[test]
fn record_in_one_branch_returned_escapes_after_merge() {
let f = func("brancher", 0, 1, vec![
Op::LoadLocal(0), Op::JumpIfNot(4), Op::PushConst(0), Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Jump(2), Op::PushConst(1), Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Return, ]);
let r = analyze_function(&f);
assert_escapes(&r, &[(3, true), (6, true)]);
}
#[test]
fn merged_branch_records_kept_tracked_under_request_scope() {
let f = func("brancher_req", 0, 1, vec![
Op::LoadLocal(0),
Op::JumpIfNot(4),
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Jump(2),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Return,
]);
let r = analyze_function_with_policy(&f, Policy::RequestScope);
assert_escapes(&r, &[(3, false), (6, false)]);
}
#[test]
fn merged_branch_records_all_escape_at_call_under_request_scope() {
let f = func("brancher_then_call", 0, 1, vec![
Op::LoadLocal(0),
Op::JumpIfNot(4),
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Jump(2),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Call { fn_id: 1, arity: 1, node_id_idx: 0 }, Op::Return,
]);
let r = analyze_function_with_policy(&f, Policy::RequestScope);
assert_escapes(&r, &[(3, true), (6, true)]);
}
#[test]
fn deep_leaf_nested_records_both_arena_eligible() {
let f = func("nested_ret", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::PushConst(1),
Op::MakeRecord { shape_idx: 1, field_count: 2 }, Op::Return,
]);
let r = analyze_function_with_policy(&f, Policy::RequestScope);
assert_escapes(&r, &[(1, false), (3, false)]);
}
#[test]
fn deep_leaf_three_deep_chain_all_arena_eligible() {
let f = func("three_deep", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Return,
]);
let r = analyze_function_with_policy(&f, Policy::RequestScope);
assert_escapes(&r, &[(1, false), (2, false), (3, false)]);
}
#[test]
fn deep_leaf_chain_all_escape_when_root_passed_to_call() {
let f = func("three_deep_call", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Call { fn_id: 1, arity: 1, node_id_idx: 0 }, Op::Return,
]);
let r = analyze_function_with_policy(&f, Policy::RequestScope);
assert_escapes(&r, &[(1, true), (2, true), (3, true)]);
}
#[test]
fn deep_leaf_outer_popped_inner_stays_local_under_frame_scope() {
let f = func("nested_pop", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f); assert_escapes(&r, &[(1, false), (2, false)]);
}
#[test]
fn two_sites_classified_independently() {
let f = func("mixed", 1, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::StoreLocal(0),
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Pop,
Op::LoadLocal(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, true), (4, false)]);
}
#[test]
fn function_with_no_record_sites_produces_empty_report() {
let f = func("pure_arith", 0, 2, vec![
Op::LoadLocal(0),
Op::LoadLocal(1),
Op::IntAdd,
Op::Return,
]);
let r = analyze_function(&f);
assert!(r.sites.is_empty());
}
#[test]
fn analyze_program_skips_no_record_functions() {
let f1 = func("noreds", 0, 0, vec![Op::PushConst(0), Op::Return]);
let f2 = func("hasrec", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::Return,
]);
let reports = analyze_program(&[f1, f2]);
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].fn_name, "hasrec");
}
#[test]
fn record_passed_to_effect_call_escapes() {
let f = func("effecting", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::EffectCall { kind_idx: 0, op_idx: 0, arity: 1, node_id_idx: 0 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, true)]);
}
#[test]
fn record_duplicated_escapes() {
let f = func("duper", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::Dup,
Op::Pop,
Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, true)]);
}
#[test]
fn record_in_list_escapes() {
let f = func("listed", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::MakeList(1),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, true)]);
}
#[test]
fn build_escape_index_keys_by_fn_and_pc() {
let f = func("idx_test", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let idx = build_escape_index(&[f]);
assert_eq!(idx.get(&("idx_test".into(), 1)), Some(&false));
}
#[test]
fn tuple_built_and_dropped_does_not_escape() {
let f = func("tup_drop", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, false)]);
assert_eq!(r.sites[0].kind, SiteKind::Tuple);
assert_eq!(r.sites[0].field_count, 2);
}
#[test]
fn tuple_returned_escapes() {
let f = func("tup_ret", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true)]);
}
#[test]
fn tuple_elem_read_only_does_not_escape() {
let f = func("tup_read", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::GetElem(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, false)]);
}
#[test]
fn tuple_round_tripped_through_local_does_not_escape() {
let f = func("tup_rt", 1, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::StoreLocal(0),
Op::LoadLocal(0),
Op::GetElem(1),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, false)]);
}
#[test]
fn tuple_passed_to_call_escapes() {
let f = func("tup_call", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::Call { fn_id: 1, arity: 1, node_id_idx: 0 },
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true)]);
}
#[test]
fn record_stored_into_tuple_dropped_outer_neither_escapes() {
let f = func("rec_in_tup", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::MakeTuple(1), Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, false), (2, false)]);
}
#[test]
fn tuple_stored_into_record_escapes() {
let f = func("tup_in_rec", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(2, true), (3, true)]);
}
#[test]
fn tuple_and_record_sites_carry_distinct_kinds() {
let f = func("mixed_kinds", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 7, field_count: 1 }, Op::Pop,
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2), Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_eq!(r.sites.len(), 2);
assert_eq!((r.sites[0].pc, r.sites[0].kind, r.sites[0].shape_idx), (1, SiteKind::Record, 7));
assert_eq!((r.sites[1].pc, r.sites[1].kind, r.sites[1].field_count), (5, SiteKind::Tuple, 2));
assert!(!r.sites[0].escapes && !r.sites[1].escapes);
}
#[test]
fn aggregate_overwritten_in_dead_slot_does_not_escape() {
let f = func("overwrite", 1, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::StoreLocal(0),
Op::PushConst(0),
Op::StoreLocal(0), Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, false)]);
}
#[test]
fn aggregate_loaded_then_returned_still_escapes() {
let f = func("load_then_return", 1, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 }, Op::StoreLocal(0),
Op::LoadLocal(0),
Op::Return, ]);
let r = analyze_function(&f);
assert_escapes(&r, &[(1, true)]);
}
#[test]
fn tuple_only_function_now_produces_report() {
let f = func("tuponly", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2),
Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let reports = analyze_program(&[f]);
assert_eq!(reports.len(), 1);
assert_eq!(reports[0].sites.len(), 1);
assert_eq!(reports[0].sites[0].kind, SiteKind::Tuple);
}
}