use synth_core::wcet::{
WcetCallSite, WcetDecline, WcetFunction, WcetFunctionHints, WcetIntermediate,
};
use synth_synthesis::{ArmInstruction, ArmOp};
use crate::wcet_loops::{LoopAnalysis, analyze_loops};
const BL_BLX_CALL_OVERHEAD_CYCLES: u64 = 4;
const STRAIGHTLINE_CEIL_PER_HALFWORD: u64 = 5;
fn straightline_expansion(op: &ArmOp) -> u64 {
let byte_len = synth_synthesis::estimate_arm_byte_size(op) as u64;
(byte_len / 2) * STRAIGHTLINE_CEIL_PER_HALFWORD
}
enum OpCost {
Cycles(u64),
LoopedExpansion,
Unmodeled,
}
#[allow(clippy::match_same_arms)] fn op_cost(op: &ArmOp) -> OpCost {
use ArmOp::*;
use OpCost::{Cycles, LoopedExpansion, Unmodeled};
match op {
Add { .. }
| Sub { .. }
| Adds { .. }
| Subs { .. }
| Adc { .. }
| Sbc { .. }
| And { .. }
| Orr { .. }
| Eor { .. }
| Rsb { .. }
| Mvn { .. } => Cycles(1),
Mov { .. } | Movw { .. } | Movt { .. } | MovwSym { .. } | MovtSym { .. } => Cycles(1),
Clz { .. } | Rbit { .. } => Cycles(1),
Sxtb { .. } | Sxth { .. } | Uxtb { .. } | Uxth { .. } => Cycles(1),
Cmp { .. } | Cmn { .. } => Cycles(1),
Lsl { .. }
| Lsr { .. }
| Asr { .. }
| Ror { .. }
| LslReg { .. }
| LsrReg { .. }
| AsrReg { .. }
| RorReg { .. } => Cycles(1),
Mul { .. } => Cycles(1),
Mls { .. } | Mla { .. } => Cycles(2),
Umull { .. } => Cycles(5),
Sdiv { .. } | Udiv { .. } => Cycles(12),
Ldr { .. }
| Str { .. }
| Ldrb { .. }
| Ldrh { .. }
| Ldrsb { .. }
| Ldrsh { .. }
| Strb { .. }
| Strh { .. }
| LdrSym { .. } => Cycles(2),
BOffset { .. } | BCondOffset { .. } | Bx { .. } => Cycles(4),
Bl { .. } | Blx { .. } => Cycles(BL_BLX_CALL_OVERHEAD_CYCLES),
SetCond { .. } => Cycles(3),
SelectMove { .. } => Cycles(2),
Select { .. } => Unmodeled,
Popcnt { .. } => Cycles(straightline_expansion(op)),
Udf { .. } => Cycles(1),
I64DivU { .. } | I64DivS { .. } | I64RemU { .. } | I64RemS { .. } => LoopedExpansion,
I64SetCond { .. }
| I64SetCondZ { .. }
| I64Mul { .. }
| I64Shl { .. }
| I64ShrU { .. }
| I64ShrS { .. }
| I64Rotl { .. }
| I64Rotr { .. }
| I64Clz { .. }
| I64Ctz { .. }
| I64Popcnt { .. }
| I64Extend8S { .. }
| I64Extend16S { .. }
| I64Extend32S { .. } => Cycles(straightline_expansion(op)),
Push { regs } => Cycles(1 + regs.len() as u64 + 3),
Pop { regs } => Cycles(1 + regs.len() as u64 + 3),
Label { .. } | Nop => Cycles(0),
B { .. } | Bcc { .. } | Bhs { .. } | Blo { .. } => Unmodeled, LocalGet { .. }
| LocalSet { .. }
| LocalTee { .. }
| GlobalGet { .. }
| GlobalSet { .. } => Unmodeled,
Call { .. } | CallIndirect { .. } | BrTable { .. } => Unmodeled,
MemorySize { .. } | MemoryGrow { .. } => Unmodeled,
I64Add { .. }
| I64Sub { .. }
| I64And { .. }
| I64Or { .. }
| I64Xor { .. }
| I64Eqz { .. }
| I64Eq { .. }
| I64Ne { .. }
| I64LtS { .. }
| I64LtU { .. }
| I64LeS { .. }
| I64LeU { .. }
| I64GtS { .. }
| I64GtU { .. }
| I64GeS { .. }
| I64GeU { .. }
| I64Const { .. }
| I64Ldr { .. }
| I64Str { .. }
| I64ExtendI32S { .. }
| I64ExtendI32U { .. }
| I32WrapI64 { .. } => Unmodeled,
F32Add { .. }
| F32Sub { .. }
| F32Mul { .. }
| F32Div { .. }
| F32Abs { .. }
| F32Neg { .. }
| F32Sqrt { .. }
| F32Ceil { .. }
| F32Floor { .. }
| F32Trunc { .. }
| F32Nearest { .. }
| F32Min { .. }
| F32Max { .. }
| F32Copysign { .. }
| F32Eq { .. }
| F32Ne { .. }
| F32Lt { .. }
| F32Le { .. }
| F32Gt { .. }
| F32Ge { .. }
| F32Const { .. }
| F32Load { .. }
| F32Store { .. }
| F32ConvertI32S { .. }
| F32ConvertI32U { .. }
| F32ConvertI64S { .. }
| F32ConvertI64U { .. }
| F32ReinterpretI32 { .. }
| I32ReinterpretF32 { .. }
| I32TruncF32S { .. }
| I32TruncF32U { .. }
| F64Add { .. }
| F64Sub { .. }
| F64Mul { .. }
| F64Div { .. }
| F64Abs { .. }
| F64Neg { .. }
| F64Sqrt { .. }
| F64Ceil { .. }
| F64Floor { .. }
| F64Trunc { .. }
| F64Nearest { .. }
| F64Min { .. }
| F64Max { .. }
| F64Copysign { .. }
| F64Eq { .. }
| F64Ne { .. }
| F64Lt { .. }
| F64Le { .. }
| F64Gt { .. }
| F64Ge { .. }
| F64Const { .. }
| F64Load { .. }
| F64Store { .. }
| F64ConvertI32S { .. }
| F64ConvertI32U { .. }
| F64ConvertI64S { .. }
| F64ConvertI64U { .. }
| F64PromoteF32 { .. }
| F32DemoteF64 { .. }
| F64ReinterpretI64 { .. }
| I64ReinterpretF64 { .. }
| I64TruncF64S { .. }
| I64TruncF64U { .. }
| I32TruncF64S { .. }
| I32TruncF64U { .. }
| MveLoad { .. }
| MveStore { .. }
| MveConst { .. }
| MveAnd { .. }
| MveOrr { .. }
| MveEor { .. }
| MveMvn { .. }
| MveBic { .. }
| MveAddI { .. }
| MveSubI { .. }
| MveMulI { .. }
| MveNegI { .. }
| MveCmpEqI { .. }
| MveCmpNeI { .. }
| MveCmpLtS { .. }
| MveCmpLtU { .. }
| MveCmpGtS { .. }
| MveCmpGtU { .. }
| MveCmpLeS { .. }
| MveCmpLeU { .. }
| MveCmpGeS { .. }
| MveCmpGeU { .. }
| MveDup { .. }
| MveExtractLane { .. }
| MveInsertLane { .. }
| MveAddF32 { .. }
| MveSubF32 { .. }
| MveMulF32 { .. }
| MveNegF32 { .. }
| MveAbsF32 { .. }
| MveCmpEqF32 { .. }
| MveCmpNeF32 { .. }
| MveCmpLtF32 { .. }
| MveCmpLeF32 { .. }
| MveCmpGtF32 { .. }
| MveCmpGeF32 { .. }
| MveDupF32 { .. }
| MveExtractLaneF32 { .. }
| MveReplaceLaneF32 { .. }
| MveDivF32 { .. }
| MveSqrtF32 { .. } => Unmodeled,
}
}
pub fn op_worst_case_cycles(op: &ArmOp) -> u64 {
match op_cost(op) {
OpCost::Cycles(c) => c,
OpCost::LoopedExpansion | OpCost::Unmodeled => {
panic!("op_worst_case_cycles called on a non-straight-line op: {op:?}")
}
}
}
pub fn sound_core_class(triple: &str) -> Option<&'static str> {
match triple {
"thumbv7m-none-eabi" | "cortex-m3" => Some("cortex-m3"),
"thumbv7em-none-eabi" | "cortex-m4" => Some("cortex-m4"),
_ => None,
}
}
enum CallClass {
Direct(String),
Indirect,
External,
}
fn classify_call(op: &ArmOp) -> Option<CallClass> {
match op {
ArmOp::Bl { label } => {
if label == "__meld_dispatch_import" {
Some(CallClass::Indirect)
} else if is_local_func_label(label) {
Some(CallClass::Direct(label.clone()))
} else {
Some(CallClass::External)
}
}
ArmOp::Blx { .. } | ArmOp::CallIndirect { .. } => Some(CallClass::Indirect),
ArmOp::Call { func_idx, .. } => Some(CallClass::Direct(format!("func_{func_idx}"))),
_ => None,
}
}
fn is_local_func_label(label: &str) -> bool {
label
.strip_prefix("func_")
.is_some_and(|rest| !rest.is_empty() && rest.bytes().all(|b| b.is_ascii_digit()))
}
fn scan_for_decline(instrs: &[ArmInstruction]) -> Option<WcetDecline> {
for instr in instrs {
if let Some(class) = classify_call(&instr.op) {
match class {
CallClass::Direct(_) => {} CallClass::Indirect => return Some(WcetDecline::IndirectCall),
CallClass::External => return Some(WcetDecline::Call),
}
continue;
}
if matches!(
&instr.op,
ArmOp::B { .. } | ArmOp::Bcc { .. } | ArmOp::Bhs { .. } | ArmOp::Blo { .. }
) {
return Some(WcetDecline::UnresolvedBranch);
}
match op_cost(&instr.op) {
OpCost::LoopedExpansion => return Some(WcetDecline::LoopedExpansion),
OpCost::Unmodeled => return Some(WcetDecline::UnmodeledOp),
OpCost::Cycles(_) => {}
}
}
None
}
pub fn function_wcet(name: &str, instrs: &[ArmInstruction], triple: &str) -> WcetFunction {
function_wcet_with_hints(name, instrs, triple, None)
}
pub fn function_wcet_with_hints(
name: &str,
instrs: &[ArmInstruction],
triple: &str,
hints: Option<&WcetFunctionHints>,
) -> WcetFunction {
match function_wcet_intermediate(name, instrs, triple, hints, None) {
WcetIntermediate::Declined {
name,
reason,
hint_rejections,
} => WcetFunction::declined_with_rejections(name, reason, hint_rejections),
WcetIntermediate::Composable {
name,
own_cycles,
instr_count,
call_sites,
loops,
recursion_cert: _,
hint_rejections,
} => {
if call_sites.is_empty() {
WcetFunction::Bounded {
name,
cycles: own_cycles,
instr_count,
loops,
recursion: None,
hint_rejections,
}
} else {
WcetFunction::declined_with_rejections(name, WcetDecline::Call, hint_rejections)
}
}
}
}
pub fn function_wcet_intermediate(
name: &str,
instrs: &[ArmInstruction],
triple: &str,
hints: Option<&WcetFunctionHints>,
self_label: Option<&str>,
) -> WcetIntermediate {
let declined = |reason, hint_rejections| WcetIntermediate::Declined {
name: name.to_string(),
reason,
hint_rejections,
};
if sound_core_class(triple).is_none() {
return declined(WcetDecline::UnsupportedCore, Vec::new());
}
if let Some(reason) = scan_for_decline(instrs) {
return declined(reason, Vec::new());
}
let (multipliers, loops, hint_rejections) = match analyze_loops(instrs, hints) {
LoopAnalysis::NoLoops { hint_rejections } => (None, Vec::new(), hint_rejections),
LoopAnalysis::Proven {
multipliers,
loops,
hint_rejections,
} => (Some(multipliers), loops, hint_rejections),
LoopAnalysis::Unproven { hint_rejections } => {
return declined(WcetDecline::Loop, hint_rejections);
}
};
let mult_at = |i: usize| -> u128 { multipliers.as_ref().map_or(1u128, |m| m[i]) };
let cycles_wide: u128 = instrs
.iter()
.enumerate()
.map(|(i, instr)| (op_worst_case_cycles(&instr.op) as u128).saturating_mul(mult_at(i)))
.fold(0u128, u128::saturating_add);
let Ok(own_cycles) = u64::try_from(cycles_wide) else {
return declined(WcetDecline::Loop, hint_rejections);
};
let call_sites: Vec<WcetCallSite> = instrs
.iter()
.enumerate()
.filter_map(|(i, instr)| match classify_call(&instr.op) {
Some(CallClass::Direct(callee_label)) => Some(WcetCallSite {
callee_label,
multiplier: mult_at(i),
}),
_ => None,
})
.collect();
let mult_at_fn = |i: usize| mult_at(i);
let recursion_hint = hints.and_then(|h| h.recursion_depth);
let (recursion_cert, mut rec_rejections) = match crate::wcet_recursion::analyze_recursion(
instrs,
self_label,
&mult_at_fn,
recursion_hint,
) {
crate::wcet_recursion::RecursionAnalysis::NotRecursive => (None, Vec::new()),
crate::wcet_recursion::RecursionAnalysis::Unprovable { hint_rejections } => {
(None, hint_rejections)
}
crate::wcet_recursion::RecursionAnalysis::Certified {
cert,
hint_rejections,
} => (Some(cert), hint_rejections),
};
let mut hint_rejections = hint_rejections;
hint_rejections.append(&mut rec_rejections);
WcetIntermediate::Composable {
name: name.to_string(),
own_cycles,
instr_count: instrs.len(),
call_sites,
loops,
recursion_cert,
hint_rejections,
}
}
#[cfg(test)]
mod tests {
use super::*;
use synth_synthesis::{Condition, Operand2, Reg};
fn insn(op: ArmOp) -> ArmInstruction {
ArmInstruction {
op,
source_line: None,
}
}
#[test]
fn loop_free_sum_is_exact() {
let instrs = vec![
insn(ArmOp::Mov {
rd: Reg::R0,
op2: Operand2::Imm(1),
}), insn(ArmOp::Add {
rd: Reg::R0,
rn: Reg::R0,
op2: Operand2::Reg(Reg::R1),
}), insn(ArmOp::Ldr {
rd: Reg::R2,
addr: synth_synthesis::MemAddr {
base: Reg::R1,
offset: 0,
offset_reg: None,
},
}), insn(ArmOp::Bx { rm: Reg::LR }), ];
match function_wcet("leaf", &instrs, "cortex-m4") {
WcetFunction::Bounded { cycles, .. } => assert_eq!(cycles, 1 + 1 + 2 + 4),
other => panic!("expected bounded, got {other:?}"),
}
}
#[test]
fn backward_branch_declines_loop() {
let instrs = vec![
insn(ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Imm(0),
}),
insn(ArmOp::BCondOffset {
cond: Condition::NE,
offset: -4,
}),
];
assert!(matches!(
function_wcet("spin", &instrs, "cortex-m4"),
WcetFunction::Declined {
reason: WcetDecline::Loop,
..
}
));
}
#[test]
fn forward_branch_is_loop_free() {
let instrs = vec![
insn(ArmOp::BCondOffset {
cond: Condition::EQ,
offset: -1,
}),
insn(ArmOp::Bx { rm: Reg::LR }),
];
assert!(matches!(
function_wcet("fwd", &instrs, "cortex-m4"),
WcetFunction::Bounded { .. }
));
}
#[test]
fn call_declines() {
let instrs = vec![insn(ArmOp::Bl {
label: "callee".into(),
})];
assert!(matches!(
function_wcet("caller", &instrs, "cortex-m4"),
WcetFunction::Declined {
reason: WcetDecline::Call,
..
}
));
}
#[test]
fn looped_i64_div_declines() {
let instrs = vec![insn(ArmOp::I64DivU {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R2,
rnhi: Reg::R3,
rmlo: Reg::R4,
rmhi: Reg::R5,
elide_zero_guard: false,
})];
assert!(matches!(
function_wcet("div", &instrs, "cortex-m4"),
WcetFunction::Declined {
reason: WcetDecline::LoopedExpansion,
..
}
));
}
#[test]
fn m7_declines_unsupported_core() {
let instrs = vec![insn(ArmOp::Bx { rm: Reg::LR })];
assert!(matches!(
function_wcet("f", &instrs, "cortex-m7"),
WcetFunction::Declined {
reason: WcetDecline::UnsupportedCore,
..
}
));
}
}