From 866830b48501a658511503eb0547d2020b4a5aad Mon Sep 17 00:00:00 2001 From: John Doty Date: Sun, 14 Jan 2024 09:28:05 -0800 Subject: [PATCH] [fine] A wild VM appears! Untested though --- Cargo.lock | 9 +- fine/Cargo.lock | 21 ++ fine/Cargo.toml | 3 + fine/build.rs | 5 + fine/src/compiler.rs | 33 ++-- fine/src/lib.rs | 24 ++- fine/src/vm.rs | 375 +++++++++++++++++++++++++++++++++++- fine/tests/example_tests.rs | 20 ++ 8 files changed, 464 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 509dfde1..bda183bb 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -677,6 +677,7 @@ dependencies = [ "prettyplease", "quote", "syn 2.0.47", + "thiserror", ] [[package]] @@ -2615,18 +2616,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.56" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" dependencies = [ "proc-macro2", "quote", diff --git a/fine/Cargo.lock b/fine/Cargo.lock index 535a9196..89781166 100644 --- a/fine/Cargo.lock +++ b/fine/Cargo.lock @@ -17,6 +17,7 @@ dependencies = [ "prettyplease", "quote", "syn", + "thiserror", ] [[package]] @@ -74,6 +75,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "thiserror" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d54378c645627613241d077a3a79db965db602882668f9136ac42af9ecb730ad" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.56" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa0faa943b50f3db30a20aa7e265dbc66076993efed8463e8de414e5d06d3471" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "unicode-ident" version = "1.0.12" diff --git a/fine/Cargo.toml b/fine/Cargo.toml index 731a7d40..efe87168 100644 --- a/fine/Cargo.toml +++ b/fine/Cargo.toml @@ -11,3 +11,6 @@ glob = "0.3.1" prettyplease = "0.2.16" quote = "1.0.35" syn = "2.0.47" + +[dependencies] +thiserror = "1.0.56" diff --git a/fine/build.rs b/fine/build.rs index 80b3db35..2c73bc92 100644 --- a/fine/build.rs +++ b/fine/build.rs @@ -82,6 +82,11 @@ fn generate_test_for_file(path: PathBuf) -> String { assertions.push(quote! { crate::assert_no_errors(&_tree, &_lines); }); + } else if let Some(line) = line.strip_prefix("@eval:") { + let expected = line.trim(); + assertions.push(quote! { + crate::assert_eval_ok(&_tree, &_lines, #expected); + }); } else if line.starts_with("@") { panic!("Test file {display_path} has unknown directive: {line}"); } diff --git a/fine/src/compiler.rs b/fine/src/compiler.rs index dae8e40f..09518819 100644 --- a/fine/src/compiler.rs +++ b/fine/src/compiler.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::rc::Rc; use crate::{ parser::{SyntaxTree, Tree, TreeKind, TreeRef}, @@ -8,7 +9,7 @@ use crate::{ // TODO: If I were cool this would by actual bytecode. // But I'm not cool. -#[derive(Debug)] +#[derive(Debug, Clone, Copy)] pub enum Instruction { Panic, @@ -32,9 +33,9 @@ pub enum Instruction { StoreLocal(usize), StoreModule(usize), LoadFunction(usize), - LoadExtern(usize), LoadExternFunction(usize), // NOTE: FUNKY, might want to indirect this index. Call(usize), + Return, } pub enum Export { @@ -43,7 +44,7 @@ pub enum Export { } pub struct Module { - pub functions: Vec, // Functions + pub functions: Vec>, // Functions pub globals: usize, // The number of global variables pub exports: HashMap, // Exports by name pub init: usize, // The index of the initialization function @@ -59,7 +60,7 @@ impl Module { } } - pub fn functions(&self) -> &[Function] { + pub fn functions(&self) -> &[Rc] { &self.functions } } @@ -68,7 +69,7 @@ impl Module { pub struct Function { name: String, instructions: Vec, - strings: Vec, + strings: Vec>, args: usize, // TODO: Probably type information too? locals: usize, // TODO: Same? } @@ -96,7 +97,7 @@ impl Function { self.locals } - pub fn strings(&self) -> &[String] { + pub fn strings(&self) -> &[Rc] { &self.strings } @@ -105,6 +106,16 @@ impl Function { } } +impl std::fmt::Debug for Function { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "fn {} ({} args, {} locals) ...", + self.name, self.args, self.locals + ) + } +} + struct Compiler<'a> { semantics: &'a Semantics<'a>, syntax: &'a SyntaxTree<'a>, @@ -123,7 +134,7 @@ impl<'a> Compiler<'a> { fn add_string(&mut self, result: String) -> usize { let index = self.function.strings.len(); - self.function.strings.push(result); + self.function.strings.push(result.into()); index } @@ -195,7 +206,7 @@ macro_rules! ice { // ($compiler:expr, $tr:expr, $($t:tt)*) => {{}}; // } -pub fn compile(semantics: &Semantics) -> Module { +pub fn compile(semantics: &Semantics) -> Rc { let mut compiler = Compiler { semantics, syntax: semantics.tree(), @@ -210,10 +221,10 @@ pub fn compile(semantics: &Semantics) -> Module { let mut module = compiler.module; let index = module.functions.len(); - module.functions.push(compiler.function); + module.functions.push(Rc::new(compiler.function)); module.init = index; - module + Rc::new(module) } fn file(c: &mut Compiler, t: TreeRef) { @@ -565,7 +576,7 @@ fn compile_function_declaration(c: &mut Compiler, t: TreeRef, tree: &Tree, gen_v compile_expression(c, block); std::mem::swap(&mut c.function, &mut prev); - c.module.functions.push(prev); + c.module.functions.push(Rc::new(prev)); } if gen_value { diff --git a/fine/src/lib.rs b/fine/src/lib.rs index d1eb8b8d..78808ee0 100644 --- a/fine/src/lib.rs +++ b/fine/src/lib.rs @@ -1,7 +1,9 @@ use std::fs; +use compiler::compile; use parser::parse; use semantics::{check, Semantics}; +use vm::{eval, Context}; pub mod compiler; pub mod parser; @@ -25,8 +27,24 @@ pub fn process_file(file: &str) { // OK now there might be errors. let mut errors = semantics.snapshot_errors(); - errors.reverse(); - for e in errors { - eprintln!("{file}: {}:{}: {}", e.start.0, e.start.1, e.message); + if errors.len() > 0 { + errors.reverse(); + for e in errors { + eprintln!("{file}: {}:{}: {}", e.start.0, e.start.1, e.message); + } + return; + } + + let module = compile(&semantics); + let main_function = module.functions[module.init].clone(); + + let mut context = Context::new(module.clone()); + match eval(&mut context, main_function, vec![]) { + Ok(v) => { + println!("{:?}", v); + } + Err(e) => { + eprintln!("{:?}", e); + } } } diff --git a/fine/src/vm.rs b/fine/src/vm.rs index d70ba3d6..eb9b6c81 100644 --- a/fine/src/vm.rs +++ b/fine/src/vm.rs @@ -1,12 +1,371 @@ -// use crate::compiler::{Function, Module}; +use std::rc::Rc; -// // TODO: VM state structure -// // TODO: Runtime module vs compiled module +use crate::compiler::{Function, Instruction, Module}; +use crate::semantics::Type; +use thiserror::Error; -// struct StackFrame<'a> { -// function: &'a Function, -// } +#[derive(Error, Debug)] +pub enum VMErrorCode { + #[error("code panic (syntax or semantic error)")] + Panic, + #[error("internal error: stack underflow")] + StackUnderflow, + #[error("internal error: stack type mismatch: {0:?} is not {1:?}")] + StackTypeMismatch(StackValue, Type), -// pub fn eval(module: &Module) { + // TODO: This one is *not* like the others! + #[error("divide by zero")] + DivideByZero, -// } + #[error("internal error: argument {0} out of range")] + ArgumentOutOfRange(usize), + #[error("internal error: global {0} out of range")] + GlobalOutOfRange(usize), + #[error("internal error: local {0} out of range")] + LocalOutOfRange(usize), + #[error("internal error: string {0} out of range")] + StringOutOfRange(usize), + #[error("internal error: function {0} out of range")] + FunctionOutOfRange(usize), + #[error("internal error: stack type mismatch ({0:?} is not function)")] + StackExpectedFunction(StackValue), +} + +#[derive(Debug)] +pub struct VMError { + pub code: VMErrorCode, + pub stack: Box<[Frame]>, +} + +type Result = std::result::Result; + +#[derive(Clone, Debug)] +pub enum StackValue { + Nothing, + Bool(bool), + Float(f64), + String(Rc), + Function(Rc), + ExternFunction(usize), +} + +enum FuncValue { + Function(Rc), + ExternFunction(usize), +} + +#[derive(Debug)] +pub struct Frame { + func: Rc, + args: Vec, + locals: Vec, + stack: Vec, + pc: usize, +} + +impl Frame { + fn from_function(func: Rc, args: Vec) -> Self { + let mut locals = Vec::new(); + locals.resize(func.locals(), StackValue::Nothing); + + Frame { + func, + args, + locals, + stack: Vec::new(), + pc: 0, + } + } + + fn pop_value(&mut self) -> Result { + self.stack.pop().ok_or_else(|| VMErrorCode::StackUnderflow) + } + + fn pop_bool(&mut self) -> Result { + match self.pop_value()? { + StackValue::Bool(v) => Ok(v), + v => Err(VMErrorCode::StackTypeMismatch(v, Type::Bool)), + } + } + + fn pop_float(&mut self) -> Result { + match self.pop_value()? { + StackValue::Float(v) => Ok(v), + v => Err(VMErrorCode::StackTypeMismatch(v, Type::F64)), + } + } + + fn push_value(&mut self, v: StackValue) { + self.stack.push(v) + } + + fn push_bool(&mut self, value: bool) { + self.push_value(StackValue::Bool(value)); + } + + fn push_float(&mut self, value: f64) { + self.push_value(StackValue::Float(value)); + } + + fn push_nothing(&mut self) { + self.push_value(StackValue::Nothing); + } + + fn push_string(&mut self, v: Rc) { + self.push_value(StackValue::String(v)) + } + + fn push_function(&mut self, v: Rc) { + self.push_value(StackValue::Function(v)); + } + + fn push_extern_function(&mut self, v: usize) { + self.push_value(StackValue::ExternFunction(v)); + } + + fn get_argument(&self, i: usize) -> Result { + self.args + .get(i) + .map(|v| v.clone()) + .ok_or_else(|| VMErrorCode::ArgumentOutOfRange(i)) + } + + fn get_local(&self, i: usize) -> Result { + self.locals + .get(i) + .map(|v| v.clone()) + .ok_or_else(|| VMErrorCode::LocalOutOfRange(i)) + } + + fn get_string(&self, i: usize) -> Result> { + let strings = self.func.strings(); + strings + .get(i) + .map(|v| v.clone()) + .ok_or_else(|| VMErrorCode::StringOutOfRange(i)) + } + + fn store_local(&mut self, i: usize, v: StackValue) -> Result<()> { + if i >= self.locals.len() { + Err(VMErrorCode::LocalOutOfRange(i)) + } else { + self.locals[i] = v; + Ok(()) + } + } + + fn pop_function(&mut self) -> Result { + match self.pop_value()? { + StackValue::Function(f) => Ok(FuncValue::Function(f)), + StackValue::ExternFunction(i) => Ok(FuncValue::ExternFunction(i)), + v => Err(VMErrorCode::StackExpectedFunction(v)), + } + } +} + +pub struct Context { + module: Rc, + globals: Vec, +} + +impl Context { + pub fn new(module: Rc) -> Context { + let mut globals = Vec::new(); + globals.resize(module.globals, StackValue::Nothing); + Context { module, globals } + } + + fn get_global(&self, i: usize) -> Result { + self.globals + .get(i) + .map(|v| v.clone()) + .ok_or_else(|| VMErrorCode::GlobalOutOfRange(i)) // TODO: Test + } + + fn set_global(&mut self, i: usize, v: StackValue) -> Result<()> { + if i >= self.globals.len() { + Err(VMErrorCode::GlobalOutOfRange(i)) // TODO: Test + } else { + self.globals[i] = v; + Ok(()) + } + } + + fn get_function(&self, i: usize) -> Result> { + let functions = self.module.functions(); + functions + .get(i) + .map(|v| v.clone()) + .ok_or_else(|| VMErrorCode::FunctionOutOfRange(i)) // TODO: Test + } +} + +enum Flow { + Break, + Continue, +} + +#[inline(always)] +fn eval_one( + instruction: Instruction, + index: &mut usize, + c: &mut Context, + f: &mut Frame, + stack: &mut Vec, +) -> Result { + match instruction { + Instruction::Panic => return Err(VMErrorCode::Panic), + Instruction::BoolNot => { + let value = f.pop_bool()?; + f.push_bool(!value); + } + Instruction::Discard => { + f.pop_value()?; + } + Instruction::FloatAdd => { + let x = f.pop_float()?; + let y = f.pop_float()?; + f.push_float(x + y); + } + Instruction::FloatDivide => { + let x = f.pop_float()?; + let y = f.pop_float()?; + if y == 0. { + return Err(VMErrorCode::DivideByZero); + } + f.push_float(x / y); + } + Instruction::FloatMultiply => { + let x = f.pop_float()?; + let y = f.pop_float()?; + f.push_float(x * y); + } + Instruction::FloatSubtract => { + let x = f.pop_float()?; + let y = f.pop_float()?; + f.push_float(x - y); + } + Instruction::Jump(i) => { + *index = i; + } + Instruction::JumpFalse(i) => { + if !(f.pop_bool()?) { + *index = i; + } + } + Instruction::JumpTrue(i) => { + if f.pop_bool()? { + *index = i; + } + } + Instruction::LoadArgument(i) => { + let v = f.get_argument(i)?; + f.push_value(v); + } + Instruction::LoadLocal(i) => { + let v = f.get_local(i)?; + f.push_value(v); + } + Instruction::LoadModule(i) => { + let v = c.get_global(i)?; + f.push_value(v); + } + Instruction::PushFalse => { + f.push_bool(false); + } + Instruction::PushFloat(v) => { + f.push_float(v); + } + Instruction::PushNothing => { + f.push_nothing(); + } + Instruction::PushString(s) => { + let v = f.get_string(s)?; + f.push_string(v); + } + Instruction::PushTrue => { + f.push_bool(true); + } + Instruction::StoreLocal(i) => { + let v = f.pop_value()?; + f.store_local(i, v)?; + } + Instruction::StoreModule(i) => { + let v = f.pop_value()?; + c.set_global(i, v)?; + } + Instruction::LoadFunction(i) => { + let v = c.get_function(i)?; + f.push_function(v); + } + Instruction::LoadExternFunction(i) => { + f.push_extern_function(i); + } + Instruction::Call(arg_count) => { + let function = f.pop_function()?; + let mut args = Vec::new(); + for _ in 0..arg_count { + args.push(f.pop_value()?); + } + match function { + FuncValue::Function(func) => { + let mut frame = Frame::from_function(func, args); + std::mem::swap(&mut frame, f); + frame.pc = *index; + stack.push(frame); + *index = 0; + } + FuncValue::ExternFunction(_) => todo!(), + } + } + Instruction::Return => match stack.pop() { + Some(mut frame) => { + // The return value is at the top of the stack already. + std::mem::swap(&mut frame, f); + *index = f.pc; + } + None => return Ok(Flow::Break), + }, + } + + Ok(Flow::Continue) +} + +pub fn eval( + c: &mut Context, + function: Rc, + args: Vec, +) -> std::result::Result { + let mut stack = Vec::new(); + let mut f = Frame::from_function(function, args); + + let mut index = 0; + loop { + let instructions = f.func.instructions(); + let instruction = instructions[index]; + index += 1; + + match eval_one(instruction, &mut index, c, &mut f, &mut stack) { + Ok(Flow::Break) => match f.pop_value() { + Ok(v) => return Ok(v), + Err(e) => { + f.pc = index; + stack.push(f); + return Err(VMError { + code: e, + stack: stack.into(), + }); + } + }, + Ok(Flow::Continue) => (), + Err(e) => { + f.pc = index; + stack.push(f); + return Err(VMError { + code: e, + stack: stack.into(), + }); + } + }; + } +} diff --git a/fine/tests/example_tests.rs b/fine/tests/example_tests.rs index 0e747b0e..fa974f48 100644 --- a/fine/tests/example_tests.rs +++ b/fine/tests/example_tests.rs @@ -2,6 +2,7 @@ use fine::compiler::{compile, Function, Module}; use fine::parser::SyntaxTree; use fine::semantics::{check, Error, Semantics, Type}; use fine::tokens::Lines; +use fine::vm::{eval, Context}; use pretty_assertions::assert_eq; use std::fmt::Write as _; @@ -258,4 +259,23 @@ fn assert_no_errors(tree: &SyntaxTree, lines: &Lines) { ); } +#[allow(dead_code)] +fn assert_eval_ok(tree: &SyntaxTree, lines: &Lines, expected: &str) { + let semantics = Semantics::new(tree, lines); + + let module = compile(&semantics); + let main_function = module.functions[module.init].clone(); + + let mut context = Context::new(module.clone()); + match eval(&mut context, main_function, vec![]) { + Ok(v) => { + let actual = format!("{:?}", v); + semantic_assert_eq!(&semantics, None, expected, &actual, "module evaluated"); + } + Err(e) => { + semantic_panic!(&semantics, None, "error occurred while running: {:?}", e); + } + } +} + include!(concat!(env!("OUT_DIR"), "/generated_tests.rs"));