use anyhow::{Context, Result};
use std::collections::BTreeMap;
use std::path::Path;
use std::process::Command;
const RESERVED: [&str; 3] = ["r9", "r10", "r11"];
#[derive(Clone, Copy, Debug, PartialEq)]
enum Class {
Data,
ReadOnly,
DestFirst,
DestFirstTwo,
MrcThird,
MrrcThirdFourth,
LoadMultiple,
Vmov,
}
const MNEMONICS: &[(&str, Class)] = &[
(".word", Class::Data),
(".short", Class::Data),
(".byte", Class::Data),
(".long", Class::Data),
("ldmia", Class::LoadMultiple),
("ldmdb", Class::LoadMultiple),
("ldmea", Class::LoadMultiple),
("ldmfd", Class::LoadMultiple),
("ldm", Class::LoadMultiple),
("pop", Class::LoadMultiple),
("ldrexd", Class::DestFirstTwo),
("ldrex", Class::DestFirst),
("ldrd", Class::DestFirstTwo),
("ldr", Class::DestFirst), ("strex", Class::DestFirst), ("str", Class::ReadOnly), ("push", Class::ReadOnly),
("stm", Class::ReadOnly), ("vmrs", Class::DestFirst), ("vmsr", Class::ReadOnly),
("vmov", Class::Vmov),
("vldm", Class::ReadOnly), ("vstm", Class::ReadOnly),
("vpop", Class::ReadOnly),
("vpush", Class::ReadOnly),
("vldr", Class::ReadOnly), ("vstr", Class::ReadOnly),
("vld", Class::ReadOnly), ("vst", Class::ReadOnly), ("cmp", Class::ReadOnly),
("cmn", Class::ReadOnly),
("tst", Class::ReadOnly),
("teq", Class::ReadOnly),
("mrrc2", Class::MrrcThirdFourth),
("mrrc", Class::MrrcThirdFourth),
("mrc2", Class::MrcThird),
("mrc", Class::MrcThird),
("mcrr", Class::ReadOnly),
("mcr", Class::ReadOnly),
("cdp", Class::ReadOnly),
("ldc", Class::ReadOnly), ("stc", Class::ReadOnly),
("mrs", Class::DestFirst),
("msr", Class::ReadOnly),
("bic", Class::DestFirst), ("bfi", Class::DestFirst),
("bfc", Class::DestFirst),
("bkpt", Class::ReadOnly),
("bxns", Class::ReadOnly),
("blxns", Class::ReadOnly),
("blx", Class::ReadOnly),
("bl", Class::ReadOnly),
("bx", Class::ReadOnly),
("b", Class::ReadOnly), ("cbz", Class::ReadOnly),
("cbnz", Class::ReadOnly),
("tbb", Class::ReadOnly),
("tbh", Class::ReadOnly),
("it", Class::ReadOnly), ("nop", Class::ReadOnly),
("sev", Class::ReadOnly),
("wfe", Class::ReadOnly),
("wfi", Class::ReadOnly),
("yield", Class::ReadOnly),
("dbg", Class::ReadOnly),
("dmb", Class::ReadOnly),
("dsb", Class::ReadOnly),
("isb", Class::ReadOnly),
("csdb", Class::ReadOnly),
("pldw", Class::ReadOnly),
("pld", Class::ReadOnly),
("pli", Class::ReadOnly),
("svc", Class::ReadOnly),
("udf", Class::ReadOnly),
("hlt", Class::ReadOnly),
("cps", Class::ReadOnly), ("clrex", Class::ReadOnly),
("setend", Class::ReadOnly),
("sg", Class::ReadOnly), ("tt", Class::DestFirst), ("umull", Class::DestFirstTwo),
("umlal", Class::DestFirstTwo),
("umaal", Class::DestFirstTwo),
("smull", Class::DestFirstTwo),
("smlald", Class::DestFirstTwo),
("smlal", Class::DestFirstTwo), ("smlsld", Class::DestFirstTwo),
("adc", Class::DestFirst),
("addw", Class::DestFirst),
("add", Class::DestFirst),
("adr", Class::DestFirst),
("and", Class::DestFirst),
("asr", Class::DestFirst),
("clz", Class::DestFirst),
("crc32", Class::DestFirst),
("eor", Class::DestFirst),
("lsl", Class::DestFirst),
("lsr", Class::DestFirst),
("mla", Class::DestFirst),
("mls", Class::DestFirst),
("movt", Class::DestFirst),
("movw", Class::DestFirst),
("mov", Class::DestFirst),
("mul", Class::DestFirst),
("mvn", Class::DestFirst),
("orn", Class::DestFirst),
("orr", Class::DestFirst),
("pkhbt", Class::DestFirst),
("pkhtb", Class::DestFirst),
("qadd", Class::DestFirst),
("qasx", Class::DestFirst),
("qdadd", Class::DestFirst),
("qdsub", Class::DestFirst),
("qsax", Class::DestFirst),
("qsub", Class::DestFirst),
("rbit", Class::DestFirst),
("rev", Class::DestFirst), ("ror", Class::DestFirst),
("rrx", Class::DestFirst),
("rsb", Class::DestFirst),
("sadd", Class::DestFirst),
("sasx", Class::DestFirst),
("sbc", Class::DestFirst),
("sbfx", Class::DestFirst),
("sdiv", Class::DestFirst),
("sel", Class::DestFirst),
("shadd", Class::DestFirst),
("shasx", Class::DestFirst),
("shsax", Class::DestFirst),
("shsub", Class::DestFirst),
("smla", Class::DestFirst), ("smmla", Class::DestFirst),
("smmls", Class::DestFirst),
("smmul", Class::DestFirst),
("smuad", Class::DestFirst),
("smul", Class::DestFirst), ("smusd", Class::DestFirst),
("ssat", Class::DestFirst),
("ssax", Class::DestFirst),
("ssub", Class::DestFirst),
("subw", Class::DestFirst),
("sub", Class::DestFirst),
("sxtab", Class::DestFirst),
("sxtah", Class::DestFirst),
("sxtb", Class::DestFirst),
("sxth", Class::DestFirst),
("uadd", Class::DestFirst),
("uasx", Class::DestFirst),
("ubfx", Class::DestFirst),
("udiv", Class::DestFirst),
("uhadd", Class::DestFirst),
("uhasx", Class::DestFirst),
("uhsax", Class::DestFirst),
("uhsub", Class::DestFirst),
("uqadd", Class::DestFirst),
("uqasx", Class::DestFirst),
("uqsax", Class::DestFirst),
("uqsub", Class::DestFirst),
("usad8", Class::DestFirst),
("usada8", Class::DestFirst),
("usat", Class::DestFirst),
("usax", Class::DestFirst),
("usub", Class::DestFirst),
("uxtab", Class::DestFirst),
("uxtah", Class::DestFirst),
("uxtb", Class::DestFirst),
("uxth", Class::DestFirst),
];
#[derive(Debug)]
pub struct Violation {
pub symbol: String,
pub address: String,
pub text: String,
pub regs: Vec<String>,
pub reason: &'static str,
}
#[derive(Debug, Default)]
pub struct ScanReport {
pub instructions: usize,
pub symbols: usize,
pub violations: Vec<Violation>,
pub acknowledged: BTreeMap<String, usize>,
}
fn norm_reg(tok: &str) -> String {
let t = tok
.trim()
.trim_start_matches('-')
.trim_end_matches('!')
.to_ascii_lowercase();
match t.as_str() {
"sb" => "r9".into(),
"sl" => "r10".into(),
"fp" => "r11".into(),
"ip" => "r12".into(),
_ => t,
}
}
fn is_gp_reg(tok: &str) -> bool {
matches!(
tok,
"r0" | "r1"
| "r2"
| "r3"
| "r4"
| "r5"
| "r6"
| "r7"
| "r8"
| "r9"
| "r10"
| "r11"
| "r12"
| "sp"
| "lr"
| "pc"
)
}
fn is_reserved(tok: &str) -> bool {
RESERVED.contains(&tok)
}
fn split_operands(ops: &str) -> Vec<&str> {
let mut out = Vec::new();
let (mut depth, mut start) = (0usize, 0usize);
for (i, c) in ops.char_indices() {
match c {
'[' | '{' | '(' => depth += 1,
']' | '}' | ')' => depth = depth.saturating_sub(1),
',' if depth == 0 => {
out.push(ops[start..i].trim());
start = i + 1;
}
_ => {}
}
}
let last = ops[start..].trim();
if !last.is_empty() {
out.push(last);
}
out.retain(|s| !s.is_empty());
out
}
fn reg_tokens(s: &str) -> Vec<String> {
s.split(|c: char| !c.is_ascii_alphanumeric())
.filter(|t| !t.is_empty())
.map(norm_reg)
.filter(|t| is_gp_reg(t))
.collect()
}
fn first_reg(op: &str) -> Option<String> {
let t = norm_reg(op.split([',', ' ']).next()?);
is_gp_reg(&t).then_some(t)
}
fn writeback_regs(ops: &str) -> Vec<String> {
let mut out = Vec::new();
if let Some(open) = ops.find('[')
&& let Some(close) = ops[open..].find(']').map(|i| open + i)
{
let after = ops[close + 1..].trim_start();
if (after.starts_with('!') || after.starts_with(','))
&& let Some(base) = reg_tokens(&ops[open..close]).first()
{
out.push(base.clone());
}
}
for op in split_operands(ops) {
if let Some(stripped) = op.strip_suffix('!')
&& !op.contains('[')
{
let t = norm_reg(stripped);
if is_gp_reg(&t) {
out.push(t);
}
}
}
out
}
fn classify(mnemonic: &str) -> Option<Class> {
let m = mnemonic.to_ascii_lowercase();
let m = m
.strip_suffix(".w")
.or_else(|| m.strip_suffix(".n"))
.unwrap_or(&m);
MNEMONICS
.iter()
.filter(|(p, _)| m.starts_with(p))
.max_by_key(|(p, _)| p.len())
.map(|(_, c)| *c)
}
fn written_regs(mnemonic: &str, ops: &str) -> Result<Vec<String>, &'static str> {
let Some(class) = classify(mnemonic) else {
return Err("unknown mnemonic — cannot classify, refused (fail-closed)");
};
if class == Class::Data {
return Ok(Vec::new());
}
let operands = split_operands(ops);
let mut written = writeback_regs(ops);
match class {
Class::Data | Class::ReadOnly => {}
Class::DestFirst => {
if let Some(r) = operands.first().and_then(|o| first_reg(o)) {
written.push(r);
}
}
Class::DestFirstTwo => {
for op in operands.iter().take(2) {
if let Some(r) = first_reg(op) {
written.push(r);
}
}
}
Class::MrcThird => {
if let Some(r) = operands.get(2).and_then(|o| first_reg(o)) {
written.push(r);
}
}
Class::MrrcThirdFourth => {
for idx in [2usize, 3] {
if let Some(r) = operands.get(idx).and_then(|o| first_reg(o)) {
written.push(r);
}
}
}
Class::LoadMultiple => {
if let Some(open) = ops.find('{') {
let inner = ops[open..].trim_start_matches('{').trim_end_matches('}');
written.extend(reg_tokens(inner));
}
}
Class::Vmov => {
for op in &operands {
match first_reg(op) {
Some(r) => written.push(r),
None => break, }
}
}
}
Ok(written)
}
fn parse_insn_line(line: &str) -> Option<(String, String, String)> {
let trimmed = line.trim_start();
let colon = trimmed.find(':')?;
let (addr, rest) = trimmed.split_at(colon);
if addr.is_empty() || !addr.chars().all(|c| c.is_ascii_hexdigit()) {
return None;
}
let rest = &rest[1..]; let mut toks = rest.split_whitespace().peekable();
let mut saw_bytes = false;
while let Some(&t) = toks.peek() {
let is_bytes = matches!(t.len(), 2 | 4 | 8) && t.chars().all(|c| c.is_ascii_hexdigit());
if is_bytes {
saw_bytes = true;
toks.next();
} else {
break;
}
}
if !saw_bytes {
return None; }
let mnemonic = toks.next()?.to_string();
let ops_raw: String = toks.collect::<Vec<_>>().join(" ");
let mut ops = ops_raw.as_str();
for sep in [" ;", " @", " //", "<"] {
if let Some(i) = ops.find(sep) {
ops = &ops[..i];
}
}
Some((addr.to_string(), mnemonic, ops.trim().to_string()))
}
pub fn scan_disassembly(text: &str, allow: &[String]) -> ScanReport {
let mut report = ScanReport::default();
let mut current_symbol = String::from("(before first symbol)");
for line in text.lines() {
let t = line.trim();
if t.ends_with(">:")
&& let Some(open) = t.find('<')
{
current_symbol = t[open + 1..t.len() - 2].to_string();
report.symbols += 1;
continue;
}
let Some((addr, mnemonic, ops)) = parse_insn_line(line) else {
continue;
};
let is_data = classify(&mnemonic) == Some(Class::Data);
if !is_data {
report.instructions += 1;
}
let undecodable = mnemonic.contains("unknown")
|| mnemonic.starts_with(".inst")
|| line.contains("<UNDEFINED>") || !mnemonic
.chars()
.next()
.is_some_and(|c| c.is_ascii_alphabetic() || c == '.');
let mentioned: Vec<String> = reg_tokens(&ops)
.into_iter()
.filter(|r| is_reserved(r))
.collect();
if mentioned.is_empty() && !undecodable {
continue; }
if undecodable {
let v = Violation {
symbol: current_symbol.clone(),
address: addr,
text: format!("{mnemonic} {ops}"),
regs: Vec::new(),
reason: "undecodable bytes in an executable region — refused (fail-closed)",
};
if allow.contains(¤t_symbol) {
*report
.acknowledged
.entry(current_symbol.clone())
.or_default() += 1;
} else {
report.violations.push(v);
}
continue;
}
match written_regs(&mnemonic, &ops) {
Ok(written) => {
let hit: Vec<String> = written.into_iter().filter(|r| is_reserved(r)).collect();
if hit.is_empty() {
continue;
}
if allow.contains(¤t_symbol) {
*report
.acknowledged
.entry(current_symbol.clone())
.or_default() += 1;
} else {
report.violations.push(Violation {
symbol: current_symbol.clone(),
address: addr,
text: format!("{mnemonic} {ops}"),
regs: hit,
reason: "writes a reserved register",
});
}
}
Err(reason) => {
if allow.contains(¤t_symbol) {
*report
.acknowledged
.entry(current_symbol.clone())
.or_default() += 1;
} else {
report.violations.push(Violation {
symbol: current_symbol.clone(),
address: addr,
text: format!("{mnemonic} {ops}"),
regs: mentioned,
reason,
});
}
}
}
}
report
}
fn run_objdump(input: &Path, thumb: bool) -> Result<(String, String)> {
let triple = if thumb {
"thumbv7em-none-eabi"
} else {
"armv7r-none-eabi"
};
let try_tool = |tool: &str, args: &[String]| -> Option<String> {
let out = Command::new(tool).args(args).arg(input).output().ok()?;
if !out.status.success() {
return None;
}
let text = String::from_utf8_lossy(&out.stdout).into_owned();
text.lines()
.any(|l| parse_insn_line(l).is_some())
.then_some(text)
};
if let Ok(tool) = std::env::var("SYNTH_OBJDUMP") {
let out = Command::new(&tool)
.arg("-d")
.arg(input)
.output()
.with_context(|| format!("SYNTH_OBJDUMP={tool} could not be executed"))?;
anyhow::ensure!(
out.status.success(),
"SYNTH_OBJDUMP={tool} failed: {}",
String::from_utf8_lossy(&out.stderr)
);
return Ok((String::from_utf8_lossy(&out.stdout).into_owned(), tool));
}
let candidates: [(&str, Vec<String>); 4] = [
("arm-none-eabi-objdump", vec!["-d".into()]),
(
"llvm-objdump",
vec!["-d".into(), format!("--triple={triple}")],
),
("objdump", vec!["-d".into(), format!("--triple={triple}")]),
("objdump", vec!["-d".into()]),
];
for (tool, args) in &candidates {
if let Some(text) = try_tool(tool, args) {
return Ok((text, (*tool).to_string()));
}
}
anyhow::bail!(
"no ARM-capable objdump found (tried arm-none-eabi-objdump, \
llvm-objdump, objdump; set SYNTH_OBJDUMP=<path> to point at one). \
verify-embedder checks emitted code, so it needs a disassembler."
)
}
pub fn verify_embedder_command(input: &Path, allow_writer: Vec<String>) -> Result<()> {
anyhow::ensure!(input.exists(), "File not found: {}", input.display());
let bytes = std::fs::read(input).context("failed to read input")?;
let Some(thumb) = crate::detect_arm_thumb(&bytes) else {
anyhow::bail!(
"{} is not a little-endian ELF32 EM_ARM file — verify-embedder \
checks the ARM --relocatable embedder contract (R9/R10/R11, \
docs/embedder-abi-relocatable-arm.md) and has nothing to say \
about other architectures",
input.display()
);
};
let (text, tool) = run_objdump(input, thumb)?;
let report = scan_disassembly(&text, &allow_writer);
anyhow::ensure!(
report.instructions > 0,
"verify-embedder scanned 0 instructions in {} (via {tool}) — refusing \
to report conformance about nothing",
input.display()
);
let acknowledged: usize = report.acknowledged.values().sum();
for name in &allow_writer {
if !report.acknowledged.contains_key(name) && !text.contains(&format!("<{name}>:")) {
anyhow::bail!(
"--allow-writer {name}: no symbol of that name in {} — refusing \
(a misspelled acknowledgement waives nothing and hides drift)",
input.display()
);
}
}
if report.violations.is_empty() {
println!(
"verify-embedder OK: {} — 0 reserved-register writes in {} \
instructions across {} symbols (via {tool})",
input.display(),
report.instructions,
report.symbols,
);
for (sym, n) in &report.acknowledged {
println!(
" acknowledged (--allow-writer): {sym} — {n} write(s) [the establishment site]"
);
}
println!(
" contract: R9=globals base, R10=linmem size, R11=linmem base \
(docs/embedder-abi-relocatable-arm.md)"
);
println!(
" bounds: direct writes in THIS ELF's code only — cannot see code \
outside the image, runtime context switches/exception installs, or \
stripped-object literal-pool decode; see the doc's verify-embedder \
section"
);
if acknowledged == 0 && allow_writer.is_empty() {
println!(
" note: no --allow-writer given and no writes found — if this \
image is supposed to CONTAIN its boot code, the establishment \
writes should exist somewhere; an object checked before \
linking boot is fine"
);
}
return Ok(());
}
eprintln!(
"verify-embedder REFUSED: {} — {} write(s) to reserved registers \
R9/R10/R11 (the --relocatable embedder contract, \
docs/embedder-abi-relocatable-arm.md):",
input.display(),
report.violations.len(),
);
for v in &report.violations {
let regs = if v.regs.is_empty() {
String::new()
} else {
format!(" [{}]", v.regs.join(", "))
};
eprintln!(
" <{}> {}: `{}` — {}{}",
v.symbol, v.address, v.text, v.reason, regs
);
}
eprintln!(
" fix: compile embedder objects with -ffixed-r9 -ffixed-r10 \
-ffixed-r11; name the register-establishment site (boot code) with \
--allow-writer <symbol> to acknowledge it"
);
anyhow::bail!(
"{} reserved-register write(s) — the linked code violates the \
embedder ABI",
report.violations.len()
)
}
#[cfg(test)]
mod tests {
use super::*;
fn scan(text: &str) -> ScanReport {
scan_disassembly(text, &[])
}
#[test]
fn gnu_direct_write_refused() {
let t = "00000000 <bad_shim>:\n 0:\t4683 \tmov\tfp, r0\n 2:\t4770 \tbx\tlr\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].regs, vec!["r11"]);
assert_eq!(r.violations[0].symbol, "bad_shim");
assert_eq!(r.instructions, 2);
}
#[test]
fn llvm_direct_write_refused() {
let t = "00000000 <bad_shim>:\n 0: 4683 \tmov\tr11, r0\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].regs, vec!["r11"]);
}
#[test]
fn reads_pass() {
let t = concat!(
"00000008 <run>:\n",
" 8:\tf85b 200c \tldr.w\tr2, [fp, ip]\n",
" c:\tf8d9 3000 \tldr.w\tr3, [r9]\n",
" 10:\tea4f 451a \tmov.w\tr5, sl, lsr #16\n", " 14:\t4630 \tmov\tr0, r6\n",
" 16:\tf8cb 0000 \tstr.w\tr0, [fp]\n",
);
let r = scan(t);
assert!(r.violations.is_empty(), "{:?}", r.violations);
assert_eq!(r.instructions, 5);
}
#[test]
fn pop_reglist_refused() {
let t =
"00000000 <f>:\n 0:\te8bd 8bf0 \tldmia.w\tsp!, {r4, r5, r6, r7, r8, r9, fp, pc}\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].regs, vec!["r9", "r11"]);
}
#[test]
fn push_reglist_passes() {
let t = "00000000 <f>:\n 0:\te92d 4880 \tstmdb\tsp!, {r7, fp, lr}\n";
assert!(scan(t).violations.is_empty());
}
#[test]
fn writeback_refused() {
let t = concat!(
"00000000 <f>:\n",
" 0:\tf84b 0b04 \tstr.w\tr0, [fp], #4\n",
" 4:\tf85a 0f04 \tldr.w\tr0, [sl, #4]!\n",
);
let r = scan(t);
assert_eq!(r.violations.len(), 2);
assert_eq!(r.violations[0].regs, vec!["r11"]);
assert_eq!(r.violations[1].regs, vec!["r10"]);
}
#[test]
fn umull_second_dest_refused() {
let t = "00000000 <f>:\n 0:\tfba0 9b01 \tumull\tr0, fp, r0, r1\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].regs, vec!["r11"]);
}
#[test]
fn mrc_third_operand_refused() {
let t = "00000000 <f>:\n 0:\tee1d 9f50 \tmrc\tp15, #0, r9, c13, c0, #2\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].regs, vec!["r9"]);
}
#[test]
fn vmov_direction() {
let t = "00000000 <f>:\n 0:\tee10 9a10 \tvmov\tr9, s0\n";
assert_eq!(scan(t).violations.len(), 1);
let t2 = "00000000 <f>:\n 0:\tee00 9a10 \tvmov\ts0, r9\n";
assert!(scan(t2).violations.is_empty());
}
#[test]
fn unknown_mnemonic_fail_closed() {
let t = "00000000 <f>:\n 0:\tffff ffff \tfrobnicate\tfp, r0\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert!(r.violations[0].reason.contains("cannot classify"));
}
#[test]
fn unknown_mnemonic_without_reserved_passes() {
let t = "00000000 <f>:\n 0:\tffff ffff \tfrobnicate\tr0, r1\n";
assert!(scan(t).violations.is_empty());
}
#[test]
fn undecodable_refused() {
let t = "00000000 <f>:\n 0: ffff ffff \t<unknown>\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert!(r.violations[0].reason.contains("undecodable"));
}
#[test]
fn literal_pool_skipped() {
let t = "00000000 <f>:\n 0:\t4770 \tbx\tlr\n 4:\t20000100 \t.word\t0x20000100\n";
let r = scan(t);
assert!(r.violations.is_empty());
assert_eq!(r.instructions, 1); }
#[test]
fn allow_writer_scoped() {
let t = concat!(
"00000000 <boot_entry>:\n",
" 0:\t4683 \tmov\tfp, r0\n",
"00000004 <shim>:\n",
" 4:\t46b3 \tmov\tfp, r6\n",
);
let r = scan_disassembly(t, &["boot_entry".to_string()]);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].symbol, "shim");
assert_eq!(r.acknowledged.get("boot_entry"), Some(&1));
}
#[test]
fn predicated_write_refused() {
let t = "00000000 <f>:\n 0:\tbf08 \tit\teq\n 2:\t4683 \tmoveq\tfp, r0\n";
assert_eq!(scan(t).violations.len(), 1);
}
#[test]
fn load_into_reserved_refused() {
let t = "00000000 <f>:\n 0:\tf8df 9010 \tldr.w\tr9, [pc, #16]\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.violations[0].regs, vec!["r9"]);
}
#[test]
fn a32_format_parses() {
let t = "00000000 <f>:\n 0:\te1a0b000 \tmov\tfp, r0\n 4:\te12fff1e \tbx\tlr\n";
let r = scan(t);
assert_eq!(r.violations.len(), 1);
assert_eq!(r.instructions, 2);
}
#[test]
fn empty_scan_is_vacuous() {
assert_eq!(scan("no disassembly here\n").instructions, 0);
}
}