Test harness for HyphaeVM
All checks were successful
per-push tests / build (push) Successful in 49s
per-push tests / test-frontend (push) Successful in 53s
per-push tests / test-utility (push) Successful in 1m2s
per-push tests / test-backend (push) Successful in 54s
per-push tests / timed-decomposer-parse (push) Successful in 56s

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 <ava@sunnypup.io>
This commit is contained in:
Ava Apples Affine 2025-07-29 23:29:34 +00:00
parent ddb49788af
commit 609e65a8db
4 changed files with 312 additions and 3 deletions

View file

@ -20,6 +20,7 @@ struct Instruction {
} }
fn main() { fn main() {
let mut peak = 0;
let output_path = Path::new(&env::var("OUT_DIR").unwrap()) let output_path = Path::new(&env::var("OUT_DIR").unwrap())
.join("hyphae_instr.rs"); .join("hyphae_instr.rs");
let input = fs::read_to_string("instructions.toml") let input = fs::read_to_string("instructions.toml")
@ -73,7 +74,9 @@ fn main() {
const_name, const_name).as_str(); const_name, const_name).as_str();
isa_num_args += format!(" {} => Ok({}),\n", idx, instr.args.len()) isa_num_args += format!(" {} => Ok({}),\n", idx, instr.args.len())
.as_str() .as_str();
peak = idx + 1;
}); });
isa_from_byte += " _ => Err(\"illegal instruction\"),\n"; 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, "use core::str::FromStr;\n\n\n").unwrap();
write!(&mut output_file, "{}", isa).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=build.rs");
println!("cargo::rerun-if-changed=instructions.json"); println!("cargo::rerun-if-changed=instructions.json");
} }

View file

@ -140,7 +140,6 @@ pub struct QuickMapIter<'a, T: Clone> {
impl<'a, T: Clone> Iterator for QuickMapIter<'a, T> { impl<'a, T: Clone> Iterator for QuickMapIter<'a, T> {
type Item = &'a (String, T); type Item = &'a (String, T);
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
self.vec_iter self.vec_iter
.next() .next()

View file

@ -24,16 +24,63 @@ struct StackInner<T: Sized> {
pub data: T pub data: T
} }
impl<T: Clone> Clone for StackInner<T> {
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<T: Sized> (Rc<Option<StackInner<T>>>); struct Stack<T: Sized> (Rc<Option<StackInner<T>>>);
impl<T: Clone> Clone for Stack<T> {
fn clone(&self) -> Self {
Stack(Rc::from((*self.0).clone()))
}
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
struct StackStackInner<T: Sized> { struct StackStackInner<T: Sized> {
next: StackStack<T>, next: StackStack<T>,
count: usize, count: usize,
stack: Stack<T>, stack: Stack<T>,
} }
impl<T: Clone> Clone for StackStackInner<T> {
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<T: Sized> (Rc<Option<StackStackInner<T>>>); pub struct StackStack<T: Sized> (Rc<Option<StackStackInner<T>>>);
impl<T: Clone> Clone for StackStack<T> {
fn clone(&self) -> Self {
StackStack(Rc::from((*self.0).clone()))
}
fn clone_from(&mut self, source: &Self) {
*self = source.clone()
}
}
impl<T> From<T> for StackInner<T> { impl<T> From<T> for StackInner<T> {
fn from(t: T) -> StackInner<T> { fn from(t: T) -> StackInner<T> {
StackInner { StackInner {

View file

@ -25,6 +25,7 @@ use crate::util::{Operand, Program, Address};
use crate::heap::{Gc, Datum, Cons}; use crate::heap::{Gc, Datum, Cons};
use core::ops::DerefMut; use core::ops::DerefMut;
use core::array;
use alloc::vec; use alloc::vec;
use alloc::vec::Vec; use alloc::vec::Vec;
@ -36,12 +37,12 @@ use num::pow::Pow;
const NUM_OPERAND_REGISTERS: usize = 4; const NUM_OPERAND_REGISTERS: usize = 4;
#[derive(Clone)]
pub struct VM { pub struct VM {
// execution environment // execution environment
pub stack: StackStack<Gc<Datum>>, pub stack: StackStack<Gc<Datum>>,
pub symtab: QuickMap<Operand>, pub symtab: QuickMap<Operand>,
pub prog: Program, pub prog: Program,
pub fds: Vec<u64>,
pub traps: Vec<Arc<dyn Fn(&mut VM)>>, pub traps: Vec<Arc<dyn Fn(&mut VM)>>,
// data registers // data registers
@ -58,7 +59,71 @@ pub struct VM {
pub err_state: bool, pub err_state: bool,
} }
impl From<Program> 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 { impl VM {
pub fn new_with_opts(
program: Program,
traps: Option<Vec<Arc<dyn Fn(&mut VM)>>>,
stack: Option<StackStack<Gc<Datum>>>,
syms: Option<QuickMap<Operand>>
) -> VM {
Into::<VM>::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<StackStack<Gc<Datum>>>,
) -> &mut VM {
if let Some(stack) = maybe_stack {
self.stack = stack;
}
self
}
pub fn with_symbols(
&mut self,
maybe_symbols: Option<QuickMap<Operand>>,
) -> &mut VM {
if let Some(symbols) = maybe_symbols {
self.symtab = symbols;
}
self
}
pub fn with_traps(
&mut self,
maybe_traps: Option<Vec<Arc<dyn Fn(&mut VM)>>>,
) -> &mut VM {
if let Some(traps) = maybe_traps {
self.traps = traps;
}
self
}
pub fn run_program(&mut self) { pub fn run_program(&mut self) {
if self.prog.0.len() < 1 { if self.prog.0.len() < 1 {
self.running = false; 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<Vec<(VM, TestResult)>>; 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<Datum>) -> bool {
*(state.stack[idx]) == *target
}
fn has_sym(state: &VM, sym: &str, operand: Option<Operand>) -> 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<Gc<Datum>>,
stack: Vec<(usize, Gc<Datum>)>,
syms: Vec<(&'static str, Option<Operand>)>,
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));
}
}
}
}