use alloc::format;
use alloc::string::{String, ToString};
use alloc::vec::Vec;
use std::path::Path;
use crate::core::space::{
AccessConstraints, AddressSpace, MemAttrs, MemOps, MemResult, Region, UnassignedPolicy,
};
use crate::core::sync::{self, LockRank};
use super::{Config, Reg, Regs, X86};
const KNOWN_FAILURES: &[(&str, &str)] = &[
(
"F6.5",
"IMUL r/m8: sign, zero, parity and auxiliary results, all documented \
as undefined, after a signed multiply whose microcode adjusts signs \
around the magnitude loop (3414 of 10000 vectors)",
),
("F7.5", "IMUL r/m16: as F6.5 (3499 of 10000 vectors)"),
(
"F6.6",
"DIV r/m8: the six documented-undefined arithmetic flags after the \
division loop. The carry is exact; the rest are the last trial \
subtraction's, which is close but not the microcode's own final \
state (5855 of 10000 vectors)",
),
("F7.6", "DIV r/m16: as F6.6 (5782 of 10000 vectors)"),
(
"F6.7",
"IDIV r/m8: as F6.6, plus the sign-correction steps the signed divide \
performs after the loop (7157 of 10000 vectors)",
),
("F7.7", "IDIV r/m16: as F6.7 (7133 of 10000 vectors)"),
];
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
struct Access {
addr: u32,
value: u8,
write: bool,
io: bool,
}
impl core::fmt::Display for Access {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"[{}{:05x} {:02x} {}]",
if self.io { "io:" } else { "" },
self.addr,
self.value,
if self.write { "w" } else { "r" }
)
}
}
#[derive(Debug, Default)]
struct Snapshot {
regs: Vec<(String, u16)>,
ram: Vec<(u32, u8)>,
queue: Vec<u8>,
}
#[derive(Debug, Default)]
struct Vector {
name: String,
initial: Snapshot,
expected: Snapshot,
accesses: Vec<Access>,
}
struct Parser<'a> {
bytes: &'a [u8],
at: usize,
want_cycles: bool,
}
impl<'a> Parser<'a> {
fn new(bytes: &'a [u8], want_cycles: bool) -> Parser<'a> {
Parser {
bytes,
at: 0,
want_cycles,
}
}
fn skip_space(&mut self) {
while matches!(self.bytes.get(self.at), Some(b' ' | b'\t' | b'\n' | b'\r')) {
self.at += 1;
}
}
fn peek(&mut self) -> u8 {
self.skip_space();
self.bytes.get(self.at).copied().unwrap_or(b'\0')
}
fn eat(&mut self, byte: u8) {
let got = self.peek();
assert_eq!(
got, byte,
"expected `{}` at offset {}",
byte as char, self.at
);
self.at += 1;
}
fn str_ref(&mut self) -> &'a str {
self.eat(b'"');
let start = self.at;
while self.bytes.get(self.at).is_some_and(|b| *b != b'"') {
self.at += 1;
}
let text = core::str::from_utf8(&self.bytes[start..self.at]).unwrap_or("");
self.at += 1;
text
}
fn number(&mut self) -> i64 {
self.skip_space();
let start = self.at;
if self.bytes.get(self.at) == Some(&b'-') {
self.at += 1;
}
while matches!(self.bytes.get(self.at), Some(b'0'..=b'9')) {
self.at += 1;
}
core::str::from_utf8(&self.bytes[start..self.at])
.expect("ascii")
.parse()
.expect("integer")
}
fn skip_value(&mut self) {
match self.peek() {
b'{' | b'[' => {
let close = if self.peek() == b'{' { b'}' } else { b']' };
self.at += 1;
if self.peek() == close {
self.at += 1;
return;
}
loop {
if close == b'}' {
let _ = self.str_ref();
self.eat(b':');
}
self.skip_value();
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(close);
}
b'"' => {
let _ = self.str_ref();
}
b't' => self.at += 4,
b'f' => self.at += 5,
b'n' => self.at += 4,
_ => {
let _ = self.number();
}
}
}
fn cell_list(&mut self) -> Vec<(u32, u8)> {
let mut out = Vec::new();
self.eat(b'[');
if self.peek() == b']' {
self.at += 1;
return out;
}
loop {
self.eat(b'[');
let addr = self.number() as u32;
self.eat(b',');
let value = self.number() as u8;
self.eat(b']');
out.push((addr, value));
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b']');
out
}
fn byte_list(&mut self) -> Vec<u8> {
let mut out = Vec::new();
self.eat(b'[');
if self.peek() == b']' {
self.at += 1;
return out;
}
loop {
out.push(self.number() as u8);
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b']');
out
}
fn snapshot(&mut self) -> Snapshot {
let mut snap = Snapshot::default();
self.eat(b'{');
loop {
let key = self.str_ref();
self.eat(b':');
match key {
"regs" => {
self.eat(b'{');
if self.peek() == b'}' {
self.at += 1;
} else {
loop {
let name = self.str_ref().to_string();
self.eat(b':');
let value = self.number() as u16;
snap.regs.push((name, value));
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b'}');
}
}
"ram" => snap.ram = self.cell_list(),
"queue" => snap.queue = self.byte_list(),
_ => self.skip_value(),
}
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b'}');
snap
}
fn cycles(&mut self) -> Vec<Access> {
let mut out = Vec::new();
let mut latched: Option<(u32, String)> = None;
self.eat(b'[');
if self.peek() == b']' {
self.at += 1;
return out;
}
loop {
self.eat(b'[');
let pin = self.number();
self.eat(b',');
let bus = self.number() as u32;
self.eat(b',');
let _segment = self.str_ref();
self.eat(b',');
let memory = self.str_ref().as_bytes().to_vec();
self.eat(b',');
let io = self.str_ref().as_bytes().to_vec();
self.eat(b',');
let _bhe = self.number();
self.eat(b',');
let data = self.number() as u8;
self.eat(b',');
let bus_status = self.str_ref();
self.eat(b',');
let t_state = self.str_ref();
self.eat(b',');
let _queue_op = self.str_ref();
self.eat(b',');
let _queue_byte = self.number();
self.eat(b']');
if pin & 1 != 0 {
latched = Some((bus, bus_status.to_string()));
}
if let Some((address, ref status)) = latched
&& (t_state == "T3" || t_state == "Tw")
&& status != "CODE"
{
let mem_read = memory.first() == Some(&b'R');
let mem_write = memory.get(1) == Some(&b'A');
let io_read = io.first() == Some(&b'R');
let io_write = io.get(1) == Some(&b'A');
if mem_read || mem_write || io_read || io_write {
out.push(Access {
addr: address,
value: data,
write: mem_write || io_write,
io: io_read || io_write,
});
}
}
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b']');
out
}
fn vector(&mut self) -> Vector {
let mut v = Vector::default();
self.eat(b'{');
loop {
let key = self.str_ref();
self.eat(b':');
match key {
"name" => v.name = self.str_ref().to_string(),
"initial" => v.initial = self.snapshot(),
"final" => v.expected = self.snapshot(),
"cycles" => {
if self.want_cycles {
v.accesses = self.cycles();
} else {
self.skip_value();
}
}
_ => self.skip_value(),
}
if self.peek() == b',' {
self.at += 1;
} else {
break;
}
}
self.eat(b'}');
v
}
}
#[derive(Debug)]
struct BusState {
cells: Vec<u8>,
log: Vec<Access>,
dirty: Vec<u32>,
io: bool,
}
#[derive(Debug)]
struct VectorBus(sync::Mutex<BusState>);
impl VectorBus {
fn new(len: usize, io: bool) -> VectorBus {
VectorBus(sync::Mutex::with_rank(
LockRank::DEVICE,
BusState {
cells: alloc::vec![if io { 0xff } else { 0x00 }; len],
log: Vec::new(),
dirty: Vec::new(),
io,
},
))
}
fn reset(&self, initial: &[(u32, u8)]) {
let mut m = self.0.lock();
let fill = if m.io { 0xff } else { 0x00 };
for addr in core::mem::take(&mut m.dirty) {
m.cells[addr as usize] = fill;
}
m.log.clear();
for (addr, value) in initial {
m.cells[*addr as usize] = *value;
m.dirty.push(*addr);
}
}
fn cell(&self, addr: u32) -> u8 {
self.0.lock().cells[addr as usize]
}
fn take_log(&self) -> Vec<Access> {
core::mem::take(&mut self.0.lock().log)
}
fn written(&self) -> Vec<u32> {
self.0.lock().dirty.clone()
}
}
impl MemOps for VectorBus {
fn read(&self, offset: u64, dst: &mut [u8], attrs: MemAttrs) -> MemResult {
let mut m = self.0.lock();
let io = m.io;
for (i, slot) in dst.iter_mut().enumerate() {
let addr = (offset as usize + i) % m.cells.len();
*slot = m.cells[addr];
if !attrs.debug {
let value = *slot;
m.log.push(Access {
addr: addr as u32,
value,
write: false,
io,
});
}
}
Ok(())
}
fn write(&self, offset: u64, src: &[u8], attrs: MemAttrs) -> MemResult {
let mut m = self.0.lock();
let io = m.io;
for (i, byte) in src.iter().enumerate() {
let addr = (offset as usize + i) % m.cells.len();
m.cells[addr] = *byte;
m.dirty.push(addr as u32);
if !attrs.debug {
m.log.push(Access {
addr: addr as u32,
value: *byte,
write: true,
io,
});
}
}
Ok(())
}
fn constraints(&self) -> AccessConstraints {
AccessConstraints::ANY
}
}
struct Failure {
name: String,
detail: String,
}
fn apply_reg(regs: &mut Regs, name: &str, value: u16) {
let reg = Reg::from_name(name).unwrap_or_else(|| panic!("unknown register `{name}`"));
reg.set(regs, u32::from(value));
}
fn regs_from(base: Regs, list: &[(String, u16)]) -> Regs {
let mut regs = base;
for (name, value) in list {
apply_reg(&mut regs, name, *value);
}
regs
}
fn run_file(path: &Path, cfg: Config, check_bus: bool) -> (usize, Vec<Failure>) {
let bytes = std::fs::read(path).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
let mut parser = Parser::new(&bytes, check_bus);
let mut failures = Vec::new();
let mut ran = 0usize;
let memory = alloc::sync::Arc::new(VectorBus::new(0x10_0000, false));
let ports = alloc::sync::Arc::new(VectorBus::new(0x1_0000, true));
let bits = if cfg.variant.is_32bit() { 21 } else { 20 };
let mem_space = AddressSpace::new("mem", bits).with_unassigned(UnassignedPolicy::FAULT);
mem_space
.topology()
.map(Region::io("ram", 0x10_0000, memory.clone()), 0)
.expect("1 MiB fits in 20 bits");
if cfg.variant.is_32bit() {
mem_space
.topology()
.map(Region::io("alias", 0x10_0000, memory.clone()), 0x10_0000)
.expect("the second megabyte fits in 21 bits");
}
let io_space = AddressSpace::new("io", 16).with_unassigned(UnassignedPolicy::FAULT);
io_space
.topology()
.map(Region::io("ports", 0x1_0000, ports.clone()), 0)
.expect("64 KiB fits in 16 bits");
let mem_space = alloc::sync::Arc::new(mem_space);
let io_space = alloc::sync::Arc::new(io_space);
parser.eat(b'[');
if parser.peek() == b']' {
return (0, failures);
}
loop {
let vector = parser.vector();
ran += 1;
memory.reset(&vector.initial.ram);
ports.reset(&[]);
let cpu = X86::new(cfg);
cpu.attach_space(mem_space.clone());
cpu.attach_io_space(io_space.clone());
let mut initial = regs_from(Regs::new(), &vector.initial.regs);
if cfg.variant.is_32bit() {
initial.eflags = Regs::normalise_flags(cfg.variant, initial.eflags);
}
{
let mut session = cpu.session.lock();
session.state.reset_pending = false;
session.state.regs = initial;
for index in 0..super::isa::seg::COUNT as u8 {
let selector = session.state.regs.segment(index);
let entry = session.state.sys.seg_mut(index);
entry.selector = selector;
entry.base = u32::from(selector) << 4;
entry.limit = 0xffff;
}
}
cpu.set_prefetch_queue(&vector.initial.queue)
.expect("the corpus never over-fills the queue");
memory.take_log();
ports.take_log();
cpu.step();
let mut detail = String::new();
let got = cpu.regs();
let mut want = regs_from(initial, &vector.expected.regs);
if cfg.variant.is_32bit() {
want.eflags = Regs::normalise_flags(cfg.variant, want.eflags);
}
if got != want {
detail.push_str(&format!(" regs want {want}\n got {got}\n"));
for reg in Reg::ALL {
let (a, b) = (reg.get(&want), reg.get(&got));
if a != b {
detail.push_str(&format!(" {reg}: want {a:04x} got {b:04x}\n"));
}
}
}
for (addr, value) in &vector.expected.ram {
let actual = memory.cell(*addr);
if actual != *value {
detail.push_str(&format!(
" ram[{addr:05x}] want {value:02x} got {actual:02x}\n"
));
}
}
for addr in memory.written() {
if vector.expected.ram.iter().any(|(a, _)| *a == addr) {
continue;
}
let want = vector
.initial
.ram
.iter()
.find(|(a, _)| *a == addr)
.map_or(0, |(_, v)| *v);
let actual = memory.cell(addr);
if actual != want {
detail.push_str(&format!(
" ram[{addr:05x}] was not supposed to change: want {want:02x} \
got {actual:02x}\n"
));
}
}
let mut got: Vec<Access> = memory.take_log();
got.extend(ports.take_log());
if check_bus {
let want = &vector.accesses;
let got = strip_prefetch(got, want);
if got != *want {
let show = |cs: &[Access]| {
cs.iter()
.map(ToString::to_string)
.collect::<Vec<_>>()
.join(" ")
};
detail.push_str(&format!(
" bus want {}\n got {}\n",
show(want),
show(&got)
));
}
}
if !detail.is_empty() {
failures.push(Failure {
name: vector.name.clone(),
detail,
});
}
if parser.peek() == b',' {
parser.at += 1;
} else {
break;
}
}
parser.eat(b']');
(ran, failures)
}
fn strip_prefetch(got: Vec<Access>, want: &[Access]) -> Vec<Access> {
let mut out = Vec::with_capacity(want.len());
let mut next = 0usize;
for access in got {
if next < want.len() && want[next] == access {
out.push(access);
next += 1;
} else if access.write || access.io {
out.push(access);
}
}
out
}
const DIFFERENCES_386: &[(&str, &str)] = &[
("60", "PUSHA, not an alias of JO"),
("61", "POPA, not an alias of JNO"),
("62", "BOUND, not an alias of JB"),
("63", "ARPL, not an alias of JNB"),
("64", "the FS segment-override prefix, not an alias of JZ"),
("65", "the GS segment-override prefix, not an alias of JNZ"),
("66", "the operand-size prefix, not an alias of JBE"),
("67", "the address-size prefix, not an alias of JA"),
("68", "PUSH imm16/32, not an alias of JS"),
("69", "IMUL r,r/m,imm, not an alias of JNS"),
("6A", "PUSH imm8, not an alias of JP"),
("6B", "IMUL r,r/m,imm8, not an alias of JNP"),
("6C", "INSB, not an alias of JL"),
("6D", "INSW, not an alias of JGE"),
("6E", "OUTSB, not an alias of JLE"),
("6F", "OUTSW, not an alias of JG"),
(
"C0",
"the byte shift group with an immediate count, not a second RET",
),
(
"C1",
"the word shift group with an immediate count, not a second RET",
),
("C8", "ENTER, not a second RETF"),
("C9", "LEAVE, not a second RETF"),
(
"C6",
"MOV imm with a non-zero extension is invalid on a 386",
),
(
"C7",
"MOV imm with a non-zero extension is invalid on a 386",
),
("FF.7", "the second PUSH entry is invalid on a 386"),
(
"8C",
"a 386 decodes all three bits of the segment field: FS, GS, #UD",
),
(
"8E",
"a 386 decodes all three bits of the segment field: FS, GS, #UD",
),
("9C", "PUSHF stores IOPL and NT where an 8086 stores ones"),
("9D", "POPF loads IOPL and NT where an 8086 forces ones"),
("CC", "INT3 pushes a flags word with IOPL and NT in it"),
("CD", "INT n pushes a flags word with IOPL and NT in it"),
("CE", "INTO pushes a flags word with IOPL and NT in it"),
("CF", "IRET restores IOPL and NT where an 8086 forces ones"),
(
"54",
"PUSH SP stores the value before the decrement from the 286 on",
),
("FF.6", "the same, through the indirect PUSH"),
("D2.0", "ROL by CL: the count is masked to five bits"),
("D2.1", "ROR by CL: the count is masked to five bits"),
("D2.2", "RCL by CL: the count is masked to five bits"),
("D2.3", "RCR by CL: the count is masked to five bits"),
("D2.4", "SHL by CL: the count is masked to five bits"),
("D2.5", "SHR by CL: the count is masked to five bits"),
("D2.6", "SETMO by CL: undocumented, and the count is masked"),
("D2.7", "SAR by CL: the count is masked to five bits"),
("D3.0", "ROL by CL, word: the count is masked to five bits"),
("D3.1", "ROR by CL, word: the count is masked to five bits"),
("D3.2", "RCL by CL, word: the count is masked to five bits"),
("D3.3", "RCR by CL, word: the count is masked to five bits"),
("D3.4", "SHL by CL, word: the count is masked to five bits"),
("D3.5", "SHR by CL, word: the count is masked to five bits"),
(
"D3.6",
"SETMO by CL, word: undocumented, and the count is masked",
),
("D3.7", "SAR by CL, word: the count is masked to five bits"),
(
"D4",
"AAM 0 faults, and #DE is a fault on a 386: it pushes the AAM",
),
("F6.5", "IMUL's undefined flags; see KNOWN_FAILURES"),
(
"F6.6",
"DIV's undefined flags, and #DE pushes the faulting address",
),
(
"F6.7",
"IDIV's undefined flags, and the REP-negates quirk is 8086-only",
),
("F7.5", "IMUL's undefined flags; see KNOWN_FAILURES"),
(
"F7.6",
"DIV's undefined flags, and #DE pushes the faulting address",
),
(
"F7.7",
"IDIV's undefined flags, and the REP-negates quirk is 8086-only",
),
(
"07",
"POP ES with SP at 0xffff: the stack access straddles the limit",
),
("0E", "PUSH CS with SP at 0x0001: the same, downward"),
("5A", "POP DX with SP at 0xffff"),
("81.6", "XOR word at offset 0xffff"),
("A5", "REP MOVSW stepping DI past 0xffff"),
("A7", "REPNE CMPSW stepping an index past 0xffff"),
("AB", "REP STOSW stepping DI past 0xffff"),
("AD", "REP LODSW stepping SI past 0xffff"),
("C5", "LDS reading a far pointer that straddles the limit"),
("FF.0", "INC on a word at offset 0xffff"),
("FF.3", "CALLF pushing across the top of the stack segment"),
(
"EF",
"OUT DX,AX with DX at 0xffff: an 8088 drives two byte cycles and the \
second wraps to port 0, while a 32-bit part drives one word cycle \
that runs off the top of the I/O space",
),
];
#[test]
fn single_step_tests() {
run_corpus(Config::I8088, KNOWN_FAILURES);
}
#[test]
fn single_step_tests_on_a_386() {
run_corpus(Config::I80386, DIFFERENCES_386);
}
fn run_corpus(cfg: Config, ledger: &[(&str, &str)]) {
let Ok(dir) = std::env::var("RSEMU_8088_DIR") else {
println!(
"conformance: set RSEMU_8088_DIR to a decompressed SingleStepTests/8088 \
v2 directory to run 10 000 vectors per opcode; see the module docs \
for the four-line fetch command"
);
return;
};
let dir = Path::new(&dir);
let check_bus = std::env::var("RSEMU_8088_BUS").is_ok_and(|v| v != "0");
let only = std::env::var("RSEMU_8088_OPCODES").ok();
let mut names: Vec<String> = std::fs::read_dir(dir)
.unwrap_or_else(|e| panic!("{}: {e}", dir.display()))
.filter_map(|entry| {
let name = entry.ok()?.file_name().into_string().ok()?;
let stem = name.strip_suffix(".json")?;
(stem != "metadata").then(|| stem.to_string())
})
.collect();
names.sort();
assert!(
!names.is_empty(),
"no NN.json files under {}",
dir.display()
);
let mut files = 0usize;
let mut total = 0usize;
let mut total_failures = 0usize;
let mut failed: Vec<(String, usize)> = Vec::new();
for name in &names {
if let Some(only) = &only
&& !only.split(',').any(|o| o.trim().eq_ignore_ascii_case(name))
{
continue;
}
let (ran, failures) = run_file(&dir.join(format!("{name}.json")), cfg, check_bus);
files += 1;
total += ran;
if !failures.is_empty() {
total_failures += failures.len();
failed.push((name.clone(), failures.len()));
println!(
"{name}: {} of {ran} vectors failed; first is `{}`:\n{}",
failures.len(),
failures[0].name,
failures[0].detail
);
}
}
println!(
"conformance ({}): {} of {total} vectors passed across {files} opcode files{}",
cfg.variant,
total - total_failures,
if check_bus {
" (registers, memory and the operand bus trace)"
} else {
" (registers and memory; set RSEMU_8088_BUS=1 to add the bus trace)"
}
);
let expected: Vec<&str> = ledger.iter().map(|(name, _)| *name).collect();
let (known, unexpected): (Vec<_>, Vec<_>) = failed
.iter()
.partition(|(name, _)| expected.contains(&name.as_str()));
if !known.is_empty() {
let counted: usize = known.iter().map(|(_, n)| *n).sum();
println!(
"conformance: {counted} of those are in the ledger, on {} opcode(s)",
known.len()
);
}
assert!(
unexpected.is_empty(),
"opcodes failing outside the ledger: {unexpected:?}"
);
}