use std::collections::HashMap;
use crate::escape::{analyze_function_with_policy, Policy, SiteKind};
use crate::program::Function;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArenaReport {
pub fn_name: String,
pub sites: Vec<ArenaSite>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct ArenaSite {
pub pc: u32,
pub kind: SiteKind,
pub shape_idx: u32,
pub field_count: u16,
pub arena_eligible: bool,
}
pub fn analyze_function(func: &Function) -> ArenaReport {
let r = analyze_function_with_policy(func, Policy::RequestScope);
let sites = r
.sites
.into_iter()
.map(|s| ArenaSite {
pc: s.pc,
kind: s.kind,
shape_idx: s.shape_idx,
field_count: s.field_count,
arena_eligible: !s.escapes,
})
.collect();
ArenaReport { fn_name: r.fn_name, sites }
}
pub fn analyze_program(functions: &[Function]) -> Vec<ArenaReport> {
functions
.iter()
.filter_map(|f| {
let r = analyze_function(f);
(!r.sites.is_empty()).then_some(r)
})
.collect()
}
pub fn build_arena_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.arena_eligible);
}
}
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_eligible(report: &ArenaReport, expected: &[(u32, bool)]) {
let got: Vec<(u32, bool)> = report
.sites
.iter()
.map(|s| (s.pc, s.arena_eligible))
.collect();
assert_eq!(
got, expected,
"arena eligibility for `{}` differs from expected",
report.fn_name
);
}
#[test]
fn record_returned_is_arena_eligible() {
let f = func("handler", 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_eligible(&r, &[(2, true)]);
}
#[test]
fn tuple_returned_is_arena_eligible() {
let f = func("handler_t", 0, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeTuple(2),
Op::Return,
]);
let r = analyze_function(&f);
assert_eligible(&r, &[(2, true)]);
}
#[test]
fn record_round_tripped_and_returned_is_arena_eligible() {
let f = func("handler_rt", 1, 0, vec![
Op::PushConst(0),
Op::PushConst(1),
Op::MakeRecord { shape_idx: 0, field_count: 2 },
Op::StoreLocal(0),
Op::LoadLocal(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_eligible(&r, &[(2, true)]);
}
#[test]
fn record_passed_to_call_is_not_arena_eligible() {
let f = func("caller", 0, 0, vec![
Op::PushConst(0),
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(&f);
assert_eligible(&r, &[(1, false)]);
}
#[test]
fn record_captured_in_closure_is_not_arena_eligible() {
let f = func("capturer", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::MakeClosure { fn_id: 1, capture_count: 1 },
Op::Return,
]);
let r = analyze_function(&f);
assert_eligible(&r, &[(1, false)]);
}
#[test]
fn record_passed_to_effect_is_not_arena_eligible() {
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_eligible(&r, &[(1, false)]);
}
#[test]
fn record_dropped_is_arena_eligible() {
let f = func("drop", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::Pop,
Op::PushConst(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_eligible(&r, &[(1, true)]);
}
#[test]
fn outer_returned_aggregate_makes_inner_field_arena_eligible_too() {
let f = func("nest", 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(&f);
assert_eligible(&r, &[(1, true), (3, true)]);
}
#[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::Call { fn_id: 1, arity: 1, node_id_idx: 0 },
Op::Pop,
Op::LoadLocal(0),
Op::Return,
]);
let r = analyze_function(&f);
assert_eligible(&r, &[(1, true), (4, false)]);
}
#[test]
fn build_arena_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::Return,
]);
let idx = build_arena_index(&[f]);
assert_eq!(idx.get(&("idx_test".into(), 1)), Some(&true));
}
#[test]
fn analyze_program_skips_functions_with_no_sites() {
let f1 = func("noaggs", 0, 0, vec![Op::PushConst(0), Op::Return]);
let f2 = func("hasagg", 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, "hasagg");
}
#[test]
fn parity_with_frame_escape_on_shared_hatch() {
use crate::escape::analyze_function as analyze_frame;
let f = func("parity_hatch", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::Call { fn_id: 1, arity: 1, node_id_idx: 0 },
Op::Return,
]);
let arena = analyze_function(&f);
let frame = analyze_frame(&f);
assert!(!arena.sites[0].arena_eligible);
assert!(frame.sites[0].escapes);
}
#[test]
fn diverges_from_frame_escape_on_return() {
use crate::escape::analyze_function as analyze_frame;
let f = func("parity_return", 0, 0, vec![
Op::PushConst(0),
Op::MakeRecord { shape_idx: 0, field_count: 1 },
Op::Return,
]);
let arena = analyze_function(&f);
let frame = analyze_frame(&f);
assert!(arena.sites[0].arena_eligible);
assert!(frame.sites[0].escapes);
}
}