♻️ refactor: split out tests and state module

main
_ 2023-09-24 17:40:45 -05:00
parent 8a2807f879
commit 89c3b6e0ca
3 changed files with 383 additions and 376 deletions

View File

@ -1,279 +1,24 @@
use std::collections::BTreeMap;
mod state;
#[derive (Debug)]
enum Instruction {
Add (u8, u8, u8),
Call (u8, u8, u8),
Closure (u8, i32),
// Equals Constant?
EqK (u8, u8, u8),
// Get Table, Upvalue
GetTabUp (u8, u8, u8),
// Get Immediate?
GetI (u8, u8, u8),
// Jump
Jmp (i32),
// Load Integer?
LoadI (u8, i32),
// Load Constant
LoadK (u8, i32),
// MetaMethod, Binary
MmBin (u8, u8, u8),
Move (u8, u8),
// (A, B, _C) Return B - 1 registers starting with A
Return (u8, u8, u8),
VarArgPrep (i32),
}
#[derive (Clone, Debug, PartialEq)]
enum Value {
Nil,
False,
True,
Float (f64),
String (String),
// These are all bogus, I haven't figured out how to implement
// tables and function pointers yet
BogusArg (Vec <String>),
BogusEnv (BTreeMap <String, Value>),
BogusPrint,
}
impl Default for Value {
fn default () -> Self {
Self::Nil
}
}
impl From <String> for Value {
fn from (x: String) -> Self {
Self::String (x)
}
}
impl From <&str> for Value {
fn from (x: &str) -> Self {
Self::from (String::from (x))
}
}
impl From <i32> for Value {
fn from (x: i32) -> Self {
Self::Float (f64::try_from (x).unwrap ())
}
}
impl From <f64> for Value {
fn from (x: f64) -> Self {
Self::Float (x)
}
}
impl Value {
fn as_float (&self) -> Option <f64> {
match self {
Self::Float (x) => Some (*x),
_ => None,
}
}
}
struct Chunk {
instructions: Vec <Instruction>,
constants: Vec <Value>,
}
struct VirtualMachine {
registers: Vec <Value>,
// i32 makes it a little easier to implement jumps
program_counter: i32,
}
impl Default for VirtualMachine {
fn default () -> Self {
Self {
registers: vec! [Value::Nil; 256],
program_counter: 0,
}
}
}
impl VirtualMachine {
fn upvalues_from_args <I: Iterator <Item = String>> (args: I) -> Vec <Value>
{
let arg = args.map (|s| s.to_string ()).collect ();
let env = BTreeMap::from_iter ([
("arg", Value::BogusArg (arg)),
("print", Value::BogusPrint),
].map (|(k, v)| (k.to_string (), v)).into_iter ());
vec! [
Value::BogusEnv (env),
]
}
fn execute_chunk (&mut self, chunk: &Chunk, upvalues: &Vec <Value>)
-> Vec <Value> {
let max_iters = 2000;
for _ in 0..max_iters {
let instruction = chunk.instructions.get (usize::try_from (self.program_counter).unwrap ()).unwrap ();
let r = &mut self.registers;
let k = &chunk.constants;
match instruction {
Instruction::Add (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let c = usize::try_from (*c).unwrap ();
let v_b = r [b].as_float ().unwrap ();
let v_c = r [c].as_float ().unwrap ();
r [a] = (v_b + v_c).into ();
},
Instruction::Call (a, b, c) => {
// Take arguments from registers [a + 1, a + b)
// Call the function in register [a]
// Return values in registers [a, a + c - 1)
//
// That is, call a with b - 1 arguments and expect c returns
//
// e.g. CALL 0 2 1 mean "Call 0 with 1 argument, return 1 value", like for printing a constant
// TODO: Only implement printing values for now
let a = usize::try_from (*a).unwrap ();
assert_eq! (r.get (a).unwrap (), &Value::BogusPrint);
assert_eq! (*b, 2);
assert_eq! (*c, 1);
println! ("{:?}", r.get (a + 1).unwrap ());
},
Instruction::EqK (a, b, c_k) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let equal = r [a] == k [b];
match (equal, c_k) {
(true, 0) => self.program_counter += 1,
(false, 1) => self.program_counter += 1,
_ => (),
}
},
Instruction::GetTabUp (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let c = usize::try_from (*c).unwrap ();
let env = match upvalues.get (b).unwrap () {
Value::BogusEnv (x) => x,
_ => panic! ("Only allowed upvalue is BogusEnv"),
};
let key = match k.get (c).unwrap () {
Value::String (s) => s,
_ => panic! ("GetTabUp only supports string keys"),
};
let value = env.get (key).unwrap ();
r [a] = value.clone ();
},
Instruction::GetI (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let c = usize::try_from (*c).unwrap ();
let table = r.get (b).unwrap ();
let value = match table {
Value::BogusArg (arg) => arg.get (c).map (|x| x.as_str().into ()).unwrap_or_default(),
_ => unimplemented!(),
};
r [a] = value;
},
Instruction::Jmp (s_j) => self.program_counter += s_j,
Instruction::LoadI (a, sbx) => {
let a = usize::try_from (*a).unwrap ();
r [a] = (*sbx).into ();
},
Instruction::LoadK (a, bx) => {
let a = usize::try_from (*a).unwrap ();
let bx = usize::try_from (*bx).unwrap ();
r [a] = k [bx].clone ();
},
Instruction::MmBin (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let a = &r [a];
let b = &r [b];
if a.as_float().is_some() && b.as_float().is_some () {
// No need for metamethods
}
else {
panic! ("Not sure how to implememtn OP_MMBIN for these 2 values {a:?}, {b:?}");
}
},
Instruction::Move (a, b) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
r [a] = r [b].clone ();
},
Instruction::Return (a, b, _c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
return self.registers [a..(a + b - 1)].to_vec();
},
Instruction::VarArgPrep (_) => (),
x => panic! ("Unimplemented instruction {x:?}"),
}
self.program_counter += 1;
}
panic! ("Hit max iterations before chunk returned");
}
}
#[cfg (test)]
mod tests;
fn main() {
let chunk = Chunk {
use state::Instruction as Inst;
use state::State;
let chunk = state::Chunk {
instructions: vec! [
Instruction::VarArgPrep (0),
Instruction::LoadK (0, 0),
Instruction::LoadI (1, 3),
Instruction::Add (2, 0, 1),
Instruction::MmBin (0, 1, 6),
Instruction::GetTabUp (3, 0, 1),
Instruction::Move (4, 2),
Instruction::Call (3, 2, 1),
Instruction::Return (2, 2, 1),
Instruction::Return (3, 1, 1),
Inst::VarArgPrep (0),
Inst::LoadK (0, 0),
Inst::LoadI (1, 3),
Inst::Add (2, 0, 1),
Inst::MmBin (0, 1, 6),
Inst::GetTabUp (3, 0, 1),
Inst::Move (4, 2),
Inst::Call (3, 2, 1),
Inst::Return (2, 2, 1),
Inst::Return (3, 1, 1),
],
constants: vec! [
0.5.into (),
@ -281,109 +26,7 @@ fn main() {
],
};
let upvalues = VirtualMachine::upvalues_from_args (std::env::args ());
let mut vm = VirtualMachine::default ();
let mut vm = State::default ();
let upvalues = State::upvalues_from_args (std::env::args ());
println! ("Returned: {:?}", vm.execute_chunk (&chunk, &upvalues));
}
#[cfg (test)]
mod tests {
use super::*;
#[test]
fn floats () {
/*
local a = 0.5
local b = 3
local x = a + b
print (x)
return x
*/
let chunk = Chunk {
instructions: vec! [
Instruction::VarArgPrep (0),
Instruction::LoadK (0, 0),
Instruction::LoadI (1, 3),
Instruction::Add (2, 0, 1),
Instruction::MmBin (0, 1, 6),
Instruction::GetTabUp (3, 0, 1),
Instruction::Move (4, 2),
Instruction::Call (3, 2, 1),
Instruction::Return (2, 2, 1),
Instruction::Return (3, 1, 1),
],
constants: vec! [
0.5.into (),
"print".into (),
],
};
for (arg, expected) in [
(vec! ["_exe_name"], vec! [3.5.into ()]),
] {
let mut vm = VirtualMachine::default ();
let upvalues = VirtualMachine::upvalues_from_args (arg.into_iter ().map (|s| s.to_string ()));
let actual = vm.execute_chunk (&chunk, &upvalues);
assert_eq! (actual, expected);
}
}
#[test]
fn is_93 () {
/*
if arg [1] == "93" then
print "it's 93"
return 0
else
print "it's not 93"
return 1
end
*/
let chunk = Chunk {
instructions: vec! [
Instruction::VarArgPrep (0),
Instruction::GetTabUp (1, 0, 0),
Instruction::GetI (1, 1, 1),
Instruction::EqK (1, 1, 0),
Instruction::Jmp (6),
Instruction::GetTabUp (1, 0, 2),
Instruction::LoadK (2, 3),
Instruction::Call (1, 2, 1),
Instruction::LoadI (1, 0),
Instruction::Return (1, 2, 1),
Instruction::Jmp (5),
Instruction::GetTabUp (1, 0, 2),
Instruction::LoadK (2, 4),
Instruction::Call (1, 2, 1),
Instruction::LoadI (1, 1),
Instruction::Return (1, 2, 1),
Instruction::Return (1, 1, 1),
],
constants: vec! [
"arg",
"93",
"print",
"it's 93",
"it's not 93",
].into_iter ().map (|s| Value::from (s)).collect (),
};
for (arg, expected) in [
(vec! ["_exe_name"], vec! [1.into ()]),
(vec! ["_exe_name", "93"], vec! [0.into ()]),
(vec! ["_exe_name", "94"], vec! [1.into ()]),
] {
let mut vm = VirtualMachine::default ();
let upvalues = VirtualMachine::upvalues_from_args (arg.into_iter ().map (|s| s.to_string ()));
let actual = vm.execute_chunk (&chunk, &upvalues);
assert_eq! (actual, expected);
}
}
}

262
src/state.rs Normal file
View File

@ -0,0 +1,262 @@
use std::collections::BTreeMap;
#[derive (Debug)]
pub enum Instruction {
Add (u8, u8, u8),
Call (u8, u8, u8),
Closure (u8, i32),
// Equals Constant?
EqK (u8, u8, u8),
// Get Table, Upvalue
GetTabUp (u8, u8, u8),
// Get Immediate?
GetI (u8, u8, u8),
// Jump
Jmp (i32),
// Load Integer?
LoadI (u8, i32),
// Load Constant
LoadK (u8, i32),
// MetaMethod, Binary
MmBin (u8, u8, u8),
Move (u8, u8),
// (A, B, _C) Return B - 1 registers starting with A
Return (u8, u8, u8),
VarArgPrep (i32),
}
#[derive (Clone, Debug, PartialEq)]
pub enum Value {
Nil,
False,
True,
Float (f64),
String (String),
// These are all bogus, I haven't figured out how to implement
// tables and function pointers yet
BogusArg (Vec <String>),
BogusEnv (BTreeMap <String, Value>),
BogusPrint,
}
impl Default for Value {
fn default () -> Self {
Self::Nil
}
}
impl From <String> for Value {
fn from (x: String) -> Self {
Self::String (x)
}
}
impl From <&str> for Value {
fn from (x: &str) -> Self {
Self::from (String::from (x))
}
}
impl From <i32> for Value {
fn from (x: i32) -> Self {
Self::Float (f64::try_from (x).unwrap ())
}
}
impl From <f64> for Value {
fn from (x: f64) -> Self {
Self::Float (x)
}
}
impl Value {
fn as_float (&self) -> Option <f64> {
match self {
Self::Float (x) => Some (*x),
_ => None,
}
}
}
pub struct Chunk {
pub instructions: Vec <Instruction>,
pub constants: Vec <Value>,
}
pub struct State {
registers: Vec <Value>,
// i32 makes it a little easier to implement jumps
program_counter: i32,
}
impl Default for State {
fn default () -> Self {
Self {
registers: vec! [Value::Nil; 256],
program_counter: 0,
}
}
}
impl State {
pub fn upvalues_from_args <I: Iterator <Item = String>> (args: I) -> Vec <Value>
{
let arg = args.map (|s| s.to_string ()).collect ();
let env = BTreeMap::from_iter ([
("arg", Value::BogusArg (arg)),
("print", Value::BogusPrint),
].map (|(k, v)| (k.to_string (), v)).into_iter ());
vec! [
Value::BogusEnv (env),
]
}
pub fn execute_chunk (&mut self, chunk: &Chunk, upvalues: &Vec <Value>)
-> Vec <Value> {
let max_iters = 2000;
for _ in 0..max_iters {
let instruction = chunk.instructions.get (usize::try_from (self.program_counter).unwrap ()).unwrap ();
let r = &mut self.registers;
let k = &chunk.constants;
match instruction {
Instruction::Add (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let c = usize::try_from (*c).unwrap ();
let v_b = r [b].as_float ().unwrap ();
let v_c = r [c].as_float ().unwrap ();
r [a] = (v_b + v_c).into ();
},
Instruction::Call (a, b, c) => {
// Take arguments from registers [a + 1, a + b)
// Call the function in register [a]
// Return values in registers [a, a + c - 1)
//
// That is, call a with b - 1 arguments and expect c returns
//
// e.g. CALL 0 2 1 mean "Call 0 with 1 argument, return 1 value", like for printing a constant
// TODO: Only implement printing values for now
let a = usize::try_from (*a).unwrap ();
assert_eq! (r.get (a).unwrap (), &Value::BogusPrint);
assert_eq! (*b, 2);
assert_eq! (*c, 1);
println! ("{:?}", r.get (a + 1).unwrap ());
},
Instruction::EqK (a, b, c_k) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let equal = r [a] == k [b];
match (equal, c_k) {
(true, 0) => self.program_counter += 1,
(false, 1) => self.program_counter += 1,
_ => (),
}
},
Instruction::GetTabUp (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let c = usize::try_from (*c).unwrap ();
let env = match upvalues.get (b).unwrap () {
Value::BogusEnv (x) => x,
_ => panic! ("Only allowed upvalue is BogusEnv"),
};
let key = match k.get (c).unwrap () {
Value::String (s) => s,
_ => panic! ("GetTabUp only supports string keys"),
};
let value = env.get (key).unwrap ();
r [a] = value.clone ();
},
Instruction::GetI (a, b, c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let c = usize::try_from (*c).unwrap ();
let table = r.get (b).unwrap ();
let value = match table {
Value::BogusArg (arg) => arg.get (c).map (|x| x.as_str().into ()).unwrap_or_default(),
_ => unimplemented!(),
};
r [a] = value;
},
Instruction::Jmp (s_j) => self.program_counter += s_j,
Instruction::LoadI (a, sbx) => {
let a = usize::try_from (*a).unwrap ();
r [a] = (*sbx).into ();
},
Instruction::LoadK (a, bx) => {
let a = usize::try_from (*a).unwrap ();
let bx = usize::try_from (*bx).unwrap ();
r [a] = k [bx].clone ();
},
Instruction::MmBin (a, b, _c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
let a = &r [a];
let b = &r [b];
if a.as_float().is_some() && b.as_float().is_some () {
// No need for metamethods
}
else {
panic! ("Not sure how to implememtn OP_MMBIN for these 2 values {a:?}, {b:?}");
}
},
Instruction::Move (a, b) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
r [a] = r [b].clone ();
},
Instruction::Return (a, b, _c) => {
let a = usize::try_from (*a).unwrap ();
let b = usize::try_from (*b).unwrap ();
return self.registers [a..(a + b - 1)].to_vec();
},
Instruction::VarArgPrep (_) => (),
x => panic! ("Unimplemented instruction {x:?}"),
}
self.program_counter += 1;
}
panic! ("Hit max iterations before chunk returned");
}
}

