From 609e65a8dba0b1a79b3a4a038fc13d13fc095189 Mon Sep 17 00:00:00 2001 From: Ava Affine Date: Tue, 29 Jul 2025 23:29:34 +0000 Subject: [PATCH] Test harness for HyphaeVM This commit adds a testing framework for HyphaeVM which enables testing various aspects of the VM state after running input programs against a VM with possibly preinitialized state. This includes a builder pattern initializer for the VM, and bespoke logic for a test case tester. Signed-off-by: Ava Affine --- hyphae/build.rs | 7 +- hyphae/src/hmap.rs | 1 - hyphae/src/stackstack.rs | 47 +++++++ hyphae/src/vm.rs | 260 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 312 insertions(+), 3 deletions(-) diff --git a/hyphae/build.rs b/hyphae/build.rs index 0255141..78115a1 100644 --- a/hyphae/build.rs +++ b/hyphae/build.rs @@ -20,6 +20,7 @@ struct Instruction { } fn main() { + let mut peak = 0; let output_path = Path::new(&env::var("OUT_DIR").unwrap()) .join("hyphae_instr.rs"); let input = fs::read_to_string("instructions.toml") @@ -73,7 +74,9 @@ fn main() { const_name, const_name).as_str(); isa_num_args += format!(" {} => Ok({}),\n", idx, instr.args.len()) - .as_str() + .as_str(); + + peak = idx + 1; }); isa_from_byte += " _ => Err(\"illegal instruction\"),\n"; @@ -104,6 +107,8 @@ fn main() { write!(&mut output_file, "use core::str::FromStr;\n\n\n").unwrap(); write!(&mut output_file, "{}", isa).unwrap(); + write!(&mut output_file, "\n\npub const TOTAL_INSTRUCTIONS: usize = {};", peak) + .unwrap(); println!("cargo::rerun-if-changed=build.rs"); println!("cargo::rerun-if-changed=instructions.json"); } diff --git a/hyphae/src/hmap.rs b/hyphae/src/hmap.rs index 2705cd1..2062f02 100755 --- a/hyphae/src/hmap.rs +++ b/hyphae/src/hmap.rs @@ -140,7 +140,6 @@ pub struct QuickMapIter<'a, T: Clone> { impl<'a, T: Clone> Iterator for QuickMapIter<'a, T> { type Item = &'a (String, T); - fn next(&mut self) -> Option { self.vec_iter .next() diff --git a/hyphae/src/stackstack.rs b/hyphae/src/stackstack.rs index db1ab3d..6a403fb 100644 --- a/hyphae/src/stackstack.rs +++ b/hyphae/src/stackstack.rs @@ -24,16 +24,63 @@ struct StackInner { pub data: T } +impl Clone for StackInner { + fn clone(&self) -> Self { + StackInner{ + next: self.next.clone(), + data: self.data.clone() + } + } + + fn clone_from(&mut self, source: &Self) { + *self = source.clone() + } +} + struct Stack (Rc>>); +impl Clone for Stack { + fn clone(&self) -> Self { + Stack(Rc::from((*self.0).clone())) + } + + fn clone_from(&mut self, source: &Self) { + *self = source.clone() + } +} + struct StackStackInner { next: StackStack, count: usize, stack: Stack, } +impl Clone for StackStackInner { + fn clone(&self) -> Self { + StackStackInner{ + next: self.next.clone(), + count: self.count, + stack: self.stack.clone() + } + } + + fn clone_from(&mut self, source: &Self) { + *self = source.clone() + } +} + pub struct StackStack (Rc>>); +impl Clone for StackStack { + fn clone(&self) -> Self { + StackStack(Rc::from((*self.0).clone())) + } + + fn clone_from(&mut self, source: &Self) { + *self = source.clone() + } +} + impl From for StackInner { fn from(t: T) -> StackInner { StackInner { diff --git a/hyphae/src/vm.rs b/hyphae/src/vm.rs index c6ad370..a181055 100644 --- a/hyphae/src/vm.rs +++ b/hyphae/src/vm.rs @@ -25,6 +25,7 @@ use crate::util::{Operand, Program, Address}; use crate::heap::{Gc, Datum, Cons}; use core::ops::DerefMut; +use core::array; use alloc::vec; use alloc::vec::Vec; @@ -36,12 +37,12 @@ use num::pow::Pow; const NUM_OPERAND_REGISTERS: usize = 4; +#[derive(Clone)] pub struct VM { // execution environment pub stack: StackStack>, pub symtab: QuickMap, pub prog: Program, - pub fds: Vec, pub traps: Vec>, // data registers @@ -58,7 +59,71 @@ pub struct VM { pub err_state: bool, } +impl From for VM { + fn from(value: Program) -> Self { + VM{ + stack: StackStack::new(), + symtab: QuickMap::new(), + prog: value, + traps: vec![], + expr: Datum::None.into(), + oper: array::from_fn(|_| Datum::None.into()), + retn: 0, + ictr: 0, + errr: Datum::None.into(), + running: true, + err_state: false + } + } +} + impl VM { + pub fn new_with_opts( + program: Program, + traps: Option>>, + stack: Option>>, + syms: Option> + ) -> VM { + Into::::into(program) + .with_stack(stack) + .with_symbols(syms) + .with_traps(traps) + .to_owned() // not efficient, but we are not executing + } + + pub fn with_stack( + &mut self, + maybe_stack: Option>>, + ) -> &mut VM { + if let Some(stack) = maybe_stack { + self.stack = stack; + } + + self + } + + pub fn with_symbols( + &mut self, + maybe_symbols: Option>, + ) -> &mut VM { + if let Some(symbols) = maybe_symbols { + self.symtab = symbols; + } + + self + } + + pub fn with_traps( + &mut self, + maybe_traps: Option>>, + ) -> &mut VM { + if let Some(traps) = maybe_traps { + self.traps = traps; + } + + self + } + pub fn run_program(&mut self) { if self.prog.0.len() < 1 { self.running = false; @@ -521,3 +586,196 @@ impl VM { } } +#[cfg(test)] +mod tests { + use core::array; + use super::*; + use crate::instr; + + const ISA_TESTS: [Option>; instr::TOTAL_INSTRUCTIONS] = [ + // TRAP + None, + // BIND + None, + // UNBIND + None, + // BOUND + None, + // PUSH + None, + // POP + None, + // ENTER + None, + // EXIT + None, + // LOAD + None, + // DUPL + None, + // CLEAR + None, + // NOP + None, + // HALT + None, + // PANIC + None, + // JMP + None, + // JMPIF + None, + // EQ + None, + // LT + None, + // GT + None, + // LTE + None, + // GTE + None, + // BOOL_NOT + None, + // BOOL_AND + None, + // BOOL_OR + None, + // BYTE_AND + None, + // BYTE_OR + None, + // XOR + None, + // BYTE_NOT + None, + // ADD + None, + // SUB + None, + // MUL + None, + // FDIV + None, + // IDIV + None, + // POW + None, + // MODULO + None, + // REM + None, + // INC + None, + // DEC + None, + // CTON + None, + // NTOC + None, + // NTOI + None, + // NTOE + None, + // CONST + None, + // MKVEC + None, + // MKBVEC + None, + // INDEX + None, + // LENGTH + None, + // SUBSL + None, + // INSER + None, + // CONS + None, + // CAR + None, + // CDR + None, + // CONCAT + None, + // S_APPEND + None, + ]; + + fn has_stack(state: &VM, idx: usize, target: Gc) -> bool { + *(state.stack[idx]) == *target + } + + fn has_sym(state: &VM, sym: &str, operand: Option) -> bool { + (operand.is_some() == state.symtab.contains_key(sym)) && + (operand.is_none() || state.symtab.get(&sym.to_owned()) == + operand.as_ref()) + } + + #[derive(Clone)] + struct TestResult { + expr: Option>, + stack: Vec<(usize, Gc)>, + syms: Vec<(&'static str, Option)>, + errr: Option<&'static str> + } + + impl TestResult { + fn chk_expr(&self, state: &VM) -> bool { + self.expr.is_none() || *(state.expr) == *(self.expr.clone().unwrap()) + } + + fn chk_err(&self, state: &VM) -> bool { + if let Datum::String(ref msg) = *state.errr { + let msg = unsafe { str::from_utf8_unchecked(msg) }; + msg == self.errr.unwrap() + } else if let Datum::None = *state.errr { + self.errr.is_none() + } else { + false + } + } + + fn chk_stack(&self, state: &VM) -> bool { + for i in &self.stack { + if !has_stack(&state, i.0, i.1.clone()) { + return false + } + } + true + } + + fn chk_syms(&self, state: &VM) -> bool { + for i in &self.syms { + if !has_sym(&state, i.0, i.1.clone()) { + return false + } + } + true + } + + fn test_passes(&self, state: VM) -> bool { + (self.expr.is_none() || self.chk_expr(&state)) && + (self.stack.is_empty() || self.chk_stack(&state)) && + (self.syms.is_empty() || self.chk_syms(&state)) && + (self.errr.is_none() || self.chk_err(&state)) + } + } + + #[test] + fn run_isa_tests() { + for i in &ISA_TESTS.to_vec() { + let Some(test_case) = i else { + assert!(false); // dont let untested instructions happen + return + }; + + for i in test_case { + let (mut vm, result) = (i.0.clone(), i.1.clone()); + vm.run_program(); + assert!(result.test_passes(vm)); + } + } + } +}