From 89c3b6e0cab1a91bb49d8763aadc0b179870c01f Mon Sep 17 00:00:00 2001 From: _ <_@_> Date: Sun, 24 Sep 2023 17:40:45 -0500 Subject: [PATCH] :recycle: refactor: split out tests and state module --- src/main.rs | 395 +++------------------------------------------------ src/state.rs | 262 ++++++++++++++++++++++++++++++++++ src/tests.rs | 102 +++++++++++++ 3 files changed, 383 insertions(+), 376 deletions(-) create mode 100644 src/state.rs create mode 100644 src/tests.rs diff --git a/src/main.rs b/src/main.rs index f0a03d3..d125781 100644 --- a/src/main.rs +++ b/src/main.rs @@ -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 ), - BogusEnv (BTreeMap ), - BogusPrint, -} - -impl Default for Value { - fn default () -> Self { - Self::Nil - } -} - -impl From 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 for Value { - fn from (x: i32) -> Self { - Self::Float (f64::try_from (x).unwrap ()) - } -} - -impl From for Value { - fn from (x: f64) -> Self { - Self::Float (x) - } -} - -impl Value { - fn as_float (&self) -> Option { - match self { - Self::Float (x) => Some (*x), - _ => None, - } - } -} - -struct Chunk { - instructions: Vec , - constants: Vec , -} - -struct VirtualMachine { - registers: Vec , - - // 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 > (args: I) -> Vec - { - 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 ) - -> Vec { - 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); - } - } -} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..b7b3a68 --- /dev/null +++ b/src/state.rs @@ -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 ), + BogusEnv (BTreeMap ), + BogusPrint, +} + +impl Default for Value { + fn default () -> Self { + Self::Nil + } +} + +impl From 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 for Value { + fn from (x: i32) -> Self { + Self::Float (f64::try_from (x).unwrap ()) + } +} + +impl From for Value { + fn from (x: f64) -> Self { + Self::Float (x) + } +} + +impl Value { + fn as_float (&self) -> Option { + match self { + Self::Float (x) => Some (*x), + _ => None, + } + } +} + +pub struct Chunk { + pub instructions: Vec , + pub constants: Vec , +} + +pub struct State { + registers: Vec , + + // 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 > (args: I) -> Vec + { + 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 ) + -> Vec { + 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"); + } +} diff --git a/src/tests.rs b/src/tests.rs new file mode 100644 index 0000000..491b09d --- /dev/null +++ b/src/tests.rs @@ -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); + } +} \ No newline at end of file