#![cfg(feature = "arm")]
use crate::solver::{CheckOutcome, new_solver};
use crate::term::{BV, Bool};
use crate::validator_pattern::{
Flags, I64Pair, SolverResultKind, bool_to_i32, i64_lt_s, i64_lt_u, i64_rotl, i64_rotr, i64_shl,
i64_shr_s, i64_shr_u, k32, shift_amount64, sym32,
};
use std::collections::HashMap;
use synth_core::WasmOp;
use synth_synthesis::{ArmOp, Reg};
use thiserror::Error;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ExpansionWitness {
pub wasm_op_label: String,
pub instr_count: usize,
pub byte_len: usize,
pub solver_result: SolverResultKind,
}
#[derive(Debug, Error)]
pub enum ExpansionError {
#[error("expansion decode failed at byte offset {offset}: {reason}")]
Decode { offset: usize, reason: String },
#[error("expansion validator does not support: {0}")]
Unsupported(String),
#[error("counterexample for {wasm_op_label}: {description}")]
Counterexample {
wasm_op_label: String,
description: String,
},
#[error("solver returned unknown for {0}")]
SolverUnknown(String),
#[error("internal expansion-validator error: {0}")]
Internal(String),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ShiftKind {
Lsl,
Lsr,
Asr,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum DpKind {
And,
Orr,
Add,
Sub,
Sbcs,
}
#[derive(Debug, Clone)]
enum T2Op {
Nop,
It {
firstcond: u8,
mask: u8,
},
CmpReg {
rn: u8,
rm: u8,
},
CmpImm {
rn: u8,
imm: u32,
},
MovsImm {
rd: u8,
imm: u32,
},
MovImm {
rd: u8,
imm: u32,
},
MovReg {
rd: u8,
rm: u8,
},
AddReg16 {
rdn: u8,
rm: u8,
},
AddsReg16 {
rd: u8,
rn: u8,
rm: u8,
},
BCond {
cc: u8,
target: i64,
},
B {
target: i64,
},
Push {
list: u16,
},
Pop {
list: u16,
},
StrPreDec {
rt: u8,
},
AddSpImm {
imm: u32,
},
AndImm {
rd: u8,
rn: u8,
imm: u32,
},
AddImm {
rd: u8,
rn: u8,
imm: u32,
},
SubsImm {
rd: u8,
rn: u8,
imm: u32,
},
RsbImm {
rd: u8,
rn: u8,
imm: u32,
},
MovwImm {
rd: u8,
imm16: u32,
},
MovtImm {
rd: u8,
imm16: u32,
},
DpReg {
kind: DpKind,
rd: u8,
rn: u8,
rm: u8,
},
ShiftImm {
kind: ShiftKind,
rd: u8,
rm: u8,
imm: u32,
},
ShiftReg {
kind: ShiftKind,
rd: u8,
rn: u8,
rm: u8,
},
Mul {
rd: u8,
rn: u8,
rm: u8,
},
Mla {
rd: u8,
rn: u8,
rm: u8,
ra: u8,
},
Umull {
rdlo: u8,
rdhi: u8,
rn: u8,
rm: u8,
},
Clz {
rd: u8,
rm: u8,
},
Rbit {
rd: u8,
rm: u8,
},
}
#[derive(Debug, Clone)]
struct Decoded {
offset: usize,
len: usize,
op: T2Op,
}
fn derr<T>(offset: usize, reason: impl Into<String>) -> Result<T, ExpansionError> {
Err(ExpansionError::Decode {
offset,
reason: reason.into(),
})
}
fn thumb_expand_imm(offset: usize, i: u32, imm3: u32, imm8: u32) -> Result<u32, ExpansionError> {
let imm12 = (i << 11) | (imm3 << 8) | imm8;
if imm12 & 0xF00 == 0 {
Ok(imm8)
} else {
derr(
offset,
format!("modified immediate 0x{imm12:03x} outside the plain-imm8 subset"),
)
}
}
fn decode_thumb2(code: &[u8]) -> Result<Vec<Decoded>, ExpansionError> {
if !code.len().is_multiple_of(2) {
return derr(0, "odd byte length");
}
let mut out = Vec::new();
let mut o = 0usize;
while o < code.len() {
let hw1 = u16::from_le_bytes([code[o], code[o + 1]]);
let is32 = (hw1 & 0xE000) == 0xE000 && (hw1 & 0x1800) != 0;
if !is32 {
let op = decode16(o, hw1)?;
out.push(Decoded {
offset: o,
len: 2,
op,
});
o += 2;
} else {
if o + 4 > code.len() {
return derr(o, "truncated 32-bit instruction");
}
let hw2 = u16::from_le_bytes([code[o + 2], code[o + 3]]);
let op = decode32(o, hw1, hw2)?;
out.push(Decoded {
offset: o,
len: 4,
op,
});
o += 4;
}
}
Ok(out)
}
fn decode16(o: usize, hw: u16) -> Result<T2Op, ExpansionError> {
if hw == 0xBF00 {
return Ok(T2Op::Nop);
}
if hw & 0xFF00 == 0xBF00 {
return Ok(T2Op::It {
firstcond: ((hw >> 4) & 0xF) as u8,
mask: (hw & 0xF) as u8,
});
}
if hw & 0xFFC0 == 0x4280 {
return Ok(T2Op::CmpReg {
rn: (hw & 7) as u8,
rm: ((hw >> 3) & 7) as u8,
});
}
if hw & 0xFF00 == 0x4500 {
return Ok(T2Op::CmpReg {
rn: ((((hw >> 7) & 1) << 3) | (hw & 7)) as u8,
rm: ((hw >> 3) & 0xF) as u8,
});
}
if hw & 0xF800 == 0x2800 {
return Ok(T2Op::CmpImm {
rn: ((hw >> 8) & 7) as u8,
imm: (hw & 0xFF) as u32,
});
}
if hw & 0xF800 == 0x2000 {
return Ok(T2Op::MovsImm {
rd: ((hw >> 8) & 7) as u8,
imm: (hw & 0xFF) as u32,
});
}
if hw & 0xFF00 == 0x4600 {
return Ok(T2Op::MovReg {
rd: ((((hw >> 7) & 1) << 3) | (hw & 7)) as u8,
rm: ((hw >> 3) & 0xF) as u8,
});
}
if hw & 0xFF00 == 0x4400 {
return Ok(T2Op::AddReg16 {
rdn: ((((hw >> 7) & 1) << 3) | (hw & 7)) as u8,
rm: ((hw >> 3) & 0xF) as u8,
});
}
if hw & 0xFE00 == 0x1800 {
return Ok(T2Op::AddsReg16 {
rd: (hw & 7) as u8,
rn: ((hw >> 3) & 7) as u8,
rm: ((hw >> 6) & 7) as u8,
});
}
if hw & 0xF000 == 0xD000 {
let cc = ((hw >> 8) & 0xF) as u8;
if cc >= 0xE {
return derr(o, format!("conditional branch with cc={cc:#x}"));
}
let imm8 = (hw & 0xFF) as i64;
let simm = if imm8 >= 0x80 { imm8 - 0x100 } else { imm8 };
return Ok(T2Op::BCond {
cc,
target: o as i64 + 4 + 2 * simm,
});
}
if hw & 0xF800 == 0xE000 {
let imm11 = (hw & 0x7FF) as i64;
let simm = if imm11 >= 0x400 { imm11 - 0x800 } else { imm11 };
return Ok(T2Op::B {
target: o as i64 + 4 + 2 * simm,
});
}
if hw & 0xFE00 == 0xB400 {
if hw & 0x0100 != 0 {
return derr(o, "PUSH with LR outside the modeled subset");
}
return Ok(T2Op::Push { list: hw & 0xFF });
}
if hw & 0xFE00 == 0xBC00 {
if hw & 0x0100 != 0 {
return derr(o, "POP with PC outside the modeled subset");
}
return Ok(T2Op::Pop { list: hw & 0xFF });
}
if hw & 0xFF80 == 0xB000 {
return Ok(T2Op::AddSpImm {
imm: ((hw & 0x7F) as u32) * 4,
});
}
derr(
o,
format!("16-bit instruction {hw:#06x} outside the modeled subset"),
)
}
fn decode32(o: usize, hw1: u16, hw2: u16) -> Result<T2Op, ExpansionError> {
let hw1 = hw1 as u32;
let hw2 = hw2 as u32;
if hw1 == 0xF84D && hw2 & 0x0FFF == 0x0D04 {
return Ok(T2Op::StrPreDec {
rt: ((hw2 >> 12) & 0xF) as u8,
});
}
if hw1 & 0xFFF0 == 0xFAB0 && hw2 & 0xF0F0 == 0xF080 {
return Ok(T2Op::Clz {
rd: ((hw2 >> 8) & 0xF) as u8,
rm: (hw2 & 0xF) as u8,
});
}
if hw1 & 0xFFF0 == 0xFA90 && hw2 & 0xF0F0 == 0xF0A0 {
return Ok(T2Op::Rbit {
rd: ((hw2 >> 8) & 0xF) as u8,
rm: (hw2 & 0xF) as u8,
});
}
if hw1 & 0xFF80 == 0xFA00 && hw2 & 0xF0F0 == 0xF000 {
let kind = match (hw1 >> 5) & 3 {
0 => ShiftKind::Lsl,
1 => ShiftKind::Lsr,
2 => ShiftKind::Asr,
_ => return derr(o, "ROR.W (register) outside the modeled subset"),
};
return Ok(T2Op::ShiftReg {
kind,
rd: ((hw2 >> 8) & 0xF) as u8,
rn: (hw1 & 0xF) as u8,
rm: (hw2 & 0xF) as u8,
});
}
if hw1 & 0xFFF0 == 0xFB00 && hw2 & 0x00F0 == 0x0000 {
let ra = ((hw2 >> 12) & 0xF) as u8;
let rd = ((hw2 >> 8) & 0xF) as u8;
let rn = (hw1 & 0xF) as u8;
let rm = (hw2 & 0xF) as u8;
return Ok(if ra == 0xF {
T2Op::Mul { rd, rn, rm }
} else {
T2Op::Mla { rd, rn, rm, ra }
});
}
if hw1 & 0xFFF0 == 0xFBA0 && hw2 & 0x00F0 == 0x0000 {
return Ok(T2Op::Umull {
rdlo: ((hw2 >> 12) & 0xF) as u8,
rdhi: ((hw2 >> 8) & 0xF) as u8,
rn: (hw1 & 0xF) as u8,
rm: (hw2 & 0xF) as u8,
});
}
if hw1 & 0xFBF0 == 0xF240 || hw1 & 0xFBF0 == 0xF2C0 {
let i = (hw1 >> 10) & 1;
let imm4 = hw1 & 0xF;
let imm3 = (hw2 >> 12) & 7;
let rd = ((hw2 >> 8) & 0xF) as u8;
let imm8 = hw2 & 0xFF;
let imm16 = (imm4 << 12) | (i << 11) | (imm3 << 8) | imm8;
return Ok(if hw1 & 0xFBF0 == 0xF240 {
T2Op::MovwImm { rd, imm16 }
} else {
T2Op::MovtImm { rd, imm16 }
});
}
if hw1 & 0xFA00 == 0xF000 && hw2 & 0x8000 == 0 {
let i = (hw1 >> 10) & 1;
let op = (hw1 >> 5) & 0xF;
let s = (hw1 >> 4) & 1;
let rn = (hw1 & 0xF) as u8;
let imm3 = (hw2 >> 12) & 7;
let rd = ((hw2 >> 8) & 0xF) as u8;
let imm8 = hw2 & 0xFF;
let imm = thumb_expand_imm(o, i, imm3, imm8)?;
return match (op, s) {
(0b0000, 0) => Ok(T2Op::AndImm { rd, rn, imm }),
(0b0010, 0) if rn == 0xF => Ok(T2Op::MovImm { rd, imm }),
(0b1000, 0) => Ok(T2Op::AddImm { rd, rn, imm }),
(0b1101, 1) if rd == 0xF => Ok(T2Op::CmpImm { rn, imm }),
(0b1101, 1) => Ok(T2Op::SubsImm { rd, rn, imm }),
(0b1110, 0) => Ok(T2Op::RsbImm { rd, rn, imm }),
_ => derr(
o,
format!("modified-immediate DP op={op:#06b} S={s} outside the modeled subset"),
),
};
}
if hw1 & 0xFE00 == 0xEA00 {
let op = (hw1 >> 5) & 0xF;
let s = (hw1 >> 4) & 1;
let rn = (hw1 & 0xF) as u8;
let imm3 = (hw2 >> 12) & 7;
let rd = ((hw2 >> 8) & 0xF) as u8;
let imm2 = (hw2 >> 6) & 3;
let ty = (hw2 >> 4) & 3;
let rm = (hw2 & 0xF) as u8;
let imm5 = (imm3 << 2) | imm2;
if rn == 0xF && op == 0b0010 && s == 0 {
let kind = match ty {
0 if imm5 == 0 => return Ok(T2Op::MovReg { rd, rm }),
0 => ShiftKind::Lsl,
1 => ShiftKind::Lsr,
2 => ShiftKind::Asr,
_ => return derr(o, "ROR-immediate outside the modeled subset"),
};
if imm5 == 0 {
return derr(
o,
"shift-by-32 encoding (imm5=0) outside the modeled subset",
);
}
return Ok(T2Op::ShiftImm {
kind,
rd,
rm,
imm: imm5,
});
}
if imm5 != 0 || ty != 0 {
return derr(
o,
format!(
"shifted-register operand (type={ty}, imm5={imm5}) outside the modeled subset"
),
);
}
let kind = match (op, s) {
(0b0000, 0) => DpKind::And,
(0b0010, 0) => DpKind::Orr,
(0b1000, 0) => DpKind::Add,
(0b1101, 0) => DpKind::Sub,
(0b1011, 1) => DpKind::Sbcs,
_ => {
return derr(
o,
format!("register DP op={op:#06b} S={s} outside the modeled subset"),
);
}
};
return Ok(T2Op::DpReg { kind, rd, rn, rm });
}
derr(
o,
format!("32-bit instruction {hw1:#06x} {hw2:#06x} outside the modeled subset"),
)
}
fn bool_ite(c: &Bool, t: &Bool, e: &Bool) -> Bool {
Bool::or(&[&Bool::and(&[c, t]), &Bool::and(&[&c.not(), e])])
}
fn cc_holds(cc: u8, f: &Flags) -> Result<Bool, ExpansionError> {
Ok(match cc {
0x0 => f.z.clone(), 0x1 => f.z.not(), 0x2 => f.c.clone(), 0x3 => f.c.not(), 0x4 => f.n.clone(), 0x5 => f.n.not(), 0x6 => f.v.clone(), 0x7 => f.v.not(), 0x8 => Bool::and(&[&f.c, &f.z.not()]), 0x9 => Bool::or(&[&f.c.not(), &f.z]), 0xA => f.n.eq(&f.v), 0xB => f.n.eq(&f.v).not(), 0xC => Bool::and(&[&f.z.not(), &f.n.eq(&f.v)]), 0xD => Bool::or(&[&f.z, &f.n.eq(&f.v).not()]), _ => {
return Err(ExpansionError::Internal(format!(
"condition code {cc:#x} is not evaluatable"
)));
}
})
}
fn clz32(x: &BV) -> BV {
let mut r = k32(32);
for i in 0..32u32 {
let bit = x.extract(i, i).eq(BV::from_i64(1, 1));
r = bit.ite(k32((31 - i) as i64), &r);
}
r
}
fn ctz32(x: &BV) -> BV {
let mut r = k32(32);
for i in (0..32u32).rev() {
let bit = x.extract(i, i).eq(BV::from_i64(1, 1));
r = bit.ite(k32(i as i64), &r);
}
r
}
#[cfg(test)]
fn popcnt32(x: &BV) -> BV {
let mut acc = k32(0);
for i in 0..32u32 {
acc = acc.bvadd(x.extract(i, i).zero_ext(31));
}
acc
}
fn popcnt32_hakmem(x: &BV) -> BV {
let m1 = k32(0x5555_5555);
let m2 = k32(0x3333_3333);
let m4 = k32(0x0F0F_0F0F);
let x1 = x.bvsub(x.bvlshr(k32(1)).bvand(&m1));
let x2 = x1.bvand(&m2).bvadd(x1.bvlshr(k32(2)).bvand(&m2));
let x3 = x2.bvadd(x2.bvlshr(k32(4))).bvand(&m4);
x3.bvmul(k32(0x0101_0101)).bvlshr(k32(24))
}
fn i64_mul_schoolbook(a: &I64Pair, b: &I64Pair) -> I64Pair {
let full = a.lo.zero_ext(32).bvmul(b.lo.zero_ext(32));
let lo = full.extract(31, 0);
let hi = full
.extract(63, 32)
.bvadd(a.lo.bvmul(&b.hi))
.bvadd(a.hi.bvmul(&b.lo));
I64Pair { lo, hi }
}
fn rbit32(x: &BV) -> BV {
let mut r = x.extract(0, 0);
for i in 1..32u32 {
r = r.concat(x.extract(i, i));
}
r
}
struct MachineState {
regs: Vec<BV>,
flags: Flags,
mem: HashMap<i64, BV>,
sp_off: i64,
fresh: usize,
}
impl MachineState {
fn new() -> Self {
Self {
regs: (0..16).map(|i| sym32(&format!("init_r{i}"))).collect(),
flags: Flags::unconstrained(),
mem: HashMap::new(),
sp_off: 0,
fresh: 0,
}
}
fn fresh32(&mut self, label: &str) -> BV {
self.fresh += 1;
sym32(&format!("fresh_{label}_{}", self.fresh))
}
fn set_reg(&mut self, g: &Bool, rd: u8, v: BV) -> Result<(), ExpansionError> {
let rd = rd as usize;
if rd == 13 || rd == 15 {
return Err(ExpansionError::Internal(
"direct SP/PC register write outside the modeled subset".into(),
));
}
self.regs[rd] = g.ite(&v, &self.regs[rd]);
Ok(())
}
fn get_reg(&self, r: u8) -> Result<BV, ExpansionError> {
let r = r as usize;
if r == 13 || r == 15 {
return Err(ExpansionError::Internal(
"direct SP/PC register read outside the modeled subset".into(),
));
}
Ok(self.regs[r].clone())
}
fn set_flags(&mut self, g: &Bool, new: Flags) {
self.flags = Flags {
n: bool_ite(g, &new.n, &self.flags.n),
z: bool_ite(g, &new.z, &self.flags.z),
c: bool_ite(g, &new.c, &self.flags.c),
v: bool_ite(g, &new.v, &self.flags.v),
};
}
fn mem_read(&mut self, addr: i64) -> BV {
if let Some(v) = self.mem.get(&addr) {
return v.clone();
}
let v = self.fresh32("stack");
self.mem.insert(addr, v.clone());
v
}
fn mem_write(&mut self, g: &Bool, addr: i64, v: BV) {
let old = self.mem_read(addr);
self.mem.insert(addr, g.ite(&v, &old));
}
}
fn flags_from_adds(rn: &BV, rm: &BV, result: &BV) -> Flags {
let n = result.bvslt(k32(0));
let z = result.eq(k32(0));
let c = result.bvult(rn); let rn_neg = rn.bvslt(k32(0));
let rm_neg = rm.bvslt(k32(0));
let r_neg = result.bvslt(k32(0));
let v = Bool::and(&[&rn_neg.eq(&rm_neg), &r_neg.eq(&rn_neg).not()]);
Flags { n, z, c, v }
}
fn exec_sbcs(rn: &BV, rm: &BV, c_in: &Bool) -> (BV, Flags) {
let one33 = BV::from_i64(1, 33);
let zero33 = BV::from_i64(0, 33);
let borrow = c_in.ite(&zero33, &one33);
let u = rn.zero_ext(1).bvsub(rm.zero_ext(1)).bvsub(&borrow);
let result = u.extract(31, 0);
let c = u.extract(32, 32).eq(BV::from_i64(0, 1)); let s = rn.sign_ext(1).bvsub(rm.sign_ext(1)).bvsub(&borrow);
let v = s.extract(32, 32).eq(s.extract(31, 31)).not();
let n = result.bvslt(k32(0));
let z = result.eq(k32(0));
(result, Flags { n, z, c, v })
}
fn execute(instrs: &[Decoded], state: &mut MachineState) -> Result<(), ExpansionError> {
let total_len = instrs.last().map(|d| d.offset + d.len).unwrap_or(0);
let mut incoming: HashMap<usize, Bool> = HashMap::new();
incoming.insert(0, Bool::from_bool(true));
let mut pending_joins: Vec<usize> = Vec::new();
let mut it_queue: Vec<u8> = Vec::new();
let merge = |incoming: &mut HashMap<usize, Bool>, at: usize, g: Bool| {
let cur = incoming
.get(&at)
.cloned()
.unwrap_or_else(|| Bool::from_bool(false));
incoming.insert(at, Bool::or(&[&cur, &g]));
};
for d in instrs {
let o = d.offset;
pending_joins.retain(|&j| j > o);
let g = incoming
.get(&o)
.cloned()
.unwrap_or_else(|| Bool::from_bool(false));
let in_it = !it_queue.is_empty();
let eff = if in_it {
let cc = it_queue.remove(0);
let c = cc_holds(cc, &state.flags)?;
Bool::and(&[&g, &c])
} else {
g.clone()
};
let next = o + d.len;
let in_cond_region = !pending_joins.is_empty();
let mut fallthrough = true;
match &d.op {
T2Op::Nop => {}
T2Op::It { firstcond, mask } => {
if in_it {
return Err(ExpansionError::Internal("nested IT block".into()));
}
if *mask == 0 {
return Err(ExpansionError::Internal("IT with empty mask".into()));
}
let count = 4 - mask.trailing_zeros() as usize;
let base = firstcond & 0xE;
it_queue.push(*firstcond);
for j in 1..count {
let bit = (mask >> (3 - (j - 1))) & 1;
it_queue.push(base | bit);
}
}
T2Op::CmpReg { rn, rm } => {
let a = state.get_reg(*rn)?;
let b = state.get_reg(*rm)?;
let f = Flags::from_cmp(&a, &b);
state.set_flags(&eff, f);
}
T2Op::CmpImm { rn, imm } => {
let a = state.get_reg(*rn)?;
let f = Flags::from_cmp(&a, &k32(*imm as i64));
state.set_flags(&eff, f);
}
T2Op::MovsImm { rd, imm } => {
let v = k32(*imm as i64);
if !in_it {
let f = Flags {
n: v.bvslt(k32(0)),
z: v.eq(k32(0)),
c: state.flags.c.clone(),
v: state.flags.v.clone(),
};
state.set_flags(&eff, f);
}
state.set_reg(&eff, *rd, v)?;
}
T2Op::MovImm { rd, imm } => {
state.set_reg(&eff, *rd, k32(*imm as i64))?;
}
T2Op::MovReg { rd, rm } => {
let v = state.get_reg(*rm)?;
state.set_reg(&eff, *rd, v)?;
}
T2Op::AddReg16 { rdn, rm } => {
let v = state.get_reg(*rdn)?.bvadd(&state.get_reg(*rm)?);
state.set_reg(&eff, *rdn, v)?;
}
T2Op::AddsReg16 { rd, rn, rm } => {
let a = state.get_reg(*rn)?;
let b = state.get_reg(*rm)?;
let v = a.bvadd(&b);
if !in_it {
let f = flags_from_adds(&a, &b, &v);
state.set_flags(&eff, f);
}
state.set_reg(&eff, *rd, v)?;
}
T2Op::BCond { cc, target } => {
if in_it {
return Err(ExpansionError::Internal("branch inside IT block".into()));
}
if *target <= o as i64 {
return Err(ExpansionError::Decode {
offset: o,
reason: "backward branch (loop) — held out, needs loop unrolling"
.to_string(),
});
}
let t = *target as usize;
if t > total_len {
return derr(o, "branch target beyond sequence end");
}
let c = cc_holds(*cc, &state.flags)?;
merge(&mut incoming, t, Bool::and(&[&eff, &c]));
merge(&mut incoming, next, Bool::and(&[&eff, &c.not()]));
pending_joins.push(t);
fallthrough = false; }
T2Op::B { target } => {
if in_it {
return Err(ExpansionError::Internal("branch inside IT block".into()));
}
if *target <= o as i64 {
return Err(ExpansionError::Decode {
offset: o,
reason: "backward branch (loop) — held out, needs loop unrolling"
.to_string(),
});
}
let t = *target as usize;
if t > total_len {
return derr(o, "branch target beyond sequence end");
}
merge(&mut incoming, t, eff.clone());
pending_joins.push(t);
fallthrough = false;
}
T2Op::Push { list } => {
if in_cond_region {
return Err(ExpansionError::Internal(
"PUSH inside a conditional region — SP discipline unmodeled".into(),
));
}
let n = list.count_ones() as i64;
state.sp_off -= 4 * n;
let mut slot = 0i64;
for r in 0..8u8 {
if list & (1 << r) != 0 {
let v = state.get_reg(r)?;
let addr = state.sp_off + 4 * slot;
state.mem_write(&eff, addr, v);
slot += 1;
}
}
}
T2Op::Pop { list } => {
if in_cond_region {
return Err(ExpansionError::Internal(
"POP inside a conditional region — SP discipline unmodeled".into(),
));
}
let mut slot = 0i64;
for r in 0..8u8 {
if list & (1 << r) != 0 {
let addr = state.sp_off + 4 * slot;
let v = state.mem_read(addr);
state.set_reg(&eff, r, v)?;
slot += 1;
}
}
state.sp_off += 4 * slot;
}
T2Op::StrPreDec { rt } => {
if in_cond_region {
return Err(ExpansionError::Internal(
"STR [SP,#-4]! inside a conditional region — SP discipline unmodeled"
.into(),
));
}
state.sp_off -= 4;
let v = state.get_reg(*rt)?;
let addr = state.sp_off;
state.mem_write(&eff, addr, v);
}
T2Op::AddSpImm { imm } => {
if in_cond_region {
return Err(ExpansionError::Internal(
"ADD SP inside a conditional region — SP discipline unmodeled".into(),
));
}
state.sp_off += *imm as i64;
}
T2Op::AndImm { rd, rn, imm } => {
let v = state.get_reg(*rn)?.bvand(k32(*imm as i64));
state.set_reg(&eff, *rd, v)?;
}
T2Op::AddImm { rd, rn, imm } => {
let v = state.get_reg(*rn)?.bvadd(k32(*imm as i64));
state.set_reg(&eff, *rd, v)?;
}
T2Op::SubsImm { rd, rn, imm } => {
let a = state.get_reg(*rn)?;
let b = k32(*imm as i64);
let f = Flags::from_cmp(&a, &b);
state.set_flags(&eff, f);
state.set_reg(&eff, *rd, a.bvsub(&b))?;
}
T2Op::RsbImm { rd, rn, imm } => {
let v = k32(*imm as i64).bvsub(&state.get_reg(*rn)?);
state.set_reg(&eff, *rd, v)?;
}
T2Op::MovwImm { rd, imm16 } => {
state.set_reg(&eff, *rd, k32(*imm16 as i64))?;
}
T2Op::MovtImm { rd, imm16 } => {
let low = state.get_reg(*rd)?.bvand(k32(0xFFFF));
let v = low.bvor(k32(((*imm16 as i64) << 16) & 0xFFFF_FFFF));
state.set_reg(&eff, *rd, v)?;
}
T2Op::DpReg { kind, rd, rn, rm } => {
let a = state.get_reg(*rn)?;
let b = state.get_reg(*rm)?;
match kind {
DpKind::And => state.set_reg(&eff, *rd, a.bvand(&b))?,
DpKind::Orr => state.set_reg(&eff, *rd, a.bvor(&b))?,
DpKind::Add => state.set_reg(&eff, *rd, a.bvadd(&b))?,
DpKind::Sub => state.set_reg(&eff, *rd, a.bvsub(&b))?,
DpKind::Sbcs => {
let (v, f) = exec_sbcs(&a, &b, &state.flags.c);
state.set_flags(&eff, f);
state.set_reg(&eff, *rd, v)?;
}
}
}
T2Op::ShiftImm { kind, rd, rm, imm } => {
let a = state.get_reg(*rm)?;
let s = k32(*imm as i64);
let v = match kind {
ShiftKind::Lsl => a.bvshl(&s),
ShiftKind::Lsr => a.bvlshr(&s),
ShiftKind::Asr => a.bvashr(&s),
};
state.set_reg(&eff, *rd, v)?;
}
T2Op::ShiftReg { kind, rd, rn, rm } => {
let a = state.get_reg(*rn)?;
let s = state.get_reg(*rm)?.bvand(k32(0xFF));
let v = match kind {
ShiftKind::Lsl => a.bvshl(&s),
ShiftKind::Lsr => a.bvlshr(&s),
ShiftKind::Asr => a.bvashr(&s),
};
state.set_reg(&eff, *rd, v)?;
}
T2Op::Mul { rd, rn, rm } => {
let v = state.get_reg(*rn)?.bvmul(&state.get_reg(*rm)?);
state.set_reg(&eff, *rd, v)?;
}
T2Op::Mla { rd, rn, rm, ra } => {
let v = state
.get_reg(*ra)?
.bvadd(state.get_reg(*rn)?.bvmul(&state.get_reg(*rm)?));
state.set_reg(&eff, *rd, v)?;
}
T2Op::Umull { rdlo, rdhi, rn, rm } => {
let full = state
.get_reg(*rn)?
.zero_ext(32)
.bvmul(state.get_reg(*rm)?.zero_ext(32));
let lo = full.extract(31, 0);
let hi = full.extract(63, 32);
state.set_reg(&eff, *rdlo, lo)?;
state.set_reg(&eff, *rdhi, hi)?;
}
T2Op::Clz { rd, rm } => {
let v = clz32(&state.get_reg(*rm)?);
state.set_reg(&eff, *rd, v)?;
}
T2Op::Rbit { rd, rm } => {
let v = rbit32(&state.get_reg(*rm)?);
state.set_reg(&eff, *rd, v)?;
}
}
if fallthrough {
merge(&mut incoming, next, g);
}
}
if !it_queue.is_empty() {
return Err(ExpansionError::Internal(
"IT block extends past the end of the sequence".into(),
));
}
Ok(())
}
enum RefResult {
Word(BV),
Pair(I64Pair),
}
fn wasm_reference(op: &WasmOp, a: &I64Pair, b: &I64Pair) -> Option<RefResult> {
use WasmOp::*;
let word = |bv: BV| Some(RefResult::Word(bv));
let pair = |p: I64Pair| Some(RefResult::Pair(p));
match op {
I64Mul => pair(i64_mul_schoolbook(a, b)),
I64Shl => pair(i64_shl(a, &shift_amount64(b))),
I64ShrU => pair(i64_shr_u(a, &shift_amount64(b))),
I64ShrS => pair(i64_shr_s(a, &shift_amount64(b))),
I64Rotl => pair(i64_rotl(a, &shift_amount64(b))),
I64Rotr => pair(i64_rotr(a, &shift_amount64(b))),
I64Eq => word(bool_to_i32(&a.eq_pair(b))),
I64Ne => word(bool_to_i32(&a.eq_pair(b).not())),
I64LtU => word(bool_to_i32(&i64_lt_u(a, b))),
I64LtS => word(bool_to_i32(&i64_lt_s(a, b))),
I64GtU => word(bool_to_i32(&i64_lt_u(b, a))),
I64GtS => word(bool_to_i32(&i64_lt_s(b, a))),
I64LeU => word(bool_to_i32(&i64_lt_u(b, a).not())),
I64LeS => word(bool_to_i32(&i64_lt_s(b, a).not())),
I64GeU => word(bool_to_i32(&i64_lt_u(a, b).not())),
I64GeS => word(bool_to_i32(&i64_lt_s(a, b).not())),
I64Eqz => word(bool_to_i32(&Bool::and(&[
&a.lo.eq(k32(0)),
&a.hi.eq(k32(0)),
]))),
I64Clz => pair(I64Pair {
lo: a
.hi
.eq(k32(0))
.ite(clz32(&a.lo).bvadd(k32(32)), clz32(&a.hi)),
hi: k32(0),
}),
I64Ctz => pair(I64Pair {
lo: a
.lo
.eq(k32(0))
.ite(ctz32(&a.hi).bvadd(k32(32)), ctz32(&a.lo)),
hi: k32(0),
}),
I64Popcnt => pair(I64Pair {
lo: popcnt32_hakmem(&a.lo).bvadd(popcnt32_hakmem(&a.hi)),
hi: k32(0),
}),
_ => None,
}
}
struct Contract {
a: (u8, u8),
b: Option<(u8, Option<u8>)>,
result: ContractResult,
}
enum ContractResult {
Word(u8),
Pair(u8, u8),
}
fn ri(r: &Reg) -> u8 {
crate::validator_pattern::reg_index(r) as u8
}
fn contract_of(pseudo: &ArmOp) -> Option<Contract> {
match pseudo {
ArmOp::I64Mul {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
} => Some(Contract {
a: (ri(rn_lo), ri(rn_hi)),
b: Some((ri(rm_lo), Some(ri(rm_hi)))),
result: ContractResult::Pair(ri(rd_lo), ri(rd_hi)),
}),
ArmOp::I64Shl {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
}
| ArmOp::I64ShrU {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
}
| ArmOp::I64ShrS {
rd_lo,
rd_hi,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
} => Some(Contract {
a: (ri(rn_lo), ri(rn_hi)),
b: Some((ri(rm_lo), Some(ri(rm_hi)))),
result: ContractResult::Pair(ri(rd_lo), ri(rd_hi)),
}),
ArmOp::I64Rotl {
rdlo,
rdhi,
rnlo,
rnhi,
shift,
}
| ArmOp::I64Rotr {
rdlo,
rdhi,
rnlo,
rnhi,
shift,
} => Some(Contract {
a: (ri(rnlo), ri(rnhi)),
b: Some((ri(shift), None)),
result: ContractResult::Pair(ri(rdlo), ri(rdhi)),
}),
ArmOp::I64SetCond {
rd,
rn_lo,
rn_hi,
rm_lo,
rm_hi,
..
} => Some(Contract {
a: (ri(rn_lo), ri(rn_hi)),
b: Some((ri(rm_lo), Some(ri(rm_hi)))),
result: ContractResult::Word(ri(rd)),
}),
ArmOp::I64SetCondZ { rd, rn_lo, rn_hi } => Some(Contract {
a: (ri(rn_lo), ri(rn_hi)),
b: None,
result: ContractResult::Word(ri(rd)),
}),
ArmOp::I64Clz { rd, rnlo, rnhi }
| ArmOp::I64Ctz { rd, rnlo, rnhi }
| ArmOp::I64Popcnt { rd, rnlo, rnhi } => Some(Contract {
a: (ri(rnlo), ri(rnhi)),
b: None,
result: ContractResult::Pair(ri(rd), ri(rnhi)),
}),
_ => None,
}
}
pub fn validate_expansion(
wasm: &WasmOp,
pseudo: &ArmOp,
code: &[u8],
) -> Result<ExpansionWitness, ExpansionError> {
let label = format!("{wasm:?}");
let contract = contract_of(pseudo)
.ok_or_else(|| ExpansionError::Unsupported(format!("pseudo-op {pseudo:?}")))?;
let instrs = decode_thumb2(code)?;
let a = I64Pair {
lo: sym32("a_lo"),
hi: sym32("a_hi"),
};
let b = I64Pair {
lo: sym32("b_lo"),
hi: sym32("b_hi"),
};
let reference = wasm_reference(wasm, &a, &b)
.ok_or_else(|| ExpansionError::Unsupported(format!("wasm op {wasm:?}")))?;
let mut state = MachineState::new();
let g_true = Bool::from_bool(true);
state.set_reg(&g_true, contract.a.0, a.lo.clone())?;
state.set_reg(&g_true, contract.a.1, a.hi.clone())?;
if let Some((blo, bhi)) = &contract.b {
state.set_reg(&g_true, *blo, b.lo.clone())?;
if let Some(bhi) = bhi {
state.set_reg(&g_true, *bhi, b.hi.clone())?;
}
}
execute(&instrs, &mut state)?;
let differ = match (&reference, &contract.result) {
(RefResult::Word(w), ContractResult::Word(rd)) => w.eq(&state.get_reg(*rd)?).not(),
(RefResult::Pair(w), ContractResult::Pair(rd_lo, rd_hi)) => {
let got = I64Pair {
lo: state.get_reg(*rd_lo)?,
hi: state.get_reg(*rd_hi)?,
};
w.eq_pair(&got).not()
}
_ => {
return Err(ExpansionError::Internal(format!(
"result-shape mismatch for {label}"
)));
}
};
let mut solver = new_solver();
solver.assert(&differ);
match solver.check() {
CheckOutcome::Unsat => Ok(ExpansionWitness {
wasm_op_label: label,
instr_count: instrs.len(),
byte_len: code.len(),
solver_result: SolverResultKind::Unsat,
}),
CheckOutcome::Sat => {
let mut parts = Vec::new();
for (name, bv) in [
("a_lo", &a.lo),
("a_hi", &a.hi),
("b_lo", &b.lo),
("b_hi", &b.hi),
] {
if let Some(v) = solver.value(bv) {
parts.push(format!("{name}={v:#x}"));
}
}
let description = if parts.is_empty() {
"no model available".to_string()
} else {
parts.join(", ")
};
Err(ExpansionError::Counterexample {
wasm_op_label: label,
description,
})
}
CheckOutcome::Unknown(reason) => {
Err(ExpansionError::SolverUnknown(format!("{label}: {reason}")))
}
}
}
pub fn covered_i64_pseudo_selections() -> Vec<(WasmOp, ArmOp)> {
use synth_synthesis::rules::Condition;
let mut v: Vec<(WasmOp, ArmOp)> = Vec::new();
v.push((
WasmOp::I64Mul,
ArmOp::I64Mul {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
},
));
let setcond = |cond: Condition| ArmOp::I64SetCond {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
cond,
};
v.push((WasmOp::I64Eq, setcond(Condition::EQ)));
v.push((WasmOp::I64Ne, setcond(Condition::NE)));
v.push((WasmOp::I64LtS, setcond(Condition::LT)));
v.push((WasmOp::I64LeS, setcond(Condition::LE)));
v.push((WasmOp::I64GtS, setcond(Condition::GT)));
v.push((WasmOp::I64GeS, setcond(Condition::GE)));
v.push((WasmOp::I64LtU, setcond(Condition::LO)));
v.push((WasmOp::I64LeU, setcond(Condition::LS)));
v.push((WasmOp::I64GtU, setcond(Condition::HI)));
v.push((WasmOp::I64GeU, setcond(Condition::HS)));
v.push((
WasmOp::I64Eqz,
ArmOp::I64SetCondZ {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
},
));
for (op, mk) in [
(
WasmOp::I64Shl,
(|| ArmOp::I64Shl {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}) as fn() -> ArmOp,
),
(WasmOp::I64ShrU, || ArmOp::I64ShrU {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}),
(WasmOp::I64ShrS, || ArmOp::I64ShrS {
rd_lo: Reg::R0,
rd_hi: Reg::R1,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
rm_lo: Reg::R2,
rm_hi: Reg::R3,
}),
] {
v.push((op, mk()));
}
v.push((
WasmOp::I64Rotl,
ArmOp::I64Rotl {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R0,
rnhi: Reg::R1,
shift: Reg::R2,
},
));
v.push((
WasmOp::I64Rotr,
ArmOp::I64Rotr {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R0,
rnhi: Reg::R1,
shift: Reg::R2,
},
));
for (op, mk) in [
(
WasmOp::I64Clz,
(|| ArmOp::I64Clz {
rd: Reg::R0,
rnlo: Reg::R0,
rnhi: Reg::R1,
}) as fn() -> ArmOp,
),
(WasmOp::I64Ctz, || ArmOp::I64Ctz {
rd: Reg::R0,
rnlo: Reg::R0,
rnhi: Reg::R1,
}),
(WasmOp::I64Popcnt, || ArmOp::I64Popcnt {
rd: Reg::R0,
rnlo: Reg::R0,
rnhi: Reg::R1,
}),
] {
v.push((op, mk()));
}
v
}
#[cfg(test)]
mod tests {
use super::*;
use crate::with_verification_context;
fn halfwords(hws: &[u16]) -> Vec<u8> {
let mut b = Vec::new();
for hw in hws {
b.extend_from_slice(&hw.to_le_bytes());
}
b
}
#[test]
fn bitcount_references_sanity() {
with_verification_context(|| {
for (v, clz, ctz, pop) in [
(0u32, 32i64, 32i64, 0i64),
(1, 31, 0, 1),
(8, 28, 3, 1),
(0x8000_0000, 0, 31, 1),
(0xF0F, 20, 0, 8),
(0xFFFF_FFFF, 0, 0, 32),
] {
let x = k32(v as i64);
let mut s = new_solver();
let bad = Bool::or(&[
&clz32(&x).eq(k32(clz)).not(),
&ctz32(&x).eq(k32(ctz)).not(),
&popcnt32(&x).eq(k32(pop)).not(),
]);
s.assert(&bad);
assert_eq!(s.check(), CheckOutcome::Unsat, "bitcount refs for {v:#x}");
}
});
}
#[test]
fn schoolbook_mul_matches_u64_mul() {
with_verification_context(|| {
let grid: &[u64] = &[
0,
1,
2,
3,
0x7FFF_FFFF,
0x8000_0000,
0xFFFF_FFFF,
0x1_0000_0000,
0xDEAD_BEEF_CAFE_F00D,
0x8000_0000_0000_0000,
0xFFFF_FFFF_FFFF_FFFF,
0x0000_0001_0000_0001,
];
for &x in grid {
for &y in grid {
let truth = x.wrapping_mul(y);
let a = I64Pair {
lo: k32((x & 0xFFFF_FFFF) as i64),
hi: k32((x >> 32) as i64),
};
let b = I64Pair {
lo: k32((y & 0xFFFF_FFFF) as i64),
hi: k32((y >> 32) as i64),
};
let got = i64_mul_schoolbook(&a, &b);
let want = I64Pair {
lo: k32((truth & 0xFFFF_FFFF) as i64),
hi: k32((truth >> 32) as i64),
};
let mut s = new_solver();
s.assert(&got.eq_pair(&want).not());
assert_eq!(
s.check(),
CheckOutcome::Unsat,
"schoolbook mul wrong for {x:#x} * {y:#x}"
);
}
}
});
}
#[test]
fn rbit_clz_is_ctz() {
with_verification_context(|| {
let x = sym32("x");
let mut s = new_solver();
s.assert(&clz32(&rbit32(&x)).eq(ctz32(&x)).not());
assert_eq!(s.check(), CheckOutcome::Unsat);
});
}
#[test]
fn popcnt_hakmem_formula_is_popcnt() {
with_verification_context(|| {
let w = sym32("w");
let mut s = new_solver();
s.assert(&popcnt32_hakmem(&w).eq(popcnt32(&w)).not());
assert_eq!(
s.check(),
CheckOutcome::Unsat,
"HAKMEM fold must equal bit-sum popcount for all inputs"
);
});
}
#[test]
fn decoder_rejects_unknown_loudly() {
let r = decode_thumb2(&halfwords(&[0xDE00]));
assert!(matches!(r, Err(ExpansionError::Decode { .. })), "{r:?}");
let r = decode_thumb2(&halfwords(&[0xF7F0, 0xA000]));
assert!(matches!(r, Err(ExpansionError::Decode { .. })), "{r:?}");
}
#[test]
fn backward_branch_is_held_out_loudly() {
with_verification_context(|| {
let code = halfwords(&[0x2800, 0xD1FC]);
let pseudo = ArmOp::I64SetCondZ {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
};
let r = validate_expansion(&WasmOp::I64Eqz, &pseudo, &code);
match r {
Err(ExpansionError::Decode { reason, .. }) => {
assert!(reason.contains("backward branch"), "{reason}");
}
other => panic!("expected backward-branch decode error, got {other:?}"),
}
});
}
#[test]
fn div_rem_pseudo_ops_are_unsupported() {
let pseudo = ArmOp::I64DivU {
rdlo: Reg::R0,
rdhi: Reg::R1,
rnlo: Reg::R0,
rnhi: Reg::R1,
rmlo: Reg::R2,
rmhi: Reg::R3,
elide_zero_guard: false,
};
let r = validate_expansion(&WasmOp::I64DivU, &pseudo, &[]);
assert!(matches!(r, Err(ExpansionError::Unsupported(_))), "{r:?}");
}
#[test]
fn hand_built_eqz_sequence_certifies() {
with_verification_context(|| {
let code = halfwords(&[
0xEA40, 0x0001, 0x2800, 0xBF0C, 0x2001, 0x2000, ]);
let pseudo = ArmOp::I64SetCondZ {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
};
let w = validate_expansion(&WasmOp::I64Eqz, &pseudo, &code)
.expect("eqz sequence must certify");
assert_eq!(w.solver_result, SolverResultKind::Unsat);
assert_eq!(w.instr_count, 5);
});
}
#[test]
fn hand_built_eqz_wrong_polarity_rejected() {
with_verification_context(|| {
let code = halfwords(&[
0xEA40, 0x0001, 0x2800, 0xBF14, 0x2001, 0x2000, ]);
let pseudo = ArmOp::I64SetCondZ {
rd: Reg::R0,
rn_lo: Reg::R0,
rn_hi: Reg::R1,
};
match validate_expansion(&WasmOp::I64Eqz, &pseudo, &code) {
Err(ExpansionError::Counterexample { .. }) => {}
other => panic!("expected Counterexample, got {other:?}"),
}
});
}
}