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
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:
parent
ddb49788af
commit
609e65a8db
4 changed files with 312 additions and 3 deletions
|
|
@ -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");
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
260
hyphae/src/vm.rs
260
hyphae/src/vm.rs
|
|
@ -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));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue