#![cfg(all(not(target_arch = "wasm32"), feature = "std"))]
use std::collections::BTreeSet;
use std::path::{Path, PathBuf};
use asm_rs::{Arch, Assembler, Syntax};
const PLACEHOLDERS: &[&str] = &[
"rd",
"rs",
"rs1",
"rs2",
"rn",
"rm",
"rt",
"rt2",
"ra",
"imm",
"imm3",
"imm4",
"imm5",
"imm6",
"imm8",
"imm12",
"imm16",
"imm32",
"imm64",
"immr",
"imms",
"offset",
"mem",
"label",
"r8",
"r16",
"r32",
"r64",
"xn",
"xm",
"xd",
"xt",
"wn",
"wm",
"wd",
"wt",
"vn",
"vd",
"vm",
"zn",
"zd",
"zm",
"pg",
"shift",
"cond",
"csr",
"uimm",
"sym",
"target",
"func",
"addr",
"reg",
"vs1",
"vs2",
"xs1",
"count",
"amount",
"width",
"lsb",
"xmm",
"ymm",
"zmm",
"mm",
"k1",
"cc",
"n",
"m",
"sp_offset",
"far_label",
"loop_start",
"value",
];
fn contains_placeholder(operands: &str) -> bool {
operands
.split(|c: char| !c.is_ascii_alphanumeric() && c != '_')
.any(|w| PLACEHOLDERS.contains(&w.to_ascii_lowercase().as_str()))
}
fn is_alternation(line: &str) -> bool {
line.contains(" / ")
}
fn syntax_for(snippet: &str, arch: Arch) -> Syntax {
if snippet.contains('%') {
Syntax::Att
} else if matches!(arch, Arch::Arm | Arch::Thumb | Arch::Aarch64) {
Syntax::Ual
} else {
Syntax::Intel
}
}
fn strip_comment(line: &str, arch: Arch) -> &str {
let line = line.split("//").next().unwrap_or("");
let line = match arch {
Arch::Arm | Arch::Thumb | Arch::Aarch64 => line.split('@').next().unwrap_or(""),
_ => line.split('#').next().unwrap_or(""),
};
line.trim()
}
fn arch_for(path: &Path) -> Option<Arch> {
let name = path.file_stem()?.to_str()?;
Some(match name {
"aarch64" => Arch::Aarch64,
"arm32" => Arch::Arm,
"thumb" => Arch::Thumb,
"riscv" => Arch::Rv64,
"x86_64" => Arch::X86_64,
_ => return None,
})
}
fn docs_reference_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("../..")
.join("site/content/docs/reference")
}
fn snippets(text: &str, arch: Arch) -> Vec<String> {
let mut out = Vec::new();
let mut in_asm = false;
for line in text.lines() {
if line.starts_with("+++") {
continue;
}
if line.starts_with("```") {
in_asm = line.starts_with("```asm");
continue;
}
if !in_asm {
continue;
}
let stripped = strip_comment(line, arch);
if stripped.is_empty() || stripped.starts_with('.') || is_alternation(stripped) {
continue;
}
let operands = stripped
.split_once(char::is_whitespace)
.map_or("", |(_, rest)| rest);
if contains_placeholder(operands) {
continue;
}
out.push(stripped.to_string());
}
out
}
fn try_assemble(snippet: &str, arch: Arch) -> Result<(), asm_rs::AsmError> {
let mut asm = Assembler::new(arch);
asm.syntax(syntax_for(snippet, arch));
for name in [
"label",
"target",
"loop",
"done",
"far_label",
"msg",
"data",
"func",
"start",
"end",
] {
asm.define_external(name, 0x1000);
}
asm.emit(snippet)?;
asm.finish().map(|_| ())
}
#[test]
fn every_documented_instruction_assembles() {
let dir = docs_reference_dir();
assert!(
dir.is_dir(),
"reference pages not found at {} — this test must be run from the workspace",
dir.display()
);
let mut failures: Vec<String> = Vec::new();
let mut checked = 0usize;
let mut pages = 0usize;
let mut entries: Vec<PathBuf> = std::fs::read_dir(&dir)
.expect("read docs/reference")
.map(|e| e.expect("dir entry").path())
.collect();
entries.sort();
for path in entries {
let Some(arch) = arch_for(&path) else {
continue;
};
pages += 1;
let text = std::fs::read_to_string(&path).expect("read reference page");
let page = path.file_name().unwrap().to_string_lossy().to_string();
let mut seen = BTreeSet::new();
for snippet in snippets(&text, arch) {
if !seen.insert(snippet.clone()) {
continue;
}
checked += 1;
if let Err(e) = try_assemble(&snippet, arch) {
failures.push(format!(" {page}: `{snippet}`\n {e}"));
}
}
}
assert!(pages > 0, "no reference pages matched a known architecture");
assert!(
failures.is_empty(),
"{} of {checked} documented instructions do not assemble:\n{}\n\
\nEither implement them or correct the documentation — the reference \
pages are the project's statement of what it supports.",
failures.len(),
failures.join("\n")
);
}