use std::collections::BTreeMap;
use synth_core::wcet::{
WcetFunctionHints, WcetHintReject, WcetHintRejection, WcetLoopBound, WcetLoopBoundSource,
};
use synth_synthesis::{ArmInstruction, ArmOp, Condition, Operand2, Reg};
pub(crate) enum LoopAnalysis {
NoLoops {
hint_rejections: Vec<WcetHintRejection>,
},
Proven {
multipliers: Vec<u128>,
loops: Vec<WcetLoopBound>,
hint_rejections: Vec<WcetHintRejection>,
},
Unproven {
hint_rejections: Vec<WcetHintRejection>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Rel {
Eq,
Ne,
LtS,
LeS,
GtS,
GeS,
LoU,
LsU,
HiU,
HsU,
}
impl Rel {
pub(crate) fn of(cond: Condition) -> Rel {
match cond {
Condition::EQ => Rel::Eq,
Condition::NE => Rel::Ne,
Condition::LT => Rel::LtS,
Condition::LE => Rel::LeS,
Condition::GT => Rel::GtS,
Condition::GE => Rel::GeS,
Condition::LO => Rel::LoU,
Condition::LS => Rel::LsU,
Condition::HI => Rel::HiU,
Condition::HS => Rel::HsU,
}
}
pub(crate) fn negate(self) -> Rel {
match self {
Rel::Eq => Rel::Ne,
Rel::Ne => Rel::Eq,
Rel::LtS => Rel::GeS,
Rel::GeS => Rel::LtS,
Rel::LeS => Rel::GtS,
Rel::GtS => Rel::LeS,
Rel::LoU => Rel::HsU,
Rel::HsU => Rel::LoU,
Rel::LsU => Rel::HiU,
Rel::HiU => Rel::LsU,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Pred {
pub(crate) off: i64,
pub(crate) add: i64,
pub(crate) rel: Rel,
pub(crate) rhs: i32,
pub(crate) masked_ceiling: Option<i32>,
}
impl Pred {
pub(crate) fn negate(self) -> Pred {
Pred {
rel: self.rel.negate(),
..self
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Sym {
Top,
Const(i32),
Slot { off: i64, add: i64 },
Bool(Pred),
Masked { mask: i32 },
}
#[derive(Clone)]
struct WalkState {
regs: [Sym; 16],
written: BTreeMap<i64, Sym>,
tainted: std::collections::BTreeSet<i64>,
flags: Option<(Sym, Sym)>,
}
impl WalkState {
fn fresh() -> Self {
WalkState {
regs: [Sym::Top; 16],
written: BTreeMap::new(),
tainted: std::collections::BTreeSet::new(),
flags: None,
}
}
fn reg(&self, r: Reg) -> Sym {
self.regs[r as usize]
}
fn set_reg(&mut self, r: Reg, v: Sym) {
self.regs[r as usize] = v;
}
fn kill_all_regs(&mut self) {
self.regs = [Sym::Top; 16];
}
fn op2(&self, op2: &Operand2) -> Sym {
match op2 {
Operand2::Imm(c) => Sym::Const(*c),
Operand2::Reg(r) => self.reg(*r),
Operand2::RegShift { .. } => Sym::Top,
}
}
fn read_slot(&self, off: i64) -> Sym {
if off < 0 || off % 4 != 0 || self.tainted.contains(&off) {
return Sym::Top;
}
self.written
.get(&off)
.copied()
.unwrap_or(Sym::Slot { off, add: 0 })
}
fn write_slot_word(&mut self, off: i64, v: Sym) {
if off < 0 {
self.taint_range(off, 4);
} else if off % 4 == 0 {
self.tainted.remove(&off);
self.written.insert(off, v);
} else {
self.taint_range(off, 4);
}
}
fn taint_range(&mut self, off: i64, width: i64) {
let first = off & !3;
let last = (off + width - 1) & !3;
let mut w = first;
while w <= last {
self.written.remove(&w);
self.tainted.insert(w);
w += 4;
}
}
}
fn eval_pred(cond: Condition, flags: &Option<(Sym, Sym)>) -> Option<Pred> {
let (a, b) = flags.as_ref()?;
match (a, b) {
(Sym::Slot { off, add }, Sym::Const(c)) => Some(Pred {
off: *off,
add: *add,
rel: Rel::of(cond),
rhs: *c,
masked_ceiling: None,
}),
(Sym::Slot { off, add }, Sym::Masked { mask }) => Some(Pred {
off: *off,
add: *add,
rel: Rel::of(cond),
rhs: *mask,
masked_ceiling: Some(*mask),
}),
(Sym::Bool(p), Sym::Const(0)) => match cond {
Condition::EQ => Some(p.negate()),
Condition::NE => Some(*p),
_ => None,
},
_ => None,
}
}
#[derive(Debug, Clone, Copy)]
enum ExitShape {
HeadTest(Pred),
BottomTest(Pred),
}
struct Region {
head: usize,
closer: usize,
closer_cond: Option<Condition>,
children: Vec<usize>,
has_parent: bool,
proof: Option<(i64, i64, ExitShape)>,
init: Option<i32>,
trip: Option<(u64, bool)>,
masked_bound: bool,
factor: u128,
}
pub(crate) fn analyze_loops(
instrs: &[ArmInstruction],
hints: Option<&WcetFunctionHints>,
) -> LoopAnalysis {
let n = instrs.len();
let encoder = crate::arm_encoder::ArmEncoder::new_thumb2();
let mut sizes: Vec<i64> = Vec::with_capacity(n);
for instr in instrs {
match encoder.encode(&instr.op) {
Ok(bytes) => sizes.push(bytes.len() as i64),
Err(_) => return unproven(hints, &[]),
}
}
let mut positions = Vec::with_capacity(n);
let mut pos: i64 = 0;
for &sz in &sizes {
positions.push(pos);
pos += sz;
}
let mut idx_at: BTreeMap<i64, usize> = BTreeMap::new();
for (i, &p) in positions.iter().enumerate() {
idx_at.entry(p).or_insert(i);
}
let target_byte = |i: usize, offset: i32| positions[i] + 4 + (offset as i64) * 2;
let mut regions: Vec<Region> = Vec::new();
for (i, instr) in instrs.iter().enumerate() {
let (offset, cond) = match &instr.op {
ArmOp::BOffset { offset } => (*offset, None),
ArmOp::BCondOffset { cond, offset } => (*offset, Some(*cond)),
_ => continue,
};
let tgt = target_byte(i, offset);
if tgt > positions[i] {
continue; }
let Some(&head) = idx_at.get(&tgt) else {
return unproven(hints, &[]); };
if head > i {
return unproven(hints, &[]);
}
regions.push(Region {
head,
closer: i,
closer_cond: cond,
children: Vec::new(),
has_parent: false,
proof: None,
init: None,
trip: None,
masked_bound: false,
factor: 1,
});
}
if regions.is_empty() {
return LoopAnalysis::NoLoops {
hint_rejections: reject_extra_hints(hints, 0, &[]),
};
}
regions.sort_by_key(|r| (positions[r.head], r.closer));
let head_offsets: Vec<i64> = regions.iter().map(|r| positions[r.head]).collect();
for w in regions.windows(2) {
if w[0].head == w[1].head {
return unproven(hints, &head_offsets);
}
}
for a in 0..regions.len() {
for b in 0..regions.len() {
if a == b {
continue;
}
let (ra, rb) = (®ions[a], ®ions[b]);
let disjoint = ra.closer < rb.head || rb.closer < ra.head;
let a_in_b = rb.head < ra.head && ra.closer < rb.closer;
let b_in_a = ra.head < rb.head && rb.closer < ra.closer;
if !(disjoint || a_in_b || b_in_a) {
return unproven(hints, &head_offsets);
}
}
}
for a in 0..regions.len() {
let mut parent: Option<usize> = None;
for b in 0..regions.len() {
if b == a {
continue;
}
if regions[b].head < regions[a].head && regions[a].closer < regions[b].closer {
if parent.is_none_or(|p| {
regions[b].closer - regions[b].head < regions[p].closer - regions[p].head
}) {
parent = Some(b);
}
}
}
if let Some(p) = parent {
regions[a].has_parent = true;
regions[p].children.push(a);
}
}
for r in &mut regions {
r.children.sort_by_key(|&c| c); }
let innermost_of = |i: usize| -> Option<usize> {
regions
.iter()
.enumerate()
.filter(|(_, r)| r.head <= i && i <= r.closer)
.min_by_key(|(_, r)| r.closer - r.head)
.map(|(k, _)| k)
};
for (i, instr) in instrs.iter().enumerate() {
let (offset, is_cond) = match &instr.op {
ArmOp::BOffset { offset } => (*offset, false),
ArmOp::BCondOffset { offset, .. } => (*offset, true),
_ => continue,
};
if regions.iter().any(|r| r.closer == i) {
continue; }
let tgt = target_byte(i, offset);
if tgt <= positions[i] {
return unproven(hints, &head_offsets); }
if !is_cond {
return unproven(hints, &head_offsets); }
let Some(rid) = innermost_of(i) else {
return unproven(hints, &head_offsets); };
let resume = positions[regions[rid].closer] + sizes[regions[rid].closer];
if tgt != resume {
return unproven(hints, &head_offsets); }
}
for r in ®ions {
for instr in &instrs[r.head..=r.closer] {
match &instr.op {
ArmOp::Push { .. } | ArmOp::Pop { .. } | ArmOp::Bx { .. } => {
return unproven(hints, &head_offsets);
}
op => {
if may_move_sp(op) {
return unproven(hints, &head_offsets);
}
if let ArmOp::Str { addr, .. }
| ArmOp::Strb { addr, .. }
| ArmOp::Strh { addr, .. } = op
&& addr.base == Reg::SP
&& addr.offset_reg.is_some()
{
return unproven(hints, &head_offsets);
}
}
}
}
}
let mut order: Vec<usize> = (0..regions.len()).collect();
order.sort_by_key(|&k| regions[k].closer - regions[k].head);
for k in order {
if !prove_region_structure(&mut regions, k, instrs) {
return unproven(hints, &head_offsets);
}
}
if !resolve_toplevel_inits(&mut regions, instrs) {
return unproven(hints, &head_offsets);
}
for r in &mut regions {
let (Some((_, step, shape)), Some(init)) = (r.proof, r.init) else {
r.trip = None;
continue;
};
r.masked_bound = match shape {
ExitShape::HeadTest(p) | ExitShape::BottomTest(p) => p.masked_ceiling.is_some(),
};
r.trip = match shape {
ExitShape::HeadTest(p) => masked_exit_index(init, step, &p),
ExitShape::BottomTest(p) => {
masked_exit_index(init, step, &p.negate())
.and_then(|(m, h)| m.checked_add(1).map(|k| (k, h)))
}
};
}
let mut rejections: Vec<WcetHintRejection> = Vec::new();
let empty = Vec::new();
let hint_list: &Vec<Option<u64>> = hints.map_or(&empty, |h| &h.loop_bounds);
let mut loops_out: Vec<WcetLoopBound> = Vec::new();
let mut all_proven = true;
for (idx, r) in regions.iter_mut().enumerate() {
let hint = hint_list.get(idx).copied().flatten();
let head_off = positions[r.head] as u64;
let (accepted, source): (Option<u64>, WcetLoopBoundSource) = match (r.trip, hint) {
(Some((k, false)), None) => (Some(k), WcetLoopBoundSource::Static),
(Some((k, false)), Some(h)) => {
if h < k {
rejections.push(rejection(
idx,
Some(head_off),
h,
WcetHintReject::HintBelowDerivedTrip,
));
}
(Some(k), WcetLoopBoundSource::Static)
}
(Some((_, true)), None) => (None, WcetLoopBoundSource::Static),
(Some((k, true)), Some(h)) => {
if k <= h {
let source = if r.masked_bound {
WcetLoopBoundSource::MaskCeiling
} else {
WcetLoopBoundSource::HintVerified
};
(Some(k), source)
} else {
rejections.push(rejection(
idx,
Some(head_off),
h,
WcetHintReject::HintBelowDerivedTrip,
));
(None, WcetLoopBoundSource::Static)
}
}
(None, Some(h)) => {
rejections.push(rejection(
idx,
Some(head_off),
h,
WcetHintReject::HintUnverifiableInduction,
));
(None, WcetLoopBoundSource::Static)
}
(None, None) => (None, WcetLoopBoundSource::Static),
};
match accepted {
Some(k) => {
r.factor = match r.closer_cond {
None => k as u128 + 1,
Some(_) => (k as u128).max(1),
};
loops_out.push(WcetLoopBound {
head_offset: head_off,
trip_count: k,
region_instr_count: r.closer - r.head + 1,
source,
hint: match source {
WcetLoopBoundSource::HintVerified | WcetLoopBoundSource::MaskCeiling => {
hint
}
WcetLoopBoundSource::Static => None,
},
});
}
None => all_proven = false,
}
}
rejections.extend(reject_extra_hints(hints, regions.len(), &head_offsets));
if !all_proven {
return LoopAnalysis::Unproven {
hint_rejections: rejections,
};
}
let mut multipliers = vec![1u128; n];
for r in ®ions {
for m in multipliers.iter_mut().take(r.closer + 1).skip(r.head) {
*m = m.saturating_mul(r.factor);
}
}
LoopAnalysis::Proven {
multipliers,
loops: loops_out,
hint_rejections: rejections,
}
}
fn rejection(
loop_index: usize,
head_offset: Option<u64>,
hint: u64,
reason: WcetHintReject,
) -> WcetHintRejection {
let note = reason.note().to_string();
WcetHintRejection {
loop_index,
head_offset,
hint,
reason,
note,
}
}
fn reject_extra_hints(
hints: Option<&WcetFunctionHints>,
loop_count: usize,
_head_offsets: &[i64],
) -> Vec<WcetHintRejection> {
let Some(h) = hints else {
return Vec::new();
};
h.loop_bounds
.iter()
.enumerate()
.skip(loop_count)
.filter_map(|(i, b)| b.map(|v| rejection(i, None, v, WcetHintReject::HintUnknownLoop)))
.collect()
}
fn unproven(hints: Option<&WcetFunctionHints>, head_offsets: &[i64]) -> LoopAnalysis {
let rejections = hints
.map(|h| {
h.loop_bounds
.iter()
.enumerate()
.filter_map(|(i, b)| {
b.map(|v| {
rejection(
i,
head_offsets.get(i).map(|&o| o as u64),
v,
WcetHintReject::HintUnverifiableInduction,
)
})
})
.collect()
})
.unwrap_or_default();
LoopAnalysis::Unproven {
hint_rejections: rejections,
}
}
fn prove_region_structure(regions: &mut [Region], k: usize, instrs: &[ArmInstruction]) -> bool {
let (head, closer) = (regions[k].head, regions[k].closer);
let mut st = WalkState::fresh();
let mut store_events: Vec<(i64, Sym)> = Vec::new();
let mut exits: Vec<Option<Pred>> = Vec::new();
let mut i = head;
while i <= closer {
if let Some(&c) = regions[k].children.iter().find(|&&c| regions[c].head == i) {
let c_off = match regions[c].proof {
Some((off, _, _)) => off,
None => return false, };
match st.read_slot(c_off) {
Sym::Const(init) => {
regions[c].init = Some(init);
}
_ => return false,
}
st.kill_all_regs();
st.flags = None;
for instr in &instrs[regions[c].head..=regions[c].closer] {
match &instr.op {
ArmOp::Str { addr, .. } if addr.base == Reg::SP => {
st.taint_range(addr.offset as i64, 4);
}
ArmOp::Strb { addr, .. } if addr.base == Reg::SP => {
st.taint_range(addr.offset as i64, 1);
}
ArmOp::Strh { addr, .. } if addr.base == Reg::SP => {
st.taint_range(addr.offset as i64, 2);
}
_ => {}
}
}
i = regions[c].closer + 1;
continue;
}
let instr = &instrs[i];
match &instr.op {
ArmOp::BOffset { .. } if i == closer => break, ArmOp::BCondOffset { cond, .. } if i == closer => {
let Some(p) = eval_pred(*cond, &st.flags) else {
return false;
};
let Some((off, step, _)) = counter_candidate(&st, &store_events, &[Some(p)]) else {
return false;
};
if p.off != off {
return false;
}
regions[k].proof = Some((off, step, ExitShape::BottomTest(p)));
return true;
}
ArmOp::BCondOffset { cond, .. } => {
exits.push(eval_pred(*cond, &st.flags));
}
ArmOp::BOffset { .. } => return false, op => sym_step(op, &mut st, &mut store_events),
}
i += 1;
}
let Some((off, step, pred)) = counter_candidate(&st, &store_events, &exits) else {
return false;
};
regions[k].proof = Some((off, step, ExitShape::HeadTest(pred)));
true
}
fn counter_candidate(
st: &WalkState,
store_events: &[(i64, Sym)],
exits: &[Option<Pred>],
) -> Option<(i64, i64, Pred)> {
for p in exits.iter().flatten() {
let off = p.off;
if st.tainted.contains(&off) {
continue;
}
let events: Vec<&Sym> = store_events
.iter()
.filter(|(o, _)| *o == off)
.map(|(_, s)| s)
.collect();
if events.len() != 1 {
continue;
}
let Sym::Slot { off: so, add: step } = *events[0] else {
continue;
};
if so != off || step == 0 {
continue;
}
if st.written.get(&off) != Some(&Sym::Slot { off, add: step }) {
continue;
}
return Some((off, step, *p));
}
None
}
fn sym_step(op: &ArmOp, st: &mut WalkState, store_events: &mut Vec<(i64, Sym)>) {
use ArmOp::*;
match op {
Mov { rd, op2 } => {
let v = st.op2(op2);
st.set_reg(*rd, v);
st.flags = None; }
Movw { rd, imm16 } => {
st.set_reg(*rd, Sym::Const(*imm16 as i32));
st.flags = None;
}
Add { rd, rn, op2 } | Adds { rd, rn, op2 } => {
let v = sym_add(st.reg(*rn), st.op2(op2), 1);
st.set_reg(*rd, v);
st.flags = None; }
Sub { rd, rn, op2 } | Subs { rd, rn, op2 } => {
let v = sym_add(st.reg(*rn), st.op2(op2), -1);
st.set_reg(*rd, v);
st.flags = None;
}
Ldr { rd, addr } => {
let v = if addr.base == Reg::SP && addr.offset_reg.is_none() {
st.read_slot(addr.offset as i64)
} else {
Sym::Top
};
st.set_reg(*rd, v);
}
Ldrb { rd, addr } | Ldrh { rd, addr } | Ldrsb { rd, addr } | Ldrsh { rd, addr } => {
let _ = addr;
st.set_reg(*rd, Sym::Top);
}
LdrSym { rd, .. } => st.set_reg(*rd, Sym::Top),
Str { rd, addr } => {
if addr.base == Reg::SP {
if addr.offset_reg.is_none() {
let off = addr.offset as i64;
let v = st.reg(*rd);
st.write_slot_word(off, v);
if off % 4 == 0 {
store_events.push((off, v));
}
} else {
let offs: Vec<i64> = st.written.keys().copied().collect();
for o in offs {
st.taint_range(o, 4);
}
}
}
}
Strb { rd: _, addr } => {
if addr.base == Reg::SP && addr.offset_reg.is_none() {
st.taint_range(addr.offset as i64, 1);
}
}
Strh { rd: _, addr } => {
if addr.base == Reg::SP && addr.offset_reg.is_none() {
st.taint_range(addr.offset as i64, 2);
}
}
Cmp { rn, op2 } => {
st.flags = Some((st.reg(*rn), st.op2(op2)));
}
Cmn { .. } => st.flags = None,
SetCond { rd, cond } => {
let v = eval_pred(*cond, &st.flags).map_or(Sym::Top, Sym::Bool);
st.set_reg(*rd, v);
}
SelectMove { rd, .. } => {
st.set_reg(*rd, Sym::Top); }
Label { .. } | Nop => {}
Udf { .. } => {
st.kill_all_regs();
st.flags = None;
}
And { rd, op2, .. } => {
let v = match st.op2(op2) {
Sym::Const(mask) if mask >= 0 => Sym::Masked { mask },
_ => Sym::Top,
};
st.set_reg(*rd, v);
st.flags = None;
}
Orr { rd, .. }
| Eor { rd, .. }
| Rsb { rd, .. }
| Mvn { rd, .. }
| Adc { rd, .. }
| Sbc { rd, .. }
| Movt { rd, .. }
| MovwSym { rd, .. }
| MovtSym { rd, .. }
| Clz { rd, .. }
| Rbit { rd, .. }
| Sxtb { rd, .. }
| Sxth { rd, .. }
| Uxtb { rd, .. }
| Uxth { rd, .. }
| Lsl { rd, .. }
| Lsr { rd, .. }
| Asr { rd, .. }
| Ror { rd, .. }
| LslReg { rd, .. }
| LsrReg { rd, .. }
| AsrReg { rd, .. }
| RorReg { rd, .. }
| Mul { rd, .. }
| Mla { rd, .. }
| Mls { rd, .. }
| Sdiv { rd, .. }
| Udiv { rd, .. } => {
st.set_reg(*rd, Sym::Top);
st.flags = None;
}
Umull { rdlo, rdhi, .. } => {
st.set_reg(*rdlo, Sym::Top);
st.set_reg(*rdhi, Sym::Top);
st.flags = None;
}
_ => {
st.kill_all_regs();
st.flags = None;
}
}
}
fn sym_add(a: Sym, b: Sym, sign: i64) -> Sym {
match (a, b) {
(Sym::Const(x), Sym::Const(y)) => {
let r = x as i64 + sign * y as i64;
i32::try_from(r).map_or(Sym::Top, Sym::Const)
}
(Sym::Slot { off, add }, Sym::Const(y)) => {
match add.checked_add(sign * y as i64) {
Some(na) if na.abs() <= 1 << 33 => Sym::Slot { off, add: na },
_ => Sym::Top,
}
}
(Sym::Const(x), Sym::Slot { off, add }) if sign == 1 => match add.checked_add(x as i64) {
Some(na) if na.abs() <= 1 << 33 => Sym::Slot { off, add: na },
_ => Sym::Top,
},
_ => Sym::Top,
}
}
#[allow(clippy::match_same_arms)] fn may_move_sp(op: &ArmOp) -> bool {
use ArmOp::*;
match op {
Add { rd, .. }
| Sub { rd, .. }
| Adds { rd, .. }
| Subs { rd, .. }
| Adc { rd, .. }
| Sbc { rd, .. }
| Mov { rd, .. }
| Mvn { rd, .. }
| Movw { rd, .. }
| Movt { rd, .. }
| MovwSym { rd, .. }
| MovtSym { rd, .. }
| And { rd, .. }
| Orr { rd, .. }
| Eor { rd, .. }
| Rsb { rd, .. }
| Clz { rd, .. }
| Rbit { rd, .. }
| Sxtb { rd, .. }
| Sxth { rd, .. }
| Uxtb { rd, .. }
| Uxth { rd, .. }
| Lsl { rd, .. }
| Lsr { rd, .. }
| Asr { rd, .. }
| Ror { rd, .. }
| LslReg { rd, .. }
| LsrReg { rd, .. }
| AsrReg { rd, .. }
| RorReg { rd, .. }
| Mul { rd, .. }
| Mla { rd, .. }
| Mls { rd, .. }
| Sdiv { rd, .. }
| Udiv { rd, .. }
| Popcnt { rd, .. }
| Ldr { rd, .. }
| Ldrb { rd, .. }
| Ldrh { rd, .. }
| Ldrsb { rd, .. }
| Ldrsh { rd, .. }
| LdrSym { rd, .. }
| SetCond { rd, .. }
| SelectMove { rd, .. } => *rd == Reg::SP,
Umull { rdlo, rdhi, .. } => *rdlo == Reg::SP || *rdhi == Reg::SP,
I64SetCond { rd, .. } | I64SetCondZ { rd, .. } | I64Clz { rd, .. } | I64Ctz { rd, .. } => {
*rd == Reg::SP
}
I64Const { rdlo, rdhi, .. }
| I64Ldr { rdlo, rdhi, .. }
| I64Extend8S { rdlo, rdhi, .. }
| I64Extend16S { rdlo, rdhi, .. }
| I64Extend32S { rdlo, rdhi, .. } => *rdlo == Reg::SP || *rdhi == Reg::SP,
I64Mul { rd_lo, rd_hi, .. }
| I64Shl { rd_lo, rd_hi, .. }
| I64ShrU { rd_lo, rd_hi, .. }
| I64ShrS { rd_lo, rd_hi, .. } => *rd_lo == Reg::SP || *rd_hi == Reg::SP,
Push { .. } | Pop { .. } => true,
I64Popcnt { .. }
| I64Rotl { .. }
| I64Rotr { .. }
| I64DivS { .. }
| I64DivU { .. }
| I64RemS { .. }
| I64RemU { .. } => false,
Cmp { .. }
| Cmn { .. }
| Str { .. }
| Strb { .. }
| Strh { .. }
| I64Str { .. }
| Label { .. }
| Nop
| Udf { .. }
| Bx { .. }
| Bl { .. }
| BOffset { .. }
| BCondOffset { .. } => false,
MemorySize { .. }
| MemoryGrow { .. }
| B { .. }
| Bhs { .. }
| Blo { .. }
| Bcc { .. }
| Blx { .. }
| Select { .. }
| LocalGet { .. }
| LocalSet { .. }
| LocalTee { .. }
| GlobalGet { .. }
| GlobalSet { .. }
| BrTable { .. }
| Call { .. }
| CallIndirect { .. }
| I64Add { .. }
| I64Sub { .. }
| I64And { .. }
| I64Or { .. }
| I64Xor { .. }
| I64Eqz { .. }
| I64Eq { .. }
| I64Ne { .. }
| I64LtS { .. }
| I64LtU { .. }
| I64LeS { .. }
| I64LeU { .. }
| I64GtS { .. }
| I64GtU { .. }
| I64GeS { .. }
| I64GeU { .. }
| I64ExtendI32S { .. }
| I64ExtendI32U { .. }
| I32WrapI64 { .. }
| 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 { .. } => true,
}
}
fn resolve_toplevel_inits(regions: &mut [Region], instrs: &[ArmInstruction]) -> bool {
let mut st = WalkState::fresh();
let mut events: Vec<(i64, Sym)> = Vec::new();
let mut i = 0usize;
while i < instrs.len() {
if let Some(k) =
(0..regions.len()).find(|&k| !regions[k].has_parent && regions[k].head == i)
{
let off = match regions[k].proof {
Some((off, _, _)) => off,
None => return false,
};
match st.read_slot(off) {
Sym::Const(init) => regions[k].init = Some(init),
_ => return false,
}
st.kill_all_regs();
st.flags = None;
for instr in &instrs[regions[k].head..=regions[k].closer] {
match &instr.op {
ArmOp::Str { addr, .. } if addr.base == Reg::SP => {
st.taint_range(addr.offset as i64, 4);
}
ArmOp::Strb { addr, .. } if addr.base == Reg::SP => {
st.taint_range(addr.offset as i64, 1);
}
ArmOp::Strh { addr, .. } if addr.base == Reg::SP => {
st.taint_range(addr.offset as i64, 2);
}
_ => {}
}
}
i = regions[k].closer + 1;
continue;
}
let instr = &instrs[i];
match &instr.op {
ArmOp::Push { regs } => shift_slots(&mut st, 4 * regs.len() as i64),
ArmOp::Pop { regs } => shift_slots(&mut st, -4 * (regs.len() as i64)),
ArmOp::Sub { rd, rn, op2 } | ArmOp::Subs { rd, rn, op2 }
if *rd == Reg::SP && *rn == Reg::SP =>
{
match op2 {
Operand2::Imm(k) => shift_slots(&mut st, *k as i64),
_ => return false, }
}
ArmOp::Add { rd, rn, op2 } | ArmOp::Adds { rd, rn, op2 }
if *rd == Reg::SP && *rn == Reg::SP =>
{
match op2 {
Operand2::Imm(k) => shift_slots(&mut st, -(*k as i64)),
_ => return false,
}
}
op if may_move_sp(op) => return false, ArmOp::Bx { .. } => {
if (0..regions.len()).any(|k| !regions[k].has_parent && regions[k].head > i) {
return false;
}
break;
}
op => sym_step(op, &mut st, &mut events),
}
i += 1;
}
true
}
fn shift_slots(st: &mut WalkState, delta: i64) {
if delta == 0 {
return;
}
st.written = st
.written
.iter()
.map(|(&off, &v)| (off + delta, v))
.collect();
st.tainted = st.tainted.iter().map(|&off| off + delta).collect();
let below: Vec<i64> = st.written.keys().copied().filter(|&o| o < 0).collect();
for off in below {
st.written.remove(&off);
st.tainted.insert(off);
}
for r in st.regs.iter_mut() {
if matches!(r, Sym::Slot { .. } | Sym::Bool(_)) {
*r = Sym::Top;
}
}
st.flags = None;
}
fn masked_exit_index(init: i32, step: i64, p: &Pred) -> Option<(u64, bool)> {
let Some(ceiling) = p.masked_ceiling else {
return exit_index(init, step, p);
};
debug_assert!(ceiling >= 0);
if matches!(p.rel, Rel::Eq | Rel::Ne) && step.abs() != 1 {
return None;
}
let at = |rhs: i32| -> Option<u64> {
let pr = Pred {
rhs,
masked_ceiling: None,
..*p
};
exit_index(init, step, &pr).map(|(k, _)| k)
};
let (hi, lo) = (at(ceiling)?, at(0)?);
Some((hi.max(lo), true))
}
pub(crate) fn exit_index(init: i32, step: i64, p: &Pred) -> Option<(u64, bool)> {
let s = step;
debug_assert!(s != 0);
let signed = matches!(
p.rel,
Rel::LtS | Rel::LeS | Rel::GtS | Rel::GeS | Rel::Eq | Rel::Ne
);
let (v0, lo, hi): (i128, i128, i128) = if signed {
(init as i128, i32::MIN as i128, i32::MAX as i128)
} else {
(init as u32 as i128, 0, u32::MAX as i128)
};
let (rhs, add) = if signed {
(p.rhs as i128, p.add as i128)
} else {
(p.rhs as u32 as i128, p.add as i128)
};
let s = s as i128;
let k: i128 = match p.rel {
Rel::Eq | Rel::Ne => {
let target = rhs - add;
if p.rel == Rel::Ne {
let n = if v0 == target { 1 } else { 0 };
return finish(n, v0, s, add, lo, hi, false);
}
let d = target - v0;
if d % s != 0 || d / s < 0 {
return None; }
return finish(d / s, v0, s, add, lo, hi, true);
}
Rel::GeS | Rel::HsU => rhs - add, Rel::GtS | Rel::HiU => rhs - add + 1, Rel::LeS | Rel::LsU => rhs - add, Rel::LtS | Rel::LoU => rhs - add - 1, };
let upward_exit = matches!(p.rel, Rel::GeS | Rel::GtS | Rel::HsU | Rel::HiU);
if upward_exit {
if v0 >= k {
return finish(0, v0, s, add, lo, hi, false);
}
if s <= 0 {
return None; }
let n = ceil_div_pos(k - v0, s);
finish(n, v0, s, add, lo, hi, false)
} else {
if v0 <= k {
return finish(0, v0, s, add, lo, hi, false);
}
if s >= 0 {
return None;
}
let n = ceil_div_pos(v0 - k, -s);
finish(n, v0, s, add, lo, hi, false)
}
}
fn ceil_div_pos(num: i128, den: i128) -> i128 {
debug_assert!(num >= 0 && den > 0);
(num + den - 1) / den
}
fn finish(
n: i128,
v0: i128,
s: i128,
add: i128,
lo: i128,
hi: i128,
requires_hint: bool,
) -> Option<(u64, bool)> {
if n < 0 {
return None;
}
let vn = v0.checked_add(n.checked_mul(s)?)?;
for v in [v0, vn, v0 + add, vn + add] {
if v < lo || v > hi {
return None;
}
}
Some((u64::try_from(n).ok()?, requires_hint))
}
#[cfg(test)]
mod tests {
use super::*;
fn pred(rel: Rel, rhs: i32, add: i64) -> Pred {
Pred {
off: 0,
add,
rel,
rhs,
masked_ceiling: None,
}
}
#[test]
fn head_test_up_count() {
assert_eq!(exit_index(0, 1, &pred(Rel::GeS, 10, 0)), Some((10, false)));
assert_eq!(exit_index(3, 2, &pred(Rel::GeS, 10, 0)), Some((4, false)));
assert_eq!(exit_index(10, 1, &pred(Rel::GeS, 10, 0)), Some((0, false)));
assert_eq!(exit_index(0, 1, &pred(Rel::GeS, 10, 1)), Some((9, false)));
}
#[test]
fn down_count_and_unsigned() {
assert_eq!(exit_index(10, -1, &pred(Rel::LeS, 0, 0)), Some((10, false)));
assert_eq!(exit_index(0, 1, &pred(Rel::HsU, 8, 0)), Some((8, false)));
assert_eq!(exit_index(0, -1, &pred(Rel::GeS, 10, 0)), None);
}
#[test]
fn equality_exit_is_hint_gated() {
assert_eq!(exit_index(0, 1, &pred(Rel::Eq, 8, 0)), Some((8, true)));
assert_eq!(exit_index(0, 2, &pred(Rel::Eq, 9, 0)), None);
}
use synth_synthesis::{MemAddr, QReg, VfpReg};
fn true_but_absorbed_by_a_false_wildcard() -> Vec<ArmOp> {
vec![
ArmOp::I64Add {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R2,
rnhi: Reg::R3,
rmlo: Reg::R4,
rmhi: Reg::R5,
},
ArmOp::F64Add {
dd: VfpReg::D0,
dn: VfpReg::D1,
dm: VfpReg::D2,
},
ArmOp::MveAddF32 {
qd: QReg::Q0,
qn: QReg::Q1,
qm: QReg::Q2,
},
ArmOp::Select {
rd: Reg::R0,
rval1: Reg::R1,
rval2: Reg::R2,
rcond: Reg::R3,
},
ArmOp::B {
label: "L".to_string(),
},
]
}
#[test]
fn may_move_sp_true_answers_are_not_reproducible_by_a_false_wildcard() {
let ops = true_but_absorbed_by_a_false_wildcard();
assert!(
!ops.is_empty(),
"non-vacuity: with an empty list a re-added `_ => false` would be \
behaviourally identical and this tripwire could never fail"
);
for op in &ops {
assert!(
may_move_sp(op),
"{op:?}: must answer `true`; a `_ => false` wildcard would say `false`"
);
}
}
#[test]
fn may_move_sp_true_on_real_sp_definitions() {
assert!(may_move_sp(&ArmOp::Push {
regs: vec![Reg::R4]
}));
assert!(may_move_sp(&ArmOp::Pop {
regs: vec![Reg::R4]
}));
assert!(may_move_sp(&ArmOp::Add {
rd: Reg::SP,
rn: Reg::SP,
op2: Operand2::Imm(8)
}));
assert!(may_move_sp(&ArmOp::Umull {
rdlo: Reg::SP,
rdhi: Reg::R1,
rn: Reg::R2,
rm: Reg::R3
}));
assert!(may_move_sp(&ArmOp::I64Const {
rdlo: Reg::R0,
rdhi: Reg::SP,
value: 1
}));
assert!(may_move_sp(&ArmOp::I64Mul {
rd_lo: Reg::SP,
rd_hi: Reg::R1,
rn_lo: Reg::R2,
rn_hi: Reg::R3,
rm_lo: Reg::R4,
rm_hi: Reg::R5
}));
assert!(may_move_sp(&ArmOp::I64Clz {
rd: Reg::SP,
rnlo: Reg::R1,
rnhi: Reg::R2
}));
}
#[test]
fn may_move_sp_false_on_the_reachable_counted_loop_vocabulary() {
let must_be_false = vec![
ArmOp::Str {
rd: Reg::R0,
addr: MemAddr::imm(Reg::SP, 4),
},
ArmOp::I64Str {
rdlo: Reg::R0,
rdhi: Reg::R1,
addr: MemAddr::imm(Reg::SP, 8),
},
ArmOp::Cmp {
rn: Reg::R0,
op2: Operand2::Imm(10),
},
ArmOp::BOffset { offset: -6 },
ArmOp::BCondOffset {
cond: Condition::GE,
offset: 4,
},
ArmOp::Bx { rm: Reg::LR },
ArmOp::Bl {
label: "func_1".to_string(),
},
ArmOp::Label {
name: "L".to_string(),
},
ArmOp::Nop,
ArmOp::Udf { imm: 0 },
ArmOp::Add {
rd: Reg::R0,
rn: Reg::R1,
op2: Operand2::Imm(1),
},
ArmOp::I64Const {
rdlo: Reg::R0,
rdhi: Reg::R1,
value: 1,
},
ArmOp::I64Popcnt {
rd: Reg::R0,
rnlo: Reg::R1,
rnhi: Reg::R2,
},
ArmOp::I64Rotl {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R2,
rnhi: Reg::R3,
shift: Reg::R4,
},
];
for op in &must_be_false {
assert!(
!may_move_sp(op),
"{op:?}: must answer `false` — it is PRICED (so it really reaches \
this predicate) and declining it would decline proven loops"
);
}
}
#[test]
fn walk_state_never_tracks_a_slot_below_sp() {
let mut st = WalkState::fresh();
st.write_slot_word(-4, Sym::Const(7));
assert_eq!(st.read_slot(-4), Sym::Top, "a slot below SP must not track");
assert!(!st.written.contains_key(&-4));
assert_eq!(st.read_slot(-8), Sym::Top);
st.write_slot_word(8, Sym::Const(3));
assert_eq!(st.read_slot(8), Sym::Const(3));
shift_slots(&mut st, -16);
assert!(
!st.written.contains_key(&-8),
"a re-based slot that fell below SP must be dropped, not believed"
);
assert_eq!(st.read_slot(-8), Sym::Top);
}
#[test]
fn overflow_wraps_decline() {
assert_eq!(
exit_index(i32::MAX - 1, 2, &pred(Rel::GeS, i32::MAX, 0)),
None
);
assert_eq!(
exit_index(i32::MAX - 2, 1, &pred(Rel::GeS, i32::MAX, 0)),
Some((2, false))
);
}
}