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

@ -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<Gc<Datum>>,
pub symtab: QuickMap<Operand>,
pub prog: Program,
pub fds: Vec<u64>,
pub traps: Vec<Arc<dyn Fn(&mut VM)>>,
// data registers
@ -58,7 +59,71 @@ pub struct VM {
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 {
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) {
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<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));
}
}
}
}