102
src/tests.rs Normal file
View File

@ -0,0 +1,102 @@
use crate::state::{
Chunk,
Instruction as Inst,
State,
Value,
};
#[test]
fn floats () {
/*
local a = 0.5
local b = 3
local x = a + b
print (x)
return x
*/
let chunk = Chunk {
instructions: vec! [
Inst::VarArgPrep (0),
Inst::LoadK (0, 0),
Inst::LoadI (1, 3),
Inst::Add (2, 0, 1),
Inst::MmBin (0, 1, 6),
Inst::GetTabUp (3, 0, 1),
Inst::Move (4, 2),
Inst::Call (3, 2, 1),
Inst::Return (2, 2, 1),
Inst::Return (3, 1, 1),
],
constants: vec! [
0.5.into (),
"print".into (),
],
};
for (arg, expected) in [
(vec! ["_exe_name"], vec! [3.5.into ()]),
] {
let mut vm = State::default ();
let upvalues = State::upvalues_from_args (arg.into_iter ().map (|s| s.to_string ()));
let actual = vm.execute_chunk (&chunk, &upvalues);
assert_eq! (actual, expected);
}
}
#[test]
fn is_93 () {
/*
if arg [1] == "93" then
print "it's 93"
return 0
else
print "it's not 93"
return 1
end
*/
let chunk = Chunk {
instructions: vec! [
Inst::VarArgPrep (0),
Inst::GetTabUp (1, 0, 0),
Inst::GetI (1, 1, 1),
Inst::EqK (1, 1, 0),
Inst::Jmp (6),
Inst::GetTabUp (1, 0, 2),
Inst::LoadK (2, 3),
Inst::Call (1, 2, 1),
Inst::LoadI (1, 0),
Inst::Return (1, 2, 1),
Inst::Jmp (5),
Inst::GetTabUp (1, 0, 2),
Inst::LoadK (2, 4),
Inst::Call (1, 2, 1),
Inst::LoadI (1, 1),
Inst::Return (1, 2, 1),
Inst::Return (1, 1, 1),
],
constants: vec! [
"arg",
"93",
"print",
"it's 93",
"it's not 93",
].into_iter ().map (|s| Value::from (s)).collect (),
};
for (arg, expected) in [
(vec! ["_exe_name"], vec! [1.into ()]),
(vec! ["_exe_name", "93"], vec! [0.into ()]),
(vec! ["_exe_name", "94"], vec! [1.into ()]),
] {
let mut vm = State::default ();
let upvalues = State::upvalues_from_args (arg.into_iter ().map (|s| s.to_string ()));
let actual = vm.execute_chunk (&chunk, &upvalues);
assert_eq! (actual, expected);
}
}