use crate::wcet_loops::may_move_sp;
use std::collections::HashMap;
pub use synth_core::stack_depth::{StackDecline, StackFrame, StackResult};
use synth_synthesis::{ArmInstruction, ArmOp, Operand2, Reg};
const VFP_CALLEE_SAVED_BYTES: i64 = 8 * 8;
pub fn analyze_function(name: &str, instrs: &[ArmInstruction]) -> StackFrame {
let mut cur: i64 = 0;
let mut max: i64 = 0;
let mut calls = Vec::new();
let mut decline = None;
for ins in instrs {
match &ins.op {
ArmOp::Push { regs } => cur += 4 * regs.len() as i64,
ArmOp::Pop { regs } => cur -= 4 * regs.len() as i64,
ArmOp::VPushCalleeSavedVfp => cur += VFP_CALLEE_SAVED_BYTES,
ArmOp::VPopCalleeSavedVfp => cur -= VFP_CALLEE_SAVED_BYTES,
ArmOp::Sub {
rd: Reg::SP,
rn: Reg::SP,
op2: Operand2::Imm(n),
} => cur += i64::from(*n),
ArmOp::Add {
rd: Reg::SP,
rn: Reg::SP,
op2: Operand2::Imm(n),
} => cur -= i64::from(*n),
ArmOp::Bl { label } => calls.push(label.clone()),
ArmOp::CallIndirect { .. } | ArmOp::Blx { .. } | ArmOp::Bx { .. } => {
decline.get_or_insert(StackDecline::IndirectCall);
}
other => {
if may_move_sp(other) {
decline.get_or_insert(StackDecline::UnknownSpMove);
}
}
}
if cur > max {
max = cur;
}
}
StackFrame {
name: name.to_string(),
own_bytes: max.max(0) as u64,
calls,
decline,
}
}
pub fn compose(frames: &[StackFrame], index_by_label: &HashMap<String, usize>) -> Vec<StackResult> {
#[derive(Clone)]
enum State {
Pending,
OnStack,
Bounded(u64),
Declined(StackDecline),
}
let mut state = vec![State::Pending; frames.len()];
fn resolve(
i: usize,
frames: &[StackFrame],
index_by_label: &HashMap<String, usize>,
state: &mut Vec<State>,
) -> State {
match &state[i] {
State::Bounded(b) => return State::Bounded(*b),
State::Declined(d) => return State::Declined(d.clone()),
State::OnStack => return State::Declined(StackDecline::Recursion),
State::Pending => {}
}
if let Some(d) = &frames[i].decline {
let s = State::Declined(d.clone());
state[i] = s.clone();
return s;
}
state[i] = State::OnStack;
let mut deepest: u64 = 0;
for label in &frames[i].calls {
let Some(&j) = index_by_label.get(label) else {
let s = State::Declined(StackDecline::ExternalCall);
state[i] = s.clone();
return s;
};
match resolve(j, frames, index_by_label, state) {
State::Bounded(b) => deepest = deepest.max(b),
State::Declined(StackDecline::Recursion) => {
let s = State::Declined(StackDecline::Recursion);
state[i] = s.clone();
return s;
}
State::Declined(_) => {
let s = State::Declined(StackDecline::CalleeUnbounded);
state[i] = s.clone();
return s;
}
State::Pending | State::OnStack => unreachable!("resolve always settles"),
}
}
let s = State::Bounded(frames[i].own_bytes.saturating_add(deepest));
state[i] = s.clone();
s
}
(0..frames.len())
.map(|i| match resolve(i, frames, index_by_label, &mut state) {
State::Bounded(b) => StackResult::Bounded {
name: frames[i].name.clone(),
bytes: b,
},
State::Declined(d) => StackResult::Declined {
name: frames[i].name.clone(),
reason: d,
},
State::Pending | State::OnStack => unreachable!("resolve always settles"),
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
fn ins(op: ArmOp) -> ArmInstruction {
ArmInstruction {
op,
source_line: None,
}
}
fn push(n: usize) -> ArmInstruction {
ins(ArmOp::Push {
regs: vec![Reg::R4; n],
})
}
fn sub_sp(n: i32) -> ArmInstruction {
ins(ArmOp::Sub {
rd: Reg::SP,
rn: Reg::SP,
op2: Operand2::Imm(n),
})
}
#[test]
fn prologue_push_counts_even_with_no_sub_sp() {
let f = analyze_function(
"leaf",
&[
push(2),
ins(ArmOp::Pop {
regs: vec![Reg::R4; 2],
}),
],
);
assert_eq!(f.own_bytes, 8, "2 registers pushed = 8 bytes, not 0");
}
#[test]
fn frame_is_push_plus_sub_sp() {
let f = analyze_function("mid", &[push(6), sub_sp(32)]);
assert_eq!(f.own_bytes, 24 + 32);
}
#[test]
fn transient_push_inside_the_body_raises_the_maximum() {
let f = analyze_function(
"t",
&[
push(6),
sub_sp(8),
push(2),
ins(ArmOp::Pop {
regs: vec![Reg::R4; 2],
}),
],
);
assert_eq!(f.own_bytes, 24 + 8 + 8);
}
#[test]
fn composition_takes_the_max_over_branches_not_the_sum() {
let frames = vec![
StackFrame {
name: "top".into(),
own_bytes: 56,
calls: vec!["func_1".into(), "func_2".into()],
decline: None,
},
StackFrame {
name: "a".into(),
own_bytes: 56,
calls: vec![],
decline: None,
},
StackFrame {
name: "b".into(),
own_bytes: 56,
calls: vec![],
decline: None,
},
];
let idx = HashMap::from([("func_1".to_string(), 1), ("func_2".to_string(), 2)]);
let out = compose(&frames, &idx);
match &out[0] {
StackResult::Bounded { bytes, .. } => assert_eq!(
*bytes, 112,
"must be own + MAX(callees) = 112, not the sum 168"
),
other => panic!("expected bounded, got {other:?}"),
}
}
#[test]
fn a_cycle_declines_recursion_rather_than_looping_forever() {
let frames = vec![
StackFrame {
name: "f".into(),
own_bytes: 8,
calls: vec!["func_1".into()],
decline: None,
},
StackFrame {
name: "g".into(),
own_bytes: 8,
calls: vec!["func_0".into()],
decline: None,
},
];
let idx = HashMap::from([("func_0".to_string(), 0), ("func_1".to_string(), 1)]);
for r in compose(&frames, &idx) {
match r {
StackResult::Declined { reason, .. } => assert_eq!(reason, StackDecline::Recursion),
other => panic!("expected a recursion decline, got {other:?}"),
}
}
}
#[test]
fn an_unresolved_label_is_an_external_call_decline_not_a_zero() {
let frames = vec![StackFrame {
name: "f".into(),
own_bytes: 24,
calls: vec!["func_99".into()],
decline: None,
}];
match &compose(&frames, &HashMap::new())[0] {
StackResult::Declined { reason, .. } => assert_eq!(*reason, StackDecline::ExternalCall),
other => panic!("an unknown callee must DECLINE, not contribute 0: {other:?}"),
}
}
#[test]
fn a_decline_propagates_up_as_callee_unbounded() {
let frames = vec![
StackFrame {
name: "top".into(),
own_bytes: 24,
calls: vec!["func_1".into()],
decline: None,
},
StackFrame {
name: "bad".into(),
own_bytes: 24,
calls: vec![],
decline: Some(StackDecline::IndirectCall),
},
];
let idx = HashMap::from([("func_1".to_string(), 1)]);
match &compose(&frames, &idx)[0] {
StackResult::Declined { reason, .. } => {
assert_eq!(*reason, StackDecline::CalleeUnbounded)
}
other => panic!("expected propagation, got {other:?}"),
}
}
}