From ac05959042dcb5368c78a5bd1426bbb8e53c5edd Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Sat, 11 Nov 2023 15:59:04 -0600 Subject: [PATCH] A big refactor, step 3 --- forge_core/src/compiler.rs | 1444 ----------------- .../src/compiler/ast_nodes/assignment.rs | 17 + forge_core/src/compiler/ast_nodes/block.rs | 209 +++ forge_core/src/compiler/ast_nodes/call.rs | 93 ++ forge_core/src/compiler/ast_nodes/const.rs | 149 ++ .../src/compiler/ast_nodes/declaration.rs | 16 + forge_core/src/compiler/ast_nodes/expr.rs | 222 +++ forge_core/src/compiler/ast_nodes/function.rs | 64 + .../compiler/ast_nodes/function_prototype.rs | 31 + forge_core/src/compiler/ast_nodes/global.rs | 43 + forge_core/src/compiler/ast_nodes/lvalue.rs | 64 + forge_core/src/compiler/ast_nodes/mod.rs | 14 + forge_core/src/compiler/ast_nodes/program.rs | 14 + .../src/compiler/ast_nodes/repeat_loop.rs | 173 ++ forge_core/src/compiler/ast_nodes/return.rs | 66 + forge_core/src/compiler/ast_nodes/var_decl.rs | 54 + forge_core/src/compiler/compilable.rs | 18 + forge_core/src/compiler/compile_error.rs | 17 + forge_core/src/compiler/compiled_fn.rs | 142 ++ forge_core/src/compiler/mod.rs | 94 ++ forge_core/src/compiler/state.rs | 79 + forge_core/src/compiler/test_utils.rs | 16 + forge_core/src/compiler/utils.rs | 31 + .../src/parser/ast_nodes/conditional.rs | 2 +- forge_core/src/parser/ast_nodes/mod.rs | 1 - forge_core/src/parser/ast_nodes/program.rs | 2 +- .../src/parser/ast_nodes/repeat_loop.rs | 2 +- forge_core/src/parser/ast_nodes/while_loop.rs | 2 +- forge_core/src/parser/mod.rs | 21 +- .../src/parser/{ast_nodes => }/test_utils.rs | 0 vcore/src/cpu.rs | 6 +- vemu/src/main.rs | 4 +- vtest/src/forge_tests.rs | 3 +- vtest/src/lib.rs | 2 + 34 files changed, 1649 insertions(+), 1466 deletions(-) delete mode 100644 forge_core/src/compiler.rs create mode 100644 forge_core/src/compiler/ast_nodes/assignment.rs create mode 100644 forge_core/src/compiler/ast_nodes/block.rs create mode 100644 forge_core/src/compiler/ast_nodes/call.rs create mode 100644 forge_core/src/compiler/ast_nodes/const.rs create mode 100644 forge_core/src/compiler/ast_nodes/declaration.rs create mode 100644 forge_core/src/compiler/ast_nodes/expr.rs create mode 100644 forge_core/src/compiler/ast_nodes/function.rs create mode 100644 forge_core/src/compiler/ast_nodes/function_prototype.rs create mode 100644 forge_core/src/compiler/ast_nodes/global.rs create mode 100644 forge_core/src/compiler/ast_nodes/lvalue.rs create mode 100644 forge_core/src/compiler/ast_nodes/mod.rs create mode 100644 forge_core/src/compiler/ast_nodes/program.rs create mode 100644 forge_core/src/compiler/ast_nodes/repeat_loop.rs create mode 100644 forge_core/src/compiler/ast_nodes/return.rs create mode 100644 forge_core/src/compiler/ast_nodes/var_decl.rs create mode 100644 forge_core/src/compiler/compilable.rs create mode 100644 forge_core/src/compiler/compile_error.rs create mode 100644 forge_core/src/compiler/compiled_fn.rs create mode 100644 forge_core/src/compiler/mod.rs create mode 100644 forge_core/src/compiler/state.rs create mode 100644 forge_core/src/compiler/test_utils.rs create mode 100644 forge_core/src/compiler/utils.rs rename forge_core/src/parser/{ast_nodes => }/test_utils.rs (100%) diff --git a/forge_core/src/compiler.rs b/forge_core/src/compiler.rs deleted file mode 100644 index 3860306..0000000 --- a/forge_core/src/compiler.rs +++ /dev/null @@ -1,1444 +0,0 @@ -use crate::ast::*; - -use std::collections::btree_map::Entry::Vacant; -use std::collections::{BTreeMap}; -use std::fmt::{Display, Formatter}; -use crate::parser::{ParseError, parse}; - -#[derive(Eq, Clone, PartialEq, Debug)] -pub struct CompileError(pub usize, pub usize, pub String); - -impl From for CompileError { - fn from(value: ParseError) -> Self { - Self(value.0, value.1, value.2) - } -} - -impl Display for CompileError { - fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { - write!(f, "({}:{}) {}", self.0, self.1, self.2) - } -} - -/// A variable, of various different types: -/// - Literals have static values and can just be pushed to the stack -/// - IndirectLabels contain the label pointing to the value (think `foo: .db 0`) -/// - Direct labels are the value themselves (think `call foo`) -/// - Locals contain an index into the local frame -#[derive(Clone, PartialEq, Debug)] -pub enum Variable { - Literal(i32), - IndirectLabel(String), - DirectLabel(String), - Local(usize), -} - -/// The data associated with a function signature: -/// - The frame size is in bytes -/// - The local scope is full of `Variable::Local`s, and contains args and local vars -/// - The body is just the statements of the function, not the code to move the frame -/// pointer around (which can't be generated until the variable declarations are all found) -/// -/// A complete function implementation consists of: -/// - A label for the entrypoint -/// - The function body (including preamble code to set up the stack frame) -/// -/// The stack frame is managed by the function through a global pointer called "frame". When -/// the function is called, it can assume that all memory after "frame" is free for use (this -/// isn't actually true because you can blow out the stack, but within reason it is). So when -/// you make a call to another function, you need to increment frame by the current frame size, -/// and then after the other function has returned, decrement it back so that frame again points -/// at your stack frame. Locals can be found by adding some offset from the frame pointer. -/// -/// todo: the allocator problem has (probably) been solved! Make an alloca() that increases the -/// current frame pointer by some size. To dynamically allocate memory, just put it in the stack -/// frame of the current fn. All fns return one word and all params are one word long (structs -/// get passed around by reference) -/// -/// todo: more of a global todo. Add a register that stores an offset that's added implicitly to -/// all absolute addresses. This makes it a lot simpler to make relocatable code -/// -/// alternate todo: add a callr instruction. This moves the basic unit of linking up from "fn" to -/// "library". Near calls are callr, far calls are call. Globals, reserve a word in the zero page -/// for a global frame pointer, save / restore that around a far call (using the rstack). -/// -/// Weird todo: add an abs (and maybe rel) instruction that converts a relative address on the -/// stack to an absolute one by adding the instruction pointer (of the abs / rel). Make room for it -/// by replacing sdp / setsdp / setint with just reg / setreg, which will push / pop register -/// values to the stack, given a register index (0 for data ptr, 1 for rstack, 2 for int enabled, -/// whatever). For a second removable opcode, how often is peekr actually used? -/// -/// Another instruction todo: remove the copy instruction (currently a dumb copy-region command) -/// and replace it with the design from last year with the mode argument... but as a DMA "device" -/// with an interface in the zero page. -#[derive(Clone, PartialEq, Debug, Default)] -pub struct CompiledFn { - pub label: Label, - pub frame_size: usize, - pub local_scope: Scope, - pub arity: usize, - pub body: Vec, -} - -impl CompiledFn { - /// Add a name to the local scope, which: - /// - Increases the size of the stack frame by that much - /// - Records the offset into the local stack frame where that variable is stored - fn add_local(&mut self, name: &str) -> Result<(), CompileError> { - if let Vacant(e) = self.local_scope.entry(name.into()) { - e.insert(Variable::Local(self.frame_size)); - self.frame_size += 3; - Ok(()) - } else { - Err(CompileError(0, 0, format!("Duplicate name {}", name))) - } - } - - /// Emit a string (ideally one instruction, but whatever) to the function body. - /// This doesn't actually emit anything to output, the body will eventually be emitted - /// in a final pass by the compiler once all functions are compiled. - fn emit(&mut self, opcode: &str) { - self.body.push(String::from(opcode)) - } - - /// A shorthand method to emit something with a `Display` arg, because emitting a - /// single instruction with a variable (numeric or label) arg is very common. - fn emit_arg(&mut self, opcode: &str, arg: T) { - self.body.push(format!("{} {}", opcode, arg)) - } - - /// The size of the local scope in bytes. This increases as variables are declared - /// todo: this needs to change for arrays; it won't like having vars that aren't 3 bytes long - fn frame_size(&self) -> usize { - self.local_scope.len() * 3 - } - - /// Emit the code to add the arguments' names to the local scopr, and move the arguments off - /// the stack into the frame. Should be run before the body is compiled, because this code - /// has to be the first thing in the body - fn handle_args(&mut self, function: &Function) -> Result<(), CompileError> { - let mut arg_names: Vec<&str> = Vec::new(); - - // Add each argument as a local - for name in function.args.iter() { - self.add_local(name.as_str())?; - arg_names.push(name.as_str()); - self.arity += 1; - } - - // Arguments in calls are pushed with the last on top, so, reverse the vec just to make - // the following loop easier: - arg_names.reverse(); - - for name in arg_names { - if let Variable::Local(offset) = self.local_scope[name] { - self.emit("loadw frame"); - if offset != 0 { - self.emit_arg("add", offset); - } - self.emit("storew"); - } - } - - Ok(()) - } - - /// Takes a new frame size and removes all names from the local scope that would be placed - /// beyond that frame length: used when compiling blocks to de-scope names that should only - /// be visible in the block - fn reduce_frame_size_to(&mut self, new_size: usize) { - self.frame_size = new_size; - let mut to_remove: Vec = Vec::new(); - for (name, var) in self.local_scope.iter() { - if let Variable::Local(offset) = var { - if *offset >= self.frame_size { - to_remove.push(name.clone()); - } - } - } - - for name in to_remove { - self.local_scope.remove(name.as_str()); - } - } - - /// Reduces the frame size by one word, to throw out the most recently declared local. - /// Used for things like repeat loops where there's a name declared outside a block but which - /// should only be visible in the block anyway. - fn forget_last_local(&mut self) { - self.reduce_frame_size_to(self.frame_size - 3); - } -} - -/// Maps from names to the variables they represent -pub type Scope = BTreeMap; - -/// So we don't get confused between string-strings and assembly-label strings -pub type Label = String; - -/// The compiler state: -#[derive(Clone, PartialEq, Debug, Default)] -struct State { - /// Used by gensym to generate unique symbols - pub gensym_index: usize, - /// The globally-defined names - pub global_scope: Scope, - /// The functions - pub functions: BTreeMap, - /// The string table - pub strings: Vec<(Label, String)>, - /// The functions that have been prototyped but not yet defined - pub prototypes: Scope, -} - -impl State { - /// Generate a guaranteed-unique symbolic name - fn gensym(&mut self) -> Label { - self.gensym_index += 1; - format!("_forge_gensym_{}", self.gensym_index) - } - - /// Return whether a name exists in the global scope already - fn defined(&self, name: &str) -> bool { - self.global_scope.contains_key(name) - } - - /// Add a symbol to the global namespace, catching name collisions - fn add_global Variable>( - &mut self, - name: &str, - val: F, - ) -> Result<(), CompileError> { - if self.defined(name) { - Err(CompileError(0, 0, format!("name {} already defined", name))) - } else { - let val = val(self); - self.global_scope.insert(name.into(), val); - Ok(()) - } - } - - fn add_string(&mut self, string: &str) -> Label { - let sym = self.gensym(); - self.strings.push((sym.clone(), string.into())); - sym - } - - fn declare_function(&mut self, name: &str, _args: Vec) -> Result<(), CompileError> { - // If it's not already prototyped, gensym a label and put it in the list. If it is, just - // ignore this (we don't check arity so it's not like the arglist being different matters) - // todo: check arity, store it here - if !self.prototypes.contains_key(name) { - let label = self.gensym(); - self.add_global(name, |_| Variable::DirectLabel(label.clone()))?; - self.prototypes.insert(name.into(), Variable::DirectLabel(label)); - } - Ok(()) - } - - fn find_or_declare_function(&mut self, name: &str, _loc: Location) -> Result { - // If it's in here, remove it and return the label: - // (this will only ever match this way; only thing that puts stuff in prototypes adds directlabels) - if let Some(Variable::DirectLabel(label)) = self.prototypes.remove(name) { - Ok(label) - } else { - // Otherwise, make a new label, add it to globals, and return it - let label = self.gensym(); - self.add_global(name, |_| Variable::DirectLabel(label.clone()))?; - Ok(label) - } - } -} - -trait Compilable { - // Every AST node we visit can see the global compiler state (to make unique - // symbols and reach for global names) as well as optionally the current - // function (for AST nodes within functions). Additionally se send a Location: - // This is the closest ancestor's location, in case we need to emit a compile - // error - fn process( - self, - state: &mut State, - function: Option<&mut CompiledFn>, - location: Location, - ) -> Result<(), CompileError>; -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Program { - fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { - for decl in self.0 { - decl.ast.process(state, None, decl.location)? - } - Ok(()) - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Declaration { - fn process(self, state: &mut State, _: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - match self { - Declaration::Function(f) => f.process(state, None, loc), - Declaration::Global(g) => g.process(state, None, loc), - Declaration::Const(c) => c.process(state, None, loc), - Declaration::Prototype(p) => p.process(state, None, loc), - } - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Block { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { - let sig = sig.expect("Block outside function"); - - let frame_size_before_block = sig.frame_size; - - // Compile each statement: - for stmt in self.0 { - let loc = stmt.location; - match stmt.ast { - Statement::Return(Return(None)) => { - // Returning nothing, so just default to returning a 0: - sig.emit_arg("ret", 0) - } - Statement::Return(Return(Some(expr))) => { - // Eval the expr and emit a ret for it - expr.process(state, Some(sig), loc)?; - sig.emit("ret") - } - Statement::Assignment(assign) => assign.process(state, Some(sig), loc)?, - Statement::Expr(expr) => { - expr.process(state, Some(sig), loc)?; - // Every expr leaves a single-word return value on the stack. In an rvalue this - // is useful but in a statement it's garbage (because nothing else is about to - // pick it up) so, drop it: - sig.emit("pop") - } - Statement::VarDecl(vardecl) => vardecl.process(state, Some(sig), loc)?, - Statement::Asm(Asm { args, body }) => { - // Process all the args, if any - for a in args { - (*a.0).process(state, Some(sig), loc)? - } - // Emit the body - sig.emit(body.as_str()) - } - Statement::Conditional(Conditional { condition, body, alternative }) => { - condition.process(state, Some(sig), loc)?; - sig.emit("#if"); // We went to a lot of trouble making macros, shame not to use them - body.process(state, Some(sig), loc)?; - if let Some(alternative) = alternative { - sig.emit("#else"); - alternative.process(state, Some(sig), loc)?; - } - sig.emit("#end") - } - Statement::WhileLoop(WhileLoop { condition, body }) => { - sig.emit("#while"); - condition.process(state, Some(sig), loc)?; - sig.emit("#do"); - body.process(state, Some(sig), loc)?; - sig.emit("#end") - } - Statement::RepeatLoop(repeat_loop) => { - repeat_loop.process(state, Some(sig), loc)?; - } - } - } - - // If we declared anything inside the block, we'll know that because the frame size will - // have increased, so, simply blow away those names to make them block scoped. This doesn't - // give us shadowing, it's not a true scope chain, but this is much simpler and fixes some - // common cases where you want that. - sig.reduce_frame_size_to(frame_size_before_block); - - Ok(()) - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for RepeatLoop { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - let sig = sig.expect("Repeat loop outside function"); - - let RepeatLoop { count, name, body } = self; - let named_counter = name.is_some(); - let counter_name = name.unwrap_or_default(); - - if named_counter { - let decl = VarDecl { - name: counter_name.clone(), - size: None, - initial: Some(Expr::Number(0)), - }; - decl.process(state, Some(sig), loc)?; - } - - // Okay, the counter (if present) is now declared and in scope; we'll eval the limit once - count.process(state, Some(sig), loc)?; - - // Now we have a fairly normal while loop: - sig.emit("#while"); - // Stack currently has the limit on top. We need to cmp the counter to that. - sig.emit("dup"); - - // Load the counter, if present: - if named_counter { - Expr::Name(counter_name.clone()).process(state, Some(sig), loc)?; - // Subtract the counter from (the copy of) the limit. - sig.emit("sub"); - } - - // cmp that to zero, for our flag: this is either limit - ctr or limit, depending if - // there's a counter - sig.emit_arg("agt", 0); - - // Loop body: - sig.emit("#do"); - body.process(state, Some(sig), loc)?; - - // After the body we need to increment the counter if there is one - if named_counter { - // Put its address on top: - Expr::Address(Expr::Name(counter_name.clone()).into()).process(state, Some(sig), loc)?; - // Dup and load it: - sig.emit("dup"); - sig.emit("loadw"); - // Increment it: - sig.emit_arg("add", 1); - // Now we have ( counted-addr new-ctr-val ) so swap and store - sig.emit("swap"); - sig.emit("storew"); - } else { - // No counter, so just decrement the limit, which is already on top: - sig.emit_arg("sub", 1); - } - - // Back to the check! - sig.emit("#end"); - // Done with the loop, but the limit is still on the stack, pop it: - sig.emit("pop"); - - // If we have added a counter variable, we need to forget that because (even - // though declared outside the block) it should be scoped to the block: - if named_counter { - sig.forget_last_local(); - } - - Ok(()) - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Function { - fn process(self, state: &mut State, _: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - let label = state.find_or_declare_function(self.name.as_str(), loc)?; - - // The CompiledFn for this function, which will eventually get stuff populated into it: - let mut sig = CompiledFn { - label, - ..Default::default() - }; - - // This generates the code to copy the args into the frame as well as adds all the arguments - // to the local scope and calculates the arity. - sig.handle_args(&self)?; - - // todo we need to store arity somehow in the state, so we can check arglists even - // with recursive calls. This probably becomes splitting "signature" from "function context" - // and setting signature immutably right now, passing context down ("block?") and setting it - // at the end - - // Compile the body, storing all of it in the CompiledFn we just created / added - self.body.process(state, Some(&mut sig), loc)?; - - // This can't fail because if it were a dupe name, adding the global would have failed - state.functions.insert(self.name.clone(), sig); - - // And we're done: the function body now stores everything we need to emit that function... - // but we can't actually emit it yet because we don't know how we're building things. This - // is the equivalent of the object-file step in a more real compiler. - Ok(()) - } -} - -impl Compilable for FunctionPrototype { - fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { - state.declare_function(self.name.as_str(), self.args) - } -} - -/// Look up a name first in the local scope, and failing that in the global scope. -fn lookup<'a>(name: &str, global_scope: &'a Scope, local_scope: &'a Scope) -> Option<&'a Variable> { - if let Some(var) = local_scope.get(name) { - Some(var) - } else if let Some(var) = global_scope.get(name) { - Some(var) - } else { - None - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Assignment { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - let Assignment { lvalue, rvalue } = self; - let sig = sig.expect("Assignment outside function"); - // First the value, then the address we'll storew it to - rvalue.process(state, Some(sig), loc)?; - lvalue.process(state, Some(sig), loc)?; - sig.emit("storew"); - Ok(()) - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for VarDecl { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - let sig = sig.expect("Var declaration outside function"); - if self.size.is_some() { - todo!("Arrays are not yet supported") - } - if let Some(initial) = self.initial { - // If it's got an initial value, we have to compile that before we add - // the name to scope, or else UB will ensue if it refers to itself: - initial.process(state, Some(sig), loc)?; - // But then add it to scope and assign: - sig.add_local(&self.name)?; - // We'll just whip up an lvalue real quick... - Lvalue::from(self.name).process(state, Some(sig), loc)?; - sig.emit("storew"); // And store the initial value there - } else { - // Otherwise, just add it to scope and leave garbage in there: - sig.add_local(&self.name)?; - } - Ok(()) - } -} - -/////////////////////////////////////////////////////////// - -/// Evaluate an lvalue and leave its address on the stack (ready to be consumed by storew) -impl Compilable for Lvalue { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - let global_scope = &state.global_scope; - let sig = sig.expect("lvalue outside a function"); - - match *(self.0.0) { - // A bunch of different things that aren't allowed - Expr::Number(_) | - Expr::Neg(_) | - Expr::Not(_) | - Expr::Address(_) | - Expr::Call(_) | - Expr::Infix(_, _, _) | - Expr::String(_) => { - Err(CompileError(0, 0, String::from("Not a valid lvalue"))) - } - - // Names we look up and leave the address on the stack: - Expr::Name(name) => { - if let Some(var) = lookup(&name, global_scope, &sig.local_scope) { - match var { - Variable::Literal(_) | Variable::DirectLabel(_) => { - // Direct labels are (probably) functions, the important part is the - // label itself, which we can't alter, so, error: - Err(CompileError(0, 0, format!("Invalid lvalue {}", name))) - } - Variable::IndirectLabel(label) => { - // Indirect labels are variables, the label is where the data is stored, - // so we push that label so we can store stuff there - let label = label.clone(); - sig.emit_arg("push", label); - Ok(()) - } - Variable::Local(offset) => { - let offset = *offset; - sig.emit_arg("loadw", "frame"); - if offset > 0 { - sig.emit_arg("add", offset); - } - Ok(()) - } - } - } else { - Err(CompileError(0, 0, format!("Unknown name {}", name))) - } - } - // Derefs are just evaluating the expr and leaving its value (an address) on the stack - Expr::Deref(BoxExpr(expr)) => { - (*expr).process(state, Some(sig), loc) - } - Expr::Subscript(_, _) => { - Err(CompileError(0, 0, String::from("Arrays are not yet supported"))) - } - } - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Call { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - // Require a function - let sig = sig.expect("lvalue outside a function"); - - // We need a stack consisting of the arguments (last on top), followed by the address to call - // So, first eval the args: - for arg in self.args { - arg.process(state, Some(sig), loc)?; - } - - // Eval the target - self.target.0.process(state, Some(sig), loc)?; - - // Before we actually do the call though, we need to deal with some paperwork around the - // frame pointer. We will store the current frame pointer in the rstack: - sig.emit("loadw frame"); - sig.emit("pushr"); - - // Now we increment the frame ptr to right after the current frame: - let frame_size = sig.frame_size(); - if frame_size > 0 { - sig.emit("loadw frame"); - sig.emit_arg("add", frame_size); - sig.emit("storew frame"); - } - - // Frame is now pointing at a safe place, the top of stack is the target, do the call: - sig.emit("call"); - - // And this is where we'll return to. The function has popped its args and left a word on - // the stack as a return value, which someone else will deal with (this is the word that - // this expr::call will end up evaluating to). But before we're done, we need to restore - // our frame pointerS - sig.emit("popr"); - sig.emit("storew frame"); - - // And we're finished! - Ok(()) - } -} - -/////////////////////////////////////////////////////////// - -/// Evaluate an expression in the context of a local scope. The runtime brother to eval_const. -/// This recursively evaluates a Node and leaves its value on the stack. -impl Compilable for Expr { - fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { - let mut sig = sig.expect("Non-const expression outside a function"); - let global_scope = &state.global_scope; - - // First, a sanity check: try and eval_const this. If it's something incredibly basic - // that just becomes an i32, then we don't need to do anything else: - if let Ok(val) = eval_const(self.clone(), &state.global_scope) { - sig.emit_arg("push", val); - return Ok(()); - } - - // Okay, looks like we need something that's in scope. Let's recurse: - match self { - Expr::Number(n) => { - // Numbers are just pushed as literals - sig.body.push(format!("push {}", n)); - Ok(()) - } - Expr::Name(name) => { - match lookup(&name, global_scope, &sig.local_scope) { - // Names are treated differently depending on what they are - Some(Variable::Literal(val)) => { - // Names of constants are just that number - sig.emit_arg("push", *val); - Ok(()) - } - Some(Variable::IndirectLabel(label)) => { - // Names pointing at labels are loaded (rvalue; for lvalues they aren't) - // Indirect labels are the address of where the value is stored (a var, .db) - sig.emit_arg("loadw", label.clone()); - Ok(()) - } - Some(Variable::DirectLabel(label)) => { - // Direct labels are like functions, the label itself is the value, so just - // push it: - sig.emit_arg("push", label.clone()); - Ok(()) - } - Some(Variable::Local(offset)) => { - // Names of locals are added from the frame pointer - let offset = *offset; - sig.emit("loadw frame"); - if offset > 0 { - sig.emit_arg("add", offset); - } - sig.emit("loadw"); - Ok(()) - } - None => Err(CompileError(0, 0, format!("Unknown name {}", name))), - } - } - Expr::Neg(e) => { - (*e.0).process(state, Some(sig), loc)?; - // To arithmetically negate something, invert and increment (2s complement) - sig.emit("xor -1"); - sig.emit("add 1"); - Ok(()) - } - Expr::Not(e) => { - (*e.0).process(state, Some(sig), loc)?; - sig.emit("not"); - Ok(()) - } - // Handling addresses is very easy because processing an lvalue leaves the address on the stack - Expr::Address(lvalue) => lvalue.process(state, Some(sig), loc), - Expr::Deref(BoxExpr(e)) => { - (*e).process(state, Some(sig), loc)?; - sig.emit("loadw"); - Ok(()) - } - Expr::String(string) => { - let label = state.add_string(&string); - sig.emit_arg("push", label); - Ok(()) - } - Expr::Call(call) => call.process(state, Some(sig), loc), - Expr::Subscript(_, _) => todo!("Structs and arrays are not yet supported"), - Expr::Infix(lhs, op, rhs) => { - // Recurse on expressions, handling operators - (*lhs.0).process(state, Some(&mut sig), loc)?; - (*rhs.0).process(state, Some(&mut sig), loc)?; - match op { - // Basic math - Operator::Add => sig.emit("add"), - Operator::Sub => sig.emit("sub"), - Operator::Mul => sig.emit("mul"), - Operator::Div => sig.emit("div"), - Operator::Mod => sig.emit("mod"), - Operator::And => { - // Vulcan "and" is bitwise, so we need to flag-ify both args to make it logical - sig.emit("gt 0"); - sig.emit("swap"); - sig.emit("gt 0"); - sig.emit("and"); - } - Operator::Or => { - // Same as and, flag-ify both args - sig.emit("gt 0"); - sig.emit("swap"); - sig.emit("gt 0"); - sig.emit("or"); - } - Operator::BitAnd => sig.emit("and"), - Operator::BitOr => sig.emit("or"), - Operator::Xor => sig.emit("xor"), - Operator::Lt => sig.emit("alt"), - Operator::Le => { - // LE and GE are the inverses of GT and LT (arithmetic versions) - sig.emit("agt"); - sig.emit("not"); - } - Operator::Gt => sig.emit("agt"), - Operator::Ge => { - sig.emit("alt"); - sig.emit("not"); - } - Operator::Eq => { - sig.emit("xor"); - sig.emit("not"); - } - Operator::Ne => sig.emit("xor"), - Operator::Lshift => sig.emit("lshift"), - Operator::Rshift => sig.emit("arshift"), - } - Ok(()) - } - } - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Global { - fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { - if self.size.is_some() { - todo!("Arrays are not yet supported") - } - state.add_global(&self.name, |s| Variable::IndirectLabel(s.gensym())) - } -} - -/////////////////////////////////////////////////////////// - -impl Compilable for Const { - fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { - let var = if self.string.is_some() { - // If it's a string, add it to the string table - Variable::DirectLabel(state.add_string(&self.string.unwrap())) - } else if let Some(expr) = self.value { - // Otherwise eval_const it - Variable::Literal(eval_const(expr, &state.global_scope)?) - } else { - unreachable!() - }; - // Add it to the global namespace - state.add_global(&self.name, |_| var.clone()) - } -} - -/////////////////////////////////////////////////////////// - -fn to_flag(val: bool) -> i32 { - if val { - 1 - } else { - 0 - } -} - -/// Evaluate a node in a static context, for const definitions and array sizes, that sort of thing. -pub fn eval_const(expr: Expr, scope: &Scope) -> Result { - match expr { - Expr::Number(n) => Ok(n), // That was easy - Expr::Name(n) => { - if let Some(Variable::Literal(val)) = scope.get(&n) { - Ok(*val) - } else { - Err(CompileError(0, 0, format!("Unknown const {}", n))) - } - } - Expr::Neg(e) => { - let val = eval_const(*e.0, scope)?; - Ok(-val) - } - Expr::Not(e) => { - let val = eval_const(*e.0, scope)?; - Ok(if val != 0 { 0 } else { 1 }) - } - // A note about Expr::String here: 'const foo="banana"' won't hit this point; it'll be - // (currently) parsed as a special case of const. The only things that will hit this are - // const expressions that include strings, like '"foo"[2]' or something. Those can't be - // (generally) calculated at compile time, so, they're an error. - Expr::Address(_) | Expr::Deref(_) | Expr::String(_) => Err(CompileError( - 0, - 0, - String::from("Addresses are not known at compile time"), - )), - Expr::Call(_) | Expr::Subscript(_, _) => Err(CompileError( - 0, - 0, - String::from("Constants must be statically defined"), - )), - Expr::Infix(lhs, op, rhs) => { - let lhs = eval_const(*lhs.0, scope)?; - let rhs = eval_const(*rhs.0, scope)?; - match op { - Operator::Add => Ok(lhs + rhs), - Operator::Sub => Ok(lhs - rhs), - Operator::Mul => Ok(lhs * rhs), - Operator::Div => Ok(lhs / rhs), - Operator::Mod => Ok(lhs % rhs), - Operator::And => Ok(to_flag(lhs != 0 && rhs != 0)), - Operator::Or => Ok(to_flag(lhs != 0 || rhs != 0)), - Operator::BitAnd => Ok(lhs & rhs), - Operator::BitOr => Ok(lhs | rhs), - Operator::Xor => Ok(lhs ^ rhs), - Operator::Lt => Ok(to_flag(lhs < rhs)), - Operator::Le => Ok(to_flag(lhs <= rhs)), - Operator::Gt => Ok(to_flag(lhs > rhs)), - Operator::Ge => Ok(to_flag(lhs >= rhs)), - Operator::Eq => Ok(to_flag(lhs == rhs)), - Operator::Ne => Ok(to_flag(lhs != rhs)), - Operator::Lshift => Ok(lhs << rhs), - Operator::Rshift => Ok(lhs >> rhs), - } - } - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -/// Turn a `Program` into a list of assembly lines that can be assembled to run it. -/// There are various ways to build a program, this one is the simplest: it builds it as a -/// complete ROM that will place a jmp to main() at 0x400, so a Vulcan can boot from it. -pub fn build_boot(src: &str) -> Result, CompileError> { - let mut state = State::default(); - let ast = parse(src).map_err(CompileError::from)?; - ast.process(&mut state, None, (0, 0).into())?; - - if let Some(Variable::DirectLabel(label)) = state.global_scope.get("main") { - // Let's make a vec for the final listing - let mut asm: Vec = Vec::new(); - - // Now we start piling stuff into the vec, starting with an org: - asm.push(".org 0x400".into()); - - // jmp into main: - asm.push(format!("call {}", label)); - - // When main returns, just hlt: - asm.push("hlt".into()); - - // Now start dumping compiled objects into there: - for (label, val) in state.strings.iter() { - asm.push(format!("{}: .db \"{}\\0\"", label, val)) - } - for (_, val) in state.functions.iter_mut() { - asm.push(format!("{}:", val.label)); - asm.append(val.body.as_mut()); - } - - // Final thing is to place a label for the stack: - // (this is just a cell with the address of the following word) - asm.push("frame: .db $+1".into()); - asm.push(".db 0".into()); - Ok(asm) - } else { - Err(CompileError(0, 0, "Function main not defined (or not a function)".into())) - } -} - -/////////////////////////////////////////////////////////////////////////////////////////////////// - -#[cfg(test)] -mod test { - use super::*; - use crate::parser::parse; - - #[test] - fn test_eval_const() { - let empty_scope = Scope::new(); - let to_expr = |s| Expr::parse(s).unwrap(); - - // Basic arithmetic - assert_eq!(eval_const(to_expr("2 * 3 + 4"), &empty_scope), Ok(10)); - assert_eq!(eval_const(to_expr("1 + -2"), &empty_scope), Ok(-1)); - assert_eq!(eval_const(to_expr("1 << 3"), &empty_scope), Ok(8)); - - // Names - let scope: Scope = [ - ("foo".into(), Variable::Literal(10)), - ("bar".into(), Variable::Literal(5)), - ] - .into(); - assert_eq!(eval_const(to_expr("foo + 5"), &scope), Ok(15)); - assert_eq!(eval_const(to_expr("bar * foo"), &scope), Ok(50)); - - // Error - assert_eq!( - eval_const(to_expr("nope"), &scope), - Err(CompileError(0, 0, String::from("Unknown const nope"))) - ); - } - - #[test] - fn test_const_decl() { - let mut state = State::default(); - parse("const foo = 17 + 3;") - .unwrap() - .process(&mut state, None, (0, 0).into()) - .unwrap(); - assert_eq!( - state.global_scope, - [("foo".into(), Variable::Literal(20))].into() - ) - } - - #[test] - fn test_string_const() { - let mut state = State::default(); - parse("const foo = \"bar\";") - .unwrap() - .process(&mut state, None, (0, 0).into()) - .unwrap(); - assert_eq!( - state.global_scope, - [("foo".into(), Variable::DirectLabel("_forge_gensym_1".into()))].into() - ) - } - - #[test] - fn test_global_decl() { - let mut state = State::default(); - parse("global a;") - .unwrap() - .process(&mut state, None, (0, 0).into()) - .unwrap(); - assert_eq!( - state.global_scope, - [("a".into(), Variable::IndirectLabel("_forge_gensym_1".into()))].into() - ) - } - - #[test] - fn test_name_collision() { - let mut state = State::default(); - assert!(parse("const a = 7; global a;") - .unwrap() - .process(&mut state, None, (0, 0).into()) - .is_err()); - } - - fn state_for(src: &str) -> State { - let mut state = State::default(); - parse(src) - .unwrap() - .process(&mut state, None, (0, 0).into()) - .expect("Failed to compile"); - state - } - - fn test_body(state: State) -> String { - state.functions.get("test").unwrap().body.join("\n") - } - - #[test] - fn test_basic_fns() { - assert_eq!( - test_body(state_for("fn test(a, b) { b = 17 + a; }")), - vec![ - "loadw frame", // Capture var b - "add 3", - "storew", - "loadw frame", // Capture var a - "storew", - "push 17", // Start calculating the rvalue, push the literal - "loadw frame", // This is looking up the "a" arg, at frame + 0 - "loadw", - "add", // 17 + a - "loadw frame", // Calculate the lvalue - "add 3", // "b" arg is frame + 3 - "storew", // Finally store - ] - .join("\n") - ) - } - - #[test] - fn test_var_decls() { - assert_eq!( - test_body(state_for("fn test() { var a; var b = 7; a = b * 2; }")), - vec![ - "push 7", // Start calculating the rvalue, push the literal - "loadw frame", // "b" is the second local var at frame + 3 - "add 3", - "storew", // Do the initialization - "loadw frame", // Now we're evaluating b * 2 - "add 3", // b is at frame + 3... - "loadw", // load it to the stack - "push 2", - "mul", // b * 2 evaluated - "loadw frame", // Loading "a" as an lvalue - "storew", // doing the assignment - ] - .join("\n") - ) - } - - #[test] - fn test_literal_strings() { - let mut state = state_for("const s1 = \"foo\"; fn test() { var x; x = \"bar\"; var y = \"norp\"; }"); - assert_eq!( - state.strings, - vec![ - ("_forge_gensym_1".into(), "foo".into()), - ("_forge_gensym_3".into(), "bar".into()), // gensym 2 is the entrypoint of blah() - ("_forge_gensym_4".into(), "norp".into()), - ] - ); - assert_eq!( - test_body(state), - vec![ - "push _forge_gensym_3", - "loadw frame", - "storew", // the assignment for x - "push _forge_gensym_4", - "loadw frame", - "add 3", // the address of y (frame + 3) and put gensym_4 in it - "storew", - ] - .join("\n") - ) - } - - #[test] - fn test_addresses() { - assert_eq!( - test_body(state_for("fn test() { var x = 3; var y = &x; }")), - vec![ - "push 3", - "loadw frame", - "storew", // the assignment for x - "loadw frame", // The addr of x - "loadw frame", - "add 3", // the address of y (frame + 3) and put the addr of x (frame) in it - "storew", - ] - .join("\n") - ) - } - - #[test] - fn test_derefs() { - assert_eq!( - test_body(state_for("fn test() { var x = 3; *1000 = *x; }")), - vec![ - "push 3", - "loadw frame", - "storew", // the assignment for x - "loadw frame", // Now we're compiling *x, so load x's value, which is 3 - "loadw", - "loadw", // Then load the value at 3 - "push 1000", // Push the addr 1000, for the lvalue - "storew", // Store whatever's at 3 to 1000 - ] - .join("\n") - ) - } - - #[test] - fn test_string_exprs() { - assert_eq!( - test_body(state_for("const foo = \"foo\"; fn test() { var x = \"bar\" + 3; }")), - vec![ - "push _forge_gensym_3", // 1 is the label in the string table for "foo", 2 for "blah," - "push 3", // so 3 is the string "bar" - "add", // Add 3 to that address - "loadw frame", // Store it in the first var - "storew", - ] - .join("\n") - ) - } - - #[test] - fn test_asm_statements() { - assert_eq!( - test_body(state_for("fn test() { var x; asm(&x) { swap 12\nstorew } }")), - vec![ - "loadw frame", // Push the addr of x - "swap 12", // The asm body, which swaps 12 behind it and stores it there - "storew", - ] - .join("\n") - ) - } - - #[test] - fn test_conditionals() { - assert_eq!( - test_body(state_for("fn test() { var x = 3; if (x > 2) { x = 1; } }")), - vec![ - "push 3", - "loadw frame", - "storew", // x = 3 - "loadw frame", - "loadw", - "push 2", - "agt", // The condition, x > 2 - "#if", // The if itself - "push 1", - "loadw frame", - "storew", // The branch, x = 1 - "#end"] - .join("\n") - ); - - assert_eq!( - test_body(state_for("fn test() { var x = 3; if (x > 2) { x = 1; } else { x = 7; } }")), - vec![ - "push 3", - "loadw frame", - "storew", // x = 3 - "loadw frame", - "loadw", - "push 2", - "agt", // The condition, x > 2 - "#if", // The if itself - "push 1", - "loadw frame", - "storew", // The affirmative branch, x = 1 - "#else", // The alternative - "push 7", - "loadw frame", - "storew", // x = 7 - "#end"] - .join("\n") - ) - } - - #[test] - fn test_while_loops() { - assert_eq!( - test_body(state_for("fn test() { var x = 0; var c = 0; while (c < 10) { x = x + c; c = c + 1; } }")), - vec![ - "push 0", - "loadw frame", - "storew", // var x = 0 - "push 0", - "loadw frame", - "add 3", - "storew", // var c = 0 - "#while", // Start the loop - "loadw frame", - "add 3", - "loadw", - "push 10", - "alt", // c > 10 - "#do", // Start loop body - "loadw frame", - "loadw", - "loadw frame", - "add 3", - "loadw", - "add", - "loadw frame", - "storew", // x = x + c - "loadw frame", - "add 3", - "loadw", - "push 1", - "add", - "loadw frame", - "add 3", - "storew", // c = c + 1 - "#end", // End the loop body - ] - .join("\n") - ); - } - - #[test] - fn test_function_prototypes() { - let mut state = state_for("fn blah(a, b);"); - assert_eq!(state.functions.get("blah"), None); - assert_eq!(state.prototypes.remove("blah"), Some(Variable::DirectLabel(String::from("_forge_gensym_1")))); - } - - #[test] - fn test_functions_with_prototypes() { - let mut state = state_for("fn blah(a, b); const foo = 3; fn blah(a, b) { return 7; }"); - assert_eq!(state.functions.remove("blah").unwrap().label, String::from("_forge_gensym_1")); - assert_eq!(state.prototypes.remove("blah"), None); - } - - #[test] - fn test_calls() { - assert_eq!( - test_body(state_for("fn test(a, b) { test(2, 3); }")), - vec![ - "loadw frame", // capture arg b - "add 3", - "storew", - "loadw frame", // capture arg a - "storew", - "push 2", // evaluating args, in order - "push 3", - "push _forge_gensym_1", // evaluating target (this fn) - "loadw frame", // Store the frame ptr - "pushr", - "loadw frame", // Increment the frame ptr - "add 6", - "storew frame", - "call", // Actually make the call - "popr", // Restore the frame ptr - "storew frame", - "pop", // expr-as-statement drops the evaluated value - ] - .join("\n") - ); - } - - #[test] - fn test_returns() { - assert_eq!( - test_body(state_for("fn test(a) { return a + 3; }")), - vec![ - "loadw frame", // capture arg a - "storew", - "loadw frame", // Load a - "loadw", - "push 3", // Add 3 - "add", - "ret", // Return that - ] - .join("\n") - ); - - assert_eq!( - test_body(state_for("fn test(a) { if (a > 0) { return; } }")), - vec![ - "loadw frame", // capture arg a - "storew", - "loadw frame", // Load a - "loadw", - "push 0", // Compare to 0 - "agt", - "#if", // If statement - "ret 0", // Default return value, for an expr-less return - "#end", - ] - .join("\n") - ); - } - - #[test] - fn test_repeat_loops() { - // With a counter - assert_eq!( - test_body(state_for("fn test(a) { var x = 0; repeat(a) c { x = x + c; } return x; }")), - vec![ - "loadw frame", // capture arg a - "storew", - "push 0", // Create the 'x' var and store 0 in it - "loadw frame", - "add 3", - "storew", - "push 0", // Create the 'c' var and store 0 in it - "loadw frame", - "add 6", - "storew", - "loadw frame", // Load 'a' from args - "loadw", - "#while", // Starting the loop: - "dup", // Copy the limit - "loadw frame", // Load c - "add 6", - "loadw", - "sub", // Subtract c from a - "agt 0", // Are we still positive? - "#do", - "loadw frame", // Load x - "add 3", - "loadw", - "loadw frame", // Load c - "add 6", - "loadw", - "add", // Add c to x - "loadw frame", // Load x as an lvalue - "add 3", - "storew", // Store c + x into it - "loadw frame", // Load c as an lvalue - "add 6", - "dup", // Dup it, load it, add 1 - "loadw", - "add 1", - "swap", // Swap the addr on top and store it - "storew", - "#end", // End of the loop body! - "pop", // Drop the limit off the top - "loadw frame", // Load x so we can return it - "add 3", - "loadw", - "ret", - ] - .join("\n") - ); - - // No counter - assert_eq!( - test_body(state_for("fn test(a) { var x = 1; repeat(a) { x = x * 2; } return x; }")), - vec![ - "loadw frame", // capture arg a - "storew", - "push 1", // Create the 'x' var and store 1 in it - "loadw frame", - "add 3", - "storew", - "loadw frame", // Load 'a' from args - "loadw", - "#while", // Starting the loop: - "dup", // Copy the limit - "agt 0", // Are we still positive? - "#do", - "loadw frame", // Load x - "add 3", - "loadw", - "push 2", // double it - "mul", - "loadw frame", // Load x as an lvalue - "add 3", - "storew", // Store 2x into it - "sub 1", // decrement the counter - "#end", // End of the loop body! - "pop", // Drop the limit off the top - "loadw frame", // Load x so we can return it - "add 3", - "loadw", - "ret", - ].join("\n") - ); - } - - #[test] - fn test_build_boot() { - // Very basic test of one main() - let asm = build_boot("fn main() { return 5; }".into()).unwrap(); - assert_eq!(asm.join("\n"), vec![ - ".org 0x400", - "call _forge_gensym_1", - "hlt", - "_forge_gensym_1:", - "push 5", - "ret", - "frame: .db $+1", - ".db 0", - ].join("\n")); - - // Slightly more complicated, with a global str - let asm = build_boot("const str = \"blah\"; fn main() { return str; }".into()).unwrap(); - assert_eq!(asm.join("\n"), vec![ - ".org 0x400", - "call _forge_gensym_2", - "hlt", - "_forge_gensym_1: .db \"blah\\0\"", - "_forge_gensym_2:", - "push _forge_gensym_1", - "ret", - "frame: .db $+1", - ".db 0", - ].join("\n")) - } - - #[test] - fn test_block_scoping() { - assert_eq!( - test_body(state_for("fn test() { repeat(5) c { 2; } var k = 5; return k; }")), - vec![ - "push 0", // Create the 'c' var and store 0 in it - "loadw frame", - "storew", - "push 5", - "#while", // Starting the loop: - "dup", // Copy the limit - "loadw frame", // Load c - "loadw", - "sub", // Subtract c from a - "agt 0", // Are we still positive? - "#do", - "push 2", // Pointless loop body - "pop", - "loadw frame", // Load c as an lvalue - "dup", // Dup it, load it, add 1 - "loadw", - "add 1", - "swap", // Swap the addr on top and store it - "storew", - "#end", // End of the loop body! - "pop", // Drop the limit off the top - "push 5", // Push the rvalue we'll put in 'k' - "loadw frame", // THIS IS THE TEST: 'k' should go at frame + 0, because it's - "storew", // taking the same (now freed) frame slot that c took, because c is - "loadw frame", // now out of scope - "loadw", - "ret", - ] - .join("\n") - ); - } -} diff --git a/forge_core/src/compiler/ast_nodes/assignment.rs b/forge_core/src/compiler/ast_nodes/assignment.rs new file mode 100644 index 0000000..eea0d21 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/assignment.rs @@ -0,0 +1,17 @@ +use crate::ast::{Assignment, Location}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for Assignment { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let Assignment { lvalue, rvalue } = self; + let sig = sig.expect("Assignment outside function"); + // First the value, then the address we'll storew it to + rvalue.process(state, Some(sig), loc)?; + lvalue.process(state, Some(sig), loc)?; + sig.emit("storew"); + Ok(()) + } +} diff --git a/forge_core/src/compiler/ast_nodes/block.rs b/forge_core/src/compiler/ast_nodes/block.rs new file mode 100644 index 0000000..3b3c0d8 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/block.rs @@ -0,0 +1,209 @@ +use crate::ast::{Asm, Block, Conditional, Location, Statement, WhileLoop}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for Block { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { + let sig = sig.expect("Block outside function"); + + let frame_size_before_block = sig.frame_size; + + // Compile each statement: + for stmt in self.0 { + let loc = stmt.location; + match stmt.ast { + Statement::Return(ret) => { + ret.process(state, Some(sig), loc)?; + } + Statement::Assignment(assign) => assign.process(state, Some(sig), loc)?, + Statement::Expr(expr) => { + expr.process(state, Some(sig), loc)?; + // Every expr leaves a single-word return value on the stack. In an rvalue this + // is useful but in a statement it's garbage (because nothing else is about to + // pick it up) so, drop it: + sig.emit("pop") + } + Statement::VarDecl(vardecl) => vardecl.process(state, Some(sig), loc)?, + Statement::Asm(Asm { args, body }) => { + // Process all the args, if any + for a in args { + (*a.0).process(state, Some(sig), loc)? + } + // Emit the body + sig.emit(body.as_str()) + } + Statement::Conditional(Conditional { condition, body, alternative }) => { + // todo split into new file + condition.process(state, Some(sig), loc)?; + sig.emit("#if"); // We went to a lot of trouble making macros, shame not to use them + body.process(state, Some(sig), loc)?; + if let Some(alternative) = alternative { + sig.emit("#else"); + alternative.process(state, Some(sig), loc)?; + } + sig.emit("#end") + } + Statement::WhileLoop(WhileLoop { condition, body }) => { + // todo split into new file + sig.emit("#while"); + condition.process(state, Some(sig), loc)?; + sig.emit("#do"); + body.process(state, Some(sig), loc)?; + sig.emit("#end") + } + Statement::RepeatLoop(repeat_loop) => { + repeat_loop.process(state, Some(sig), loc)?; + } + } + } + + // If we declared anything inside the block, we'll know that because the frame size will + // have increased, so, simply blow away those names to make them block scoped. This doesn't + // give us shadowing, it's not a true scope chain, but this is much simpler and fixes some + // common cases where you want that. + sig.reduce_frame_size_to(frame_size_before_block); + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + + #[test] + fn test_asm_statements() { + assert_eq!( + test_body(state_for("fn test() { var x; asm(&x) { swap 12\nstorew } }")), + vec![ + "loadw frame", // Push the addr of x + "swap 12", // The asm body, which swaps 12 behind it and stores it there + "storew", + ] + .join("\n") + ) + } + + #[test] + fn test_conditionals() { + assert_eq!( + test_body(state_for("fn test() { var x = 3; if (x > 2) { x = 1; } }")), + vec![ + "push 3", + "loadw frame", + "storew", // x = 3 + "loadw frame", + "loadw", + "push 2", + "agt", // The condition, x > 2 + "#if", // The if itself + "push 1", + "loadw frame", + "storew", // The branch, x = 1 + "#end"] + .join("\n") + ); + + assert_eq!( + test_body(state_for("fn test() { var x = 3; if (x > 2) { x = 1; } else { x = 7; } }")), + vec![ + "push 3", + "loadw frame", + "storew", // x = 3 + "loadw frame", + "loadw", + "push 2", + "agt", // The condition, x > 2 + "#if", // The if itself + "push 1", + "loadw frame", + "storew", // The affirmative branch, x = 1 + "#else", // The alternative + "push 7", + "loadw frame", + "storew", // x = 7 + "#end"] + .join("\n") + ) + } + + #[test] + fn test_while_loops() { + assert_eq!( + test_body(state_for("fn test() { var x = 0; var c = 0; while (c < 10) { x = x + c; c = c + 1; } }")), + vec![ + "push 0", + "loadw frame", + "storew", // var x = 0 + "push 0", + "loadw frame", + "add 3", + "storew", // var c = 0 + "#while", // Start the loop + "loadw frame", + "add 3", + "loadw", + "push 10", + "alt", // c > 10 + "#do", // Start loop body + "loadw frame", + "loadw", + "loadw frame", + "add 3", + "loadw", + "add", + "loadw frame", + "storew", // x = x + c + "loadw frame", + "add 3", + "loadw", + "push 1", + "add", + "loadw frame", + "add 3", + "storew", // c = c + 1 + "#end", // End the loop body + ] + .join("\n") + ); + } + + #[test] + fn test_block_scoping() { + assert_eq!( + test_body(state_for("fn test() { repeat(5) c { 2; } var k = 5; return k; }")), + vec![ + "push 0", // Create the 'c' var and store 0 in it + "loadw frame", + "storew", + "push 5", + "#while", // Starting the loop: + "dup", // Copy the limit + "loadw frame", // Load c + "loadw", + "sub", // Subtract c from a + "agt 0", // Are we still positive? + "#do", + "push 2", // Pointless loop body + "pop", + "loadw frame", // Load c as an lvalue + "dup", // Dup it, load it, add 1 + "loadw", + "add 1", + "swap", // Swap the addr on top and store it + "storew", + "#end", // End of the loop body! + "pop", // Drop the limit off the top + "push 5", // Push the rvalue we'll put in 'k' + "loadw frame", // THIS IS THE TEST: 'k' should go at frame + 0, because it's + "storew", // taking the same (now freed) frame slot that c took, because c is + "loadw frame", // now out of scope + "loadw", + "ret", + ] + .join("\n") + ); + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/call.rs b/forge_core/src/compiler/ast_nodes/call.rs new file mode 100644 index 0000000..f172391 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/call.rs @@ -0,0 +1,93 @@ +use crate::ast::{Call, Location}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for Call { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + // Require a function + let sig = sig.expect("lvalue outside a function"); + + // We need a stack consisting of the arguments (last on top), followed by the address to call + // So, first eval the args: + for arg in self.args { + arg.process(state, Some(sig), loc)?; + } + + // Eval the target + self.target.0.process(state, Some(sig), loc)?; + + // Before we actually do the call though, we need to deal with some paperwork around the + // frame pointer. We will store the current frame pointer in the rstack: + sig.emit("loadw frame"); + sig.emit("pushr"); + + // todo this makes arrays not work. we can no longer statically compute the frame size, we + // need to have the frame size stored at runtime somehow, probably rstack. This will mean + // every fn will stick the frame ptr it's given in the rstack in its preamble, and that any + // vardecl will increment that ptr (that the top of the rstack is the end of the current + // frame, essentially). Returns also need to popr/pop before doing the actual ret. + // todo also for that matter, that means that the frame ptr no longer needs to be a global + // symbol: have it be stored in the rstack and passed as the first param in a call. So the + // calling convention is, push the args, push the new fn's frame ptr (current top-of-frame+1), + // call. The receiving fn puts its frame ptr into the rstack, dups it there, puts the args + // (it knows its own arity) into its frame, and then every vardecl grabs the 2nd thing from r, + // increments it by whatever, and puts it back. + // todo in fact, a larger refactor would just store the frame size as the first thing in the + // frame, to make vardecls faster + + // Now we increment the frame ptr to right after the current frame: + let frame_size = sig.frame_size(); + if frame_size > 0 { + sig.emit("loadw frame"); + sig.emit_arg("add", frame_size); + sig.emit("storew frame"); + } + + // Frame is now pointing at a safe place, the top of stack is the target, do the call: + sig.emit("call"); + + // And this is where we'll return to. The function has popped its args and left a word on + // the stack as a return value, which someone else will deal with (this is the word that + // this expr::call will end up evaluating to). But before we're done, we need to restore + // our frame pointerS + sig.emit("popr"); + sig.emit("storew frame"); + + // And we're finished! + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + + #[test] + fn test_calls() { + assert_eq!( + test_body(state_for("fn test(a, b) { test(2, 3); }")), + vec![ + "loadw frame", // capture arg b + "add 3", + "storew", + "loadw frame", // capture arg a + "storew", + "push 2", // evaluating args, in order + "push 3", + "push _forge_gensym_1", // evaluating target (this fn) + "loadw frame", // Store the frame ptr + "pushr", + "loadw frame", // Increment the frame ptr + "add 6", + "storew frame", + "call", // Actually make the call + "popr", // Restore the frame ptr + "storew frame", + "pop", // expr-as-statement drops the evaluated value + ] + .join("\n") + ); + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/const.rs b/forge_core/src/compiler/ast_nodes/const.rs new file mode 100644 index 0000000..10e624a --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/const.rs @@ -0,0 +1,149 @@ +use crate::ast::{Const, Expr, Location, Operator}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; +use crate::compiler::utils::{Scope, Variable}; + +impl Compilable for Const { + fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { + let var = if self.string.is_some() { + // If it's a string, add it to the string table + Variable::DirectLabel(state.add_string(&self.string.unwrap())) + } else if let Some(expr) = self.value { + // Otherwise eval_const it + Variable::Literal(eval_const(expr, &state.global_scope)?) + } else { + unreachable!() + }; + // Add it to the global namespace + state.add_global(&self.name, |_| var.clone()) + } +} + +/// Evaluate a node in a static context, for const definitions and array sizes, that sort of thing. +pub fn eval_const(expr: Expr, scope: &Scope) -> Result { + match expr { + Expr::Number(n) => Ok(n), // That was easy + Expr::Name(n) => { + if let Some(Variable::Literal(val)) = scope.get(&n) { + Ok(*val) + } else { + Err(CompileError(0, 0, format!("Unknown const {}", n))) + } + } + Expr::Neg(e) => { + let val = eval_const(*e.0, scope)?; + Ok(-val) + } + Expr::Not(e) => { + let val = eval_const(*e.0, scope)?; + Ok(if val != 0 { 0 } else { 1 }) + } + // A note about Expr::String here: 'const foo="banana"' won't hit this point; it'll be + // (currently) parsed as a special case of const. The only things that will hit this are + // const expressions that include strings, like '"foo"[2]' or something. Those can't be + // (generally) calculated at compile time, so, they're an error. + Expr::Address(_) | Expr::Deref(_) | Expr::String(_) => Err(CompileError( + 0, + 0, + String::from("Addresses are not known at compile time"), + )), + Expr::Call(_) | Expr::Subscript(_, _) => Err(CompileError( + 0, + 0, + String::from("Constants must be statically defined"), + )), + Expr::Infix(lhs, op, rhs) => { + let lhs = eval_const(*lhs.0, scope)?; + let rhs = eval_const(*rhs.0, scope)?; + match op { + Operator::Add => Ok(lhs + rhs), + Operator::Sub => Ok(lhs - rhs), + Operator::Mul => Ok(lhs * rhs), + Operator::Div => Ok(lhs / rhs), + Operator::Mod => Ok(lhs % rhs), + Operator::And => Ok(to_flag(lhs != 0 && rhs != 0)), + Operator::Or => Ok(to_flag(lhs != 0 || rhs != 0)), + Operator::BitAnd => Ok(lhs & rhs), + Operator::BitOr => Ok(lhs | rhs), + Operator::Xor => Ok(lhs ^ rhs), + Operator::Lt => Ok(to_flag(lhs < rhs)), + Operator::Le => Ok(to_flag(lhs <= rhs)), + Operator::Gt => Ok(to_flag(lhs > rhs)), + Operator::Ge => Ok(to_flag(lhs >= rhs)), + Operator::Eq => Ok(to_flag(lhs == rhs)), + Operator::Ne => Ok(to_flag(lhs != rhs)), + Operator::Lshift => Ok(lhs << rhs), + Operator::Rshift => Ok(lhs >> rhs), + } + } + } +} + +/// Vulcan sees boolean flags as 1 or 0, Rust has an actual bool type. Bridge the gap: +fn to_flag(val: bool) -> i32 { + if val { + 1 + } else { + 0 + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::parser::parse; + + #[test] + fn test_eval_const() { + let empty_scope = Scope::new(); + let to_expr = |s| Expr::parse(s).unwrap(); + + // Basic arithmetic + assert_eq!(eval_const(to_expr("2 * 3 + 4"), &empty_scope), Ok(10)); + assert_eq!(eval_const(to_expr("1 + -2"), &empty_scope), Ok(-1)); + assert_eq!(eval_const(to_expr("1 << 3"), &empty_scope), Ok(8)); + + // Names + let scope: Scope = [ + ("foo".into(), Variable::Literal(10)), + ("bar".into(), Variable::Literal(5)), + ] + .into(); + assert_eq!(eval_const(to_expr("foo + 5"), &scope), Ok(15)); + assert_eq!(eval_const(to_expr("bar * foo"), &scope), Ok(50)); + + // Error + assert_eq!( + eval_const(to_expr("nope"), &scope), + Err(CompileError(0, 0, String::from("Unknown const nope"))) + ); + } + + #[test] + fn test_const_decl() { + let mut state = State::default(); + parse("const foo = 17 + 3;") + .unwrap() + .process(&mut state, None, (0, 0).into()) + .unwrap(); + assert_eq!( + state.global_scope, + [("foo".into(), Variable::Literal(20))].into() + ) + } + + #[test] + fn test_string_const() { + let mut state = State::default(); + parse("const foo = \"bar\";") + .unwrap() + .process(&mut state, None, (0, 0).into()) + .unwrap(); + assert_eq!( + state.global_scope, + [("foo".into(), Variable::DirectLabel("_forge_gensym_1".into()))].into() + ) + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/declaration.rs b/forge_core/src/compiler/ast_nodes/declaration.rs new file mode 100644 index 0000000..ee5507b --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/declaration.rs @@ -0,0 +1,16 @@ +use crate::ast::{Declaration, Location}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for Declaration { + fn process(self, state: &mut State, _: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + match self { + Declaration::Function(f) => f.process(state, None, loc), + Declaration::Global(g) => g.process(state, None, loc), + Declaration::Const(c) => c.process(state, None, loc), + Declaration::Prototype(p) => p.process(state, None, loc), + } + } +} diff --git a/forge_core/src/compiler/ast_nodes/expr.rs b/forge_core/src/compiler/ast_nodes/expr.rs new file mode 100644 index 0000000..afb1ed2 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/expr.rs @@ -0,0 +1,222 @@ +use crate::ast::{BoxExpr, Expr, Location, Operator}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::{CompileError}; +use crate::compiler::ast_nodes::r#const::eval_const; +use crate::compiler::state::State; +use crate::compiler::utils::{lookup, Variable}; + +/// Evaluate an expression in the context of a local scope. The runtime brother to eval_const. +/// This recursively evaluates a Node and leaves its value on the stack. +impl Compilable for Expr { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let mut sig = sig.expect("Non-const expression outside a function"); + let global_scope = &state.global_scope; + + // First, a sanity check: try and eval_const this. If it's something incredibly basic + // that just becomes an i32, then we don't need to do anything else: + if let Ok(val) = eval_const(self.clone(), &state.global_scope) { + sig.emit_arg("push", val); + return Ok(()); + } + + // Okay, looks like we need something that's in scope. Let's recurse: + match self { + Expr::Number(n) => { + // Numbers are just pushed as literals + sig.body.push(format!("push {}", n)); + Ok(()) + } + Expr::Name(name) => { + // todo split into new file + match lookup(&name, global_scope, &sig.local_scope) { + // Names are treated differently depending on what they are + Some(Variable::Literal(val)) => { + // Names of constants are just that number + sig.emit_arg("push", *val); + Ok(()) + } + Some(Variable::IndirectLabel(label)) => { + // Names pointing at labels are loaded (rvalue; for lvalues they aren't) + // Indirect labels are the address of where the value is stored (a var, .db) + sig.emit_arg("loadw", label.clone()); + Ok(()) + } + Some(Variable::DirectLabel(label)) => { + // Direct labels are like functions, the label itself is the value, so just + // push it: + sig.emit_arg("push", label.clone()); + Ok(()) + } + Some(Variable::Local(offset)) => { + // Names of locals are added from the frame pointer + let offset = *offset; + sig.emit("loadw frame"); + if offset > 0 { + sig.emit_arg("add", offset); + } + sig.emit("loadw"); + Ok(()) + } + None => Err(CompileError(0, 0, format!("Unknown name {}", name))), + } + } + Expr::Neg(e) => { + (*e.0).process(state, Some(sig), loc)?; + // To arithmetically negate something, invert and increment (2s complement) + sig.emit("xor -1"); + sig.emit("add 1"); + Ok(()) + } + Expr::Not(e) => { + (*e.0).process(state, Some(sig), loc)?; + sig.emit("not"); + Ok(()) + } + // Handling addresses is very easy because processing an lvalue leaves the address on the stack + Expr::Address(lvalue) => lvalue.process(state, Some(sig), loc), + Expr::Deref(BoxExpr(e)) => { + (*e).process(state, Some(sig), loc)?; + sig.emit("loadw"); + Ok(()) + } + Expr::String(string) => { + let label = state.add_string(&string); + sig.emit_arg("push", label); + Ok(()) + } + Expr::Call(call) => call.process(state, Some(sig), loc), + Expr::Subscript(_, _) => todo!("Structs and arrays are not yet supported"), + Expr::Infix(lhs, op, rhs) => { + // Recurse on expressions, handling operators + (*lhs.0).process(state, Some(&mut sig), loc)?; + (*rhs.0).process(state, Some(&mut sig), loc)?; + match op { + // Basic math + Operator::Add => sig.emit("add"), + Operator::Sub => sig.emit("sub"), + Operator::Mul => sig.emit("mul"), + Operator::Div => sig.emit("div"), + Operator::Mod => sig.emit("mod"), + Operator::And => { + // Vulcan "and" is bitwise, so we need to flag-ify both args to make it logical + sig.emit("gt 0"); + sig.emit("swap"); + sig.emit("gt 0"); + sig.emit("and"); + } + Operator::Or => { + // Same as and, flag-ify both args + sig.emit("gt 0"); + sig.emit("swap"); + sig.emit("gt 0"); + sig.emit("or"); + } + Operator::BitAnd => sig.emit("and"), + Operator::BitOr => sig.emit("or"), + Operator::Xor => sig.emit("xor"), + Operator::Lt => sig.emit("alt"), + Operator::Le => { + // LE and GE are the inverses of GT and LT (arithmetic versions) + sig.emit("agt"); + sig.emit("not"); + } + Operator::Gt => sig.emit("agt"), + Operator::Ge => { + sig.emit("alt"); + sig.emit("not"); + } + Operator::Eq => { + sig.emit("xor"); + sig.emit("not"); + } + Operator::Ne => sig.emit("xor"), + Operator::Lshift => sig.emit("lshift"), + Operator::Rshift => sig.emit("arshift"), + } + Ok(()) + } + } + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + + #[test] + fn test_literal_strings() { + let state = state_for("const s1 = \"foo\"; fn test() { var x; x = \"bar\"; var y = \"norp\"; }"); + assert_eq!( + state.strings, + vec![ + ("_forge_gensym_1".into(), "foo".into()), + ("_forge_gensym_3".into(), "bar".into()), // gensym 2 is the entrypoint of blah() + ("_forge_gensym_4".into(), "norp".into()), + ] + ); + assert_eq!( + test_body(state), + vec![ + "push _forge_gensym_3", + "loadw frame", + "storew", // the assignment for x + "push _forge_gensym_4", + "loadw frame", + "add 3", // the address of y (frame + 3) and put gensym_4 in it + "storew", + ] + .join("\n") + ) + } + + #[test] + fn test_addresses() { + assert_eq!( + test_body(state_for("fn test() { var x = 3; var y = &x; }")), + vec![ + "push 3", + "loadw frame", + "storew", // the assignment for x + "loadw frame", // The addr of x + "loadw frame", + "add 3", // the address of y (frame + 3) and put the addr of x (frame) in it + "storew", + ] + .join("\n") + ) + } + + #[test] + fn test_derefs() { + assert_eq!( + test_body(state_for("fn test() { var x = 3; *1000 = *x; }")), + vec![ + "push 3", + "loadw frame", + "storew", // the assignment for x + "loadw frame", // Now we're compiling *x, so load x's value, which is 3 + "loadw", + "loadw", // Then load the value at 3 + "push 1000", // Push the addr 1000, for the lvalue + "storew", // Store whatever's at 3 to 1000 + ] + .join("\n") + ) + } + + #[test] + fn test_string_exprs() { + assert_eq!( + test_body(state_for("const foo = \"foo\"; fn test() { var x = \"bar\" + 3; }")), + vec![ + "push _forge_gensym_3", // 1 is the label in the string table for "foo", 2 for "blah," + "push 3", // so 3 is the string "bar" + "add", // Add 3 to that address + "loadw frame", // Store it in the first var + "storew", + ] + .join("\n") + ) + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/function.rs b/forge_core/src/compiler/ast_nodes/function.rs new file mode 100644 index 0000000..02daf01 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/function.rs @@ -0,0 +1,64 @@ +use crate::ast::{Function, Location}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for Function { + fn process(self, state: &mut State, _: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let label = state.find_or_declare_function(self.name.as_str(), loc)?; + + // The CompiledFn for this function, which will eventually get stuff populated into it: + let mut sig = CompiledFn { + label, + ..Default::default() + }; + + // This generates the code to copy the args into the frame as well as adds all the arguments + // to the local scope and calculates the arity. + sig.handle_args(&self)?; + + // todo we need to store arity somehow in the state, so we can check arglists even + // with recursive calls. This probably becomes splitting "signature" from "function context" + // and setting signature immutably right now, passing context down ("block?") and setting it + // at the end + + // Compile the body, storing all of it in the CompiledFn we just created / added + self.body.process(state, Some(&mut sig), loc)?; + + // This can't fail because if it were a dupe name, adding the global would have failed + state.functions.insert(self.name.clone(), sig); + + // And we're done: the function body now stores everything we need to emit that function... + // but we can't actually emit it yet because we don't know how we're building things. This + // is the equivalent of the object-file step in a more real compiler. + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::{state_for, test_body}; + + #[test] + fn test_basic_fns() { + assert_eq!( + test_body(state_for("fn test(a, b) { b = 17 + a; }")), + vec![ + "loadw frame", // Capture var b + "add 3", + "storew", + "loadw frame", // Capture var a + "storew", + "push 17", // Start calculating the rvalue, push the literal + "loadw frame", // This is looking up the "a" arg, at frame + 0 + "loadw", + "add", // 17 + a + "loadw frame", // Calculate the lvalue + "add 3", // "b" arg is frame + 3 + "storew", // Finally store + ] + .join("\n") + ) + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/function_prototype.rs b/forge_core/src/compiler/ast_nodes/function_prototype.rs new file mode 100644 index 0000000..151bf96 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/function_prototype.rs @@ -0,0 +1,31 @@ +use crate::ast::{FunctionPrototype, Location}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for FunctionPrototype { + fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { + state.declare_function(self.name.as_str(), self.args) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + use crate::compiler::utils::Variable; + + #[test] + fn test_function_prototypes() { + let mut state = state_for("fn blah(a, b);"); + assert_eq!(state.functions.get("blah"), None); + assert_eq!(state.prototypes.remove("blah"), Some(Variable::DirectLabel(String::from("_forge_gensym_1")))); + } + + #[test] + fn test_functions_with_prototypes() { + let mut state = state_for("fn blah(a, b); const foo = 3; fn blah(a, b) { return 7; }"); + assert_eq!(state.functions.remove("blah").unwrap().label, String::from("_forge_gensym_1")); + assert_eq!(state.prototypes.remove("blah"), None); + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/global.rs b/forge_core/src/compiler/ast_nodes/global.rs new file mode 100644 index 0000000..e2a0441 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/global.rs @@ -0,0 +1,43 @@ +use crate::ast::{Global, Location}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; +use crate::compiler::utils::Variable; + +impl Compilable for Global { + fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { + if self.size.is_some() { + todo!("Arrays are not yet supported") + } + state.add_global(&self.name, |s| Variable::IndirectLabel(s.gensym())) + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::parser::parse; + + #[test] + fn test_global_decl() { + let mut state = State::default(); + parse("global a;") + .unwrap() + .process(&mut state, None, (0, 0).into()) + .unwrap(); + assert_eq!( + state.global_scope, + [("a".into(), Variable::IndirectLabel("_forge_gensym_1".into()))].into() + ) + } + + #[test] + fn test_name_collision() { + let mut state = State::default(); + assert!(parse("const a = 7; global a;") + .unwrap() + .process(&mut state, None, (0, 0).into()) + .is_err()); + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/lvalue.rs b/forge_core/src/compiler/ast_nodes/lvalue.rs new file mode 100644 index 0000000..e678682 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/lvalue.rs @@ -0,0 +1,64 @@ +use crate::ast::{BoxExpr, Expr, Location, Lvalue}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; +use crate::compiler::utils::{lookup, Variable}; + +/// Evaluate an lvalue and leave its address on the stack (ready to be consumed by storew) +impl Compilable for Lvalue { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let global_scope = &state.global_scope; + let sig = sig.expect("lvalue outside a function"); + + match *(self.0.0) { + // A bunch of different things that aren't allowed + Expr::Number(_) | + Expr::Neg(_) | + Expr::Not(_) | + Expr::Address(_) | + Expr::Call(_) | + Expr::Infix(_, _, _) | + Expr::String(_) => { + Err(CompileError(0, 0, String::from("Not a valid lvalue"))) + } + + // Names we look up and leave the address on the stack: + Expr::Name(name) => { + if let Some(var) = lookup(&name, global_scope, &sig.local_scope) { + match var { + Variable::Literal(_) | Variable::DirectLabel(_) => { + // Direct labels are (probably) functions, the important part is the + // label itself, which we can't alter, so, error: + Err(CompileError(0, 0, format!("Invalid lvalue {}", name))) + } + Variable::IndirectLabel(label) => { + // Indirect labels are variables, the label is where the data is stored, + // so we push that label so we can store stuff there + let label = label.clone(); + sig.emit_arg("push", label); + Ok(()) + } + Variable::Local(offset) => { + let offset = *offset; + sig.emit_arg("loadw", "frame"); + if offset > 0 { + sig.emit_arg("add", offset); + } + Ok(()) + } + } + } else { + Err(CompileError(0, 0, format!("Unknown name {}", name))) + } + } + // Derefs are just evaluating the expr and leaving its value (an address) on the stack + Expr::Deref(BoxExpr(expr)) => { + (*expr).process(state, Some(sig), loc) + } + Expr::Subscript(_, _) => { + Err(CompileError(0, 0, String::from("Arrays are not yet supported"))) + } + } + } +} diff --git a/forge_core/src/compiler/ast_nodes/mod.rs b/forge_core/src/compiler/ast_nodes/mod.rs new file mode 100644 index 0000000..05b2ae8 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/mod.rs @@ -0,0 +1,14 @@ +mod program; +mod declaration; +mod block; +mod repeat_loop; +mod function; +mod function_prototype; +mod assignment; +mod var_decl; +mod lvalue; +mod call; +mod expr; +mod global; +mod r#const; +mod r#return; diff --git a/forge_core/src/compiler/ast_nodes/program.rs b/forge_core/src/compiler/ast_nodes/program.rs new file mode 100644 index 0000000..99fcebb --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/program.rs @@ -0,0 +1,14 @@ +use crate::ast::{Location, Program}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compile_error::CompileError; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::state::State; + +impl Compilable for Program { + fn process(self, state: &mut State, _: Option<&mut CompiledFn>, _loc: Location) -> Result<(), CompileError> { + for decl in self.0 { + decl.ast.process(state, None, decl.location)? + } + Ok(()) + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/repeat_loop.rs b/forge_core/src/compiler/ast_nodes/repeat_loop.rs new file mode 100644 index 0000000..b20f13a --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/repeat_loop.rs @@ -0,0 +1,173 @@ +use crate::ast::{Expr, Location, RepeatLoop, VarDecl}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for RepeatLoop { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let sig = sig.expect("Repeat loop outside function"); + + let RepeatLoop { count, name, body } = self; + let named_counter = name.is_some(); + let counter_name = name.unwrap_or_default(); + + if named_counter { + let decl = VarDecl { + name: counter_name.clone(), + size: None, + initial: Some(Expr::Number(0)), + }; + decl.process(state, Some(sig), loc)?; + } + + // Okay, the counter (if present) is now declared and in scope; we'll eval the limit once + count.process(state, Some(sig), loc)?; + + // Now we have a fairly normal while loop: + sig.emit("#while"); + // Stack currently has the limit on top. We need to cmp the counter to that. + sig.emit("dup"); + + // Load the counter, if present: + if named_counter { + Expr::Name(counter_name.clone()).process(state, Some(sig), loc)?; + // Subtract the counter from (the copy of) the limit. + sig.emit("sub"); + } + + // cmp that to zero, for our flag: this is either limit - ctr or limit, depending if + // there's a counter + sig.emit_arg("agt", 0); + + // Loop body: + sig.emit("#do"); + body.process(state, Some(sig), loc)?; + + // After the body we need to increment the counter if there is one + if named_counter { + // Put its address on top: + Expr::Address(Expr::Name(counter_name.clone()).into()).process(state, Some(sig), loc)?; + // Dup and load it: + sig.emit("dup"); + sig.emit("loadw"); + // Increment it: + sig.emit_arg("add", 1); + // Now we have ( counted-addr new-ctr-val ) so swap and store + sig.emit("swap"); + sig.emit("storew"); + } else { + // No counter, so just decrement the limit, which is already on top: + sig.emit_arg("sub", 1); + } + + // Back to the check! + sig.emit("#end"); + // Done with the loop, but the limit is still on the stack, pop it: + sig.emit("pop"); + + // If we have added a counter variable, we need to forget that because (even + // though declared outside the block) it should be scoped to the block: + if named_counter { + sig.forget_last_local(); + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + + #[test] + fn test_named_repeat_loops() { + // With a counter + assert_eq!( + test_body(state_for("fn test(a) { var x = 0; repeat(a) c { x = x + c; } return x; }")), + vec![ + "loadw frame", // capture arg a + "storew", + "push 0", // Create the 'x' var and store 0 in it + "loadw frame", + "add 3", + "storew", + "push 0", // Create the 'c' var and store 0 in it + "loadw frame", + "add 6", + "storew", + "loadw frame", // Load 'a' from args + "loadw", + "#while", // Starting the loop: + "dup", // Copy the limit + "loadw frame", // Load c + "add 6", + "loadw", + "sub", // Subtract c from a + "agt 0", // Are we still positive? + "#do", + "loadw frame", // Load x + "add 3", + "loadw", + "loadw frame", // Load c + "add 6", + "loadw", + "add", // Add c to x + "loadw frame", // Load x as an lvalue + "add 3", + "storew", // Store c + x into it + "loadw frame", // Load c as an lvalue + "add 6", + "dup", // Dup it, load it, add 1 + "loadw", + "add 1", + "swap", // Swap the addr on top and store it + "storew", + "#end", // End of the loop body! + "pop", // Drop the limit off the top + "loadw frame", // Load x so we can return it + "add 3", + "loadw", + "ret", + ] + .join("\n") + ); + } + + #[test] + fn test_anon_repeat_loops() { + // No counter + assert_eq!( + test_body(state_for("fn test(a) { var x = 1; repeat(a) { x = x * 2; } return x; }")), + vec![ + "loadw frame", // capture arg a + "storew", + "push 1", // Create the 'x' var and store 1 in it + "loadw frame", + "add 3", + "storew", + "loadw frame", // Load 'a' from args + "loadw", + "#while", // Starting the loop: + "dup", // Copy the limit + "agt 0", // Are we still positive? + "#do", + "loadw frame", // Load x + "add 3", + "loadw", + "push 2", // double it + "mul", + "loadw frame", // Load x as an lvalue + "add 3", + "storew", // Store 2x into it + "sub 1", // decrement the counter + "#end", // End of the loop body! + "pop", // Drop the limit off the top + "loadw frame", // Load x so we can return it + "add 3", + "loadw", + "ret", + ].join("\n") + ); + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/return.rs b/forge_core/src/compiler/ast_nodes/return.rs new file mode 100644 index 0000000..2bdabf4 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/return.rs @@ -0,0 +1,66 @@ +use crate::ast::{Location, Return}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for Return { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let sig = sig.expect("Block outside function"); + + match self { + Return(None) => { + // Returning nothing, so just default to returning a 0: + sig.emit_arg("ret", 0) + } + Return(Some(expr)) => { + // Eval the expr and emit a ret for it + expr.process(state, Some(sig), loc)?; + sig.emit("ret") + } + } + + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + + #[test] + fn test_returns_with_value() { + assert_eq!( + test_body(state_for("fn test(a) { return a + 3; }")), + vec![ + "loadw frame", // capture arg a + "storew", + "loadw frame", // Load a + "loadw", + "push 3", // Add 3 + "add", + "ret", // Return that + ] + .join("\n") + ); + } + + #[test] + fn test_void_returns() { + assert_eq!( + test_body(state_for("fn test(a) { if (a > 0) { return; } }")), + vec![ + "loadw frame", // capture arg a + "storew", + "loadw frame", // Load a + "loadw", + "push 0", // Compare to 0 + "agt", + "#if", // If statement + "ret 0", // Default return value, for an expr-less return + "#end", + ] + .join("\n") + ); + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/var_decl.rs b/forge_core/src/compiler/ast_nodes/var_decl.rs new file mode 100644 index 0000000..32b5155 --- /dev/null +++ b/forge_core/src/compiler/ast_nodes/var_decl.rs @@ -0,0 +1,54 @@ +use crate::ast::{Location, Lvalue, VarDecl}; +use crate::compiler::compilable::Compilable; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::CompileError; +use crate::compiler::state::State; + +impl Compilable for VarDecl { + fn process(self, state: &mut State, sig: Option<&mut CompiledFn>, loc: Location) -> Result<(), CompileError> { + let sig = sig.expect("Var declaration outside function"); + if self.size.is_some() { + todo!("Arrays are not yet supported") + } + if let Some(initial) = self.initial { + // If it's got an initial value, we have to compile that before we add + // the name to scope, or else UB will ensue if it refers to itself: + initial.process(state, Some(sig), loc)?; + // But then add it to scope and assign: + sig.add_local(&self.name)?; + // We'll just whip up an lvalue real quick... + Lvalue::from(self.name).process(state, Some(sig), loc)?; + sig.emit("storew"); // And store the initial value there + } else { + // Otherwise, just add it to scope and leave garbage in there: + sig.add_local(&self.name)?; + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::compiler::test_utils::*; + + #[test] + fn test_var_decls() { + assert_eq!( + test_body(state_for("fn test() { var a; var b = 7; a = b * 2; }")), + vec![ + "push 7", // Start calculating the rvalue, push the literal + "loadw frame", // "b" is the second local var at frame + 3 + "add 3", + "storew", // Do the initialization + "loadw frame", // Now we're evaluating b * 2 + "add 3", // b is at frame + 3... + "loadw", // load it to the stack + "push 2", + "mul", // b * 2 evaluated + "loadw frame", // Loading "a" as an lvalue + "storew", // doing the assignment + ] + .join("\n") + ) + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/compilable.rs b/forge_core/src/compiler/compilable.rs new file mode 100644 index 0000000..d1699dd --- /dev/null +++ b/forge_core/src/compiler/compilable.rs @@ -0,0 +1,18 @@ +use crate::ast::Location; +use crate::compiler::compile_error::CompileError; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::state::State; + +pub(crate) trait Compilable { + // Every AST node we visit can see the global compiler state (to make unique + // symbols and reach for global names) as well as optionally the current + // function (for AST nodes within functions). Additionally se send a Location: + // This is the closest ancestor's location, in case we need to emit a compile + // error + fn process( + self, + state: &mut State, + function: Option<&mut CompiledFn>, + location: Location, + ) -> Result<(), CompileError>; +} \ No newline at end of file diff --git a/forge_core/src/compiler/compile_error.rs b/forge_core/src/compiler/compile_error.rs new file mode 100644 index 0000000..34645ee --- /dev/null +++ b/forge_core/src/compiler/compile_error.rs @@ -0,0 +1,17 @@ +use std::fmt::{Display, Formatter}; +use crate::parser::ParseError; + +#[derive(Eq, Clone, PartialEq, Debug)] +pub struct CompileError(pub usize, pub usize, pub String); + +impl From for CompileError { + fn from(value: ParseError) -> Self { + Self(value.0, value.1, value.2) + } +} + +impl Display for CompileError { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + write!(f, "({}:{}) {}", self.0, self.1, self.2) + } +} diff --git a/forge_core/src/compiler/compiled_fn.rs b/forge_core/src/compiler/compiled_fn.rs new file mode 100644 index 0000000..5d6f7e5 --- /dev/null +++ b/forge_core/src/compiler/compiled_fn.rs @@ -0,0 +1,142 @@ +use std::collections::btree_map::Entry::Vacant; +use std::fmt::Display; +use crate::ast::Function; +use crate::compiler::compile_error::CompileError; +use crate::compiler::utils::{Label, Scope, Variable}; + +/// The data associated with a function signature: +/// - The frame size is in bytes +/// - The local scope is full of `Variable::Local`s, and contains args and local vars +/// - The body is just the statements of the function, not the code to move the frame +/// pointer around (which can't be generated until the variable declarations are all found) +/// +/// A complete function implementation consists of: +/// - A label for the entrypoint +/// - The function body (including preamble code to set up the stack frame) +/// +/// The stack frame is managed by the function through a global pointer called "frame". When +/// the function is called, it can assume that all memory after "frame" is free for use (this +/// isn't actually true because you can blow out the stack, but within reason it is). So when +/// you make a call to another function, you need to increment frame by the current frame size, +/// and then after the other function has returned, decrement it back so that frame again points +/// at your stack frame. Locals can be found by adding some offset from the frame pointer. +/// +/// todo: the allocator problem has (probably) been solved! Make an alloca() that increases the +/// current frame pointer by some size. To dynamically allocate memory, just put it in the stack +/// frame of the current fn. All fns return one word and all params are one word long (structs +/// get passed around by reference) +/// +/// todo: more of a global todo. Add a register that stores an offset that's added implicitly to +/// all absolute addresses. This makes it a lot simpler to make relocatable code +/// +/// alternate todo: add a callr instruction. This moves the basic unit of linking up from "fn" to +/// "library". Near calls are callr, far calls are call. Globals, reserve a word in the zero page +/// for a global frame pointer, save / restore that around a far call (using the rstack). +/// +/// Weird todo: add an abs (and maybe rel) instruction that converts a relative address on the +/// stack to an absolute one by adding the instruction pointer (of the abs / rel). Make room for it +/// by replacing sdp / setsdp / setint with just reg / setreg, which will push / pop register +/// values to the stack, given a register index (0 for data ptr, 1 for rstack, 2 for int enabled, +/// whatever). For a second removable opcode, how often is peekr actually used? +/// +/// Another instruction todo: remove the copy instruction (currently a dumb copy-region command) +/// and replace it with the design from last year with the mode argument... but as a DMA "device" +/// with an interface in the zero page. +#[derive(Clone, PartialEq, Debug, Default)] +pub struct CompiledFn { + pub label: Label, + pub frame_size: usize, + pub local_scope: Scope, + pub arity: usize, + pub body: Vec, +} + +impl CompiledFn { + /// Add a name to the local scope, which: + /// - Increases the size of the stack frame by that much + /// - Records the offset into the local stack frame where that variable is stored + pub(crate) fn add_local(&mut self, name: &str) -> Result<(), CompileError> { + if let Vacant(e) = self.local_scope.entry(name.into()) { + e.insert(Variable::Local(self.frame_size)); + self.frame_size += 3; + Ok(()) + } else { + Err(CompileError(0, 0, format!("Duplicate name {}", name))) + } + } + + /// Emit a string (ideally one instruction, but whatever) to the function body. + /// This doesn't actually emit anything to output, the body will eventually be emitted + /// in a final pass by the compiler once all functions are compiled. + pub(crate) fn emit(&mut self, opcode: &str) { + self.body.push(String::from(opcode)) + } + + /// A shorthand method to emit something with a `Display` arg, because emitting a + /// single instruction with a variable (numeric or label) arg is very common. + pub(crate) fn emit_arg(&mut self, opcode: &str, arg: T) { + self.body.push(format!("{} {}", opcode, arg)) + } + + /// The size of the local scope in bytes. This increases as variables are declared + /// todo: this needs to change for arrays; it won't like having vars that aren't 3 bytes long + pub(crate) fn frame_size(&self) -> usize { + self.local_scope.len() * 3 + } + + /// Emit the code to add the arguments' names to the local scopr, and move the arguments off + /// the stack into the frame. Should be run before the body is compiled, because this code + /// has to be the first thing in the body + pub(crate) fn handle_args(&mut self, function: &Function) -> Result<(), CompileError> { + let mut arg_names: Vec<&str> = Vec::new(); + + // Add each argument as a local + for name in function.args.iter() { + self.add_local(name.as_str())?; + arg_names.push(name.as_str()); + self.arity += 1; + } + + // Arguments in calls are pushed with the last on top, so, reverse the vec just to make + // the following loop easier: + arg_names.reverse(); + + for name in arg_names { + if let Variable::Local(offset) = self.local_scope[name] { + self.emit("loadw frame"); + if offset != 0 { + self.emit_arg("add", offset); + } + self.emit("storew"); + } + } + + Ok(()) + } + + /// Takes a new frame size and removes all names from the local scope that would be placed + /// beyond that frame length: used when compiling blocks to de-scope names that should only + /// be visible in the block + pub(crate) fn reduce_frame_size_to(&mut self, new_size: usize) { + self.frame_size = new_size; + let mut to_remove: Vec = Vec::new(); + for (name, var) in self.local_scope.iter() { + if let Variable::Local(offset) = var { + if *offset >= self.frame_size { + to_remove.push(name.clone()); + } + } + } + + for name in to_remove { + self.local_scope.remove(name.as_str()); + } + } + + /// Reduces the frame size by one word, to throw out the most recently declared local. + /// Used for things like repeat loops where there's a name declared outside a block but which + /// should only be visible in the block anyway. + pub(crate) fn forget_last_local(&mut self) { + self.reduce_frame_size_to(self.frame_size - 3); + } +} diff --git a/forge_core/src/compiler/mod.rs b/forge_core/src/compiler/mod.rs new file mode 100644 index 0000000..49d1539 --- /dev/null +++ b/forge_core/src/compiler/mod.rs @@ -0,0 +1,94 @@ +use crate::compiler::compilable::Compilable; +pub use crate::compiler::compile_error::CompileError; +use crate::compiler::state::State; +use crate::compiler::utils::Variable; +use crate::parser::parse; + +mod ast_nodes; +mod compile_error; +mod compiled_fn; +mod utils; +mod state; +mod compilable; + +#[cfg(test)] +mod test_utils; + +/////////////////////////////////////////////////////////// + +/// Turn a `Program` into a list of assembly lines that can be assembled to run it. +/// There are various ways to build a program, this one is the simplest: it builds it as a +/// complete ROM that will place a jmp to main() at 0x400, so a Vulcan can boot from it. +pub fn build_boot(src: &str) -> Result, CompileError> { + let mut state = State::default(); + let ast = parse(src).map_err(CompileError::from)?; + ast.process(&mut state, None, (0, 0).into())?; + + if let Some(Variable::DirectLabel(label)) = state.global_scope.get("main") { + // Let's make a vec for the final listing + let mut asm: Vec = Vec::new(); + + // Now we start piling stuff into the vec, starting with an org: + asm.push(".org 0x400".into()); + + // jmp into main: + asm.push(format!("call {}", label)); + + // When main returns, just hlt: + asm.push("hlt".into()); + + // Now start dumping compiled objects into there: + for (label, val) in state.strings.iter() { + asm.push(format!("{}: .db \"{}\\0\"", label, val)) + } + for (_, val) in state.functions.iter_mut() { + asm.push(format!("{}:", val.label)); + asm.append(val.body.as_mut()); + } + + // Final thing is to place a label for the stack: + // (this is just a cell with the address of the following word) + asm.push("frame: .db $+1".into()); + asm.push(".db 0".into()); + Ok(asm) + } else { + Err(CompileError(0, 0, "Function main not defined (or not a function)".into())) + } +} + +/////////////////////////////////////////////////////////////////////////////////////////////////// + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_build_boot() { + // Very basic test of one main() + let asm = build_boot("fn main() { return 5; }".into()).unwrap(); + assert_eq!(asm.join("\n"), vec![ + ".org 0x400", + "call _forge_gensym_1", + "hlt", + "_forge_gensym_1:", + "push 5", + "ret", + "frame: .db $+1", + ".db 0", + ].join("\n")); + + // Slightly more complicated, with a global str + let asm = build_boot("const str = \"blah\"; fn main() { return str; }".into()).unwrap(); + assert_eq!(asm.join("\n"), vec![ + ".org 0x400", + "call _forge_gensym_2", + "hlt", + "_forge_gensym_1: .db \"blah\\0\"", + "_forge_gensym_2:", + "push _forge_gensym_1", + "ret", + "frame: .db $+1", + ".db 0", + ].join("\n")) + } +} diff --git a/forge_core/src/compiler/state.rs b/forge_core/src/compiler/state.rs new file mode 100644 index 0000000..adb5d61 --- /dev/null +++ b/forge_core/src/compiler/state.rs @@ -0,0 +1,79 @@ +use std::collections::BTreeMap; +use crate::ast::Location; +use crate::compiler::compile_error::CompileError; +use crate::compiler::compiled_fn::CompiledFn; +use crate::compiler::utils::{Label, Scope, Variable}; + +/// The compiler state: +#[derive(Clone, PartialEq, Debug, Default)] +pub(crate) struct State { + /// Used by gensym to generate unique symbols + pub gensym_index: usize, + /// The globally-defined names + pub global_scope: Scope, + /// The functions + pub functions: BTreeMap, + /// The string table + pub strings: Vec<(Label, String)>, + /// The functions that have been prototyped but not yet defined + pub prototypes: Scope, +} + +impl State { + /// Generate a guaranteed-unique symbolic name + pub(crate) fn gensym(&mut self) -> Label { + self.gensym_index += 1; + format!("_forge_gensym_{}", self.gensym_index) + } + + /// Return whether a name exists in the global scope already + fn defined(&self, name: &str) -> bool { + self.global_scope.contains_key(name) + } + + /// Add a symbol to the global namespace, catching name collisions + pub(crate) fn add_global Variable>( + &mut self, + name: &str, + val: F, + ) -> Result<(), CompileError> { + if self.defined(name) { + Err(CompileError(0, 0, format!("name {} already defined", name))) + } else { + let val = val(self); + self.global_scope.insert(name.into(), val); + Ok(()) + } + } + + pub(crate) fn add_string(&mut self, string: &str) -> Label { + let sym = self.gensym(); + self.strings.push((sym.clone(), string.into())); + sym + } + + pub(crate) fn declare_function(&mut self, name: &str, _args: Vec) -> Result<(), CompileError> { + // If it's not already prototyped, gensym a label and put it in the list. If it is, just + // ignore this (we don't check arity so it's not like the arglist being different matters) + // todo: check arity, store it here + if !self.prototypes.contains_key(name) { + let label = self.gensym(); + self.add_global(name, |_| Variable::DirectLabel(label.clone()))?; + self.prototypes.insert(name.into(), Variable::DirectLabel(label)); + } + Ok(()) + } + + pub(crate) fn find_or_declare_function(&mut self, name: &str, _loc: Location) -> Result { + // If it's in here, remove it and return the label: + // (this will only ever match this way; only thing that puts stuff in prototypes adds directlabels) + if let Some(Variable::DirectLabel(label)) = self.prototypes.remove(name) { + Ok(label) + } else { + // Otherwise, make a new label, add it to globals, and return it + let label = self.gensym(); + self.add_global(name, |_| Variable::DirectLabel(label.clone()))?; + Ok(label) + } + } +} diff --git a/forge_core/src/compiler/test_utils.rs b/forge_core/src/compiler/test_utils.rs new file mode 100644 index 0000000..b6ff695 --- /dev/null +++ b/forge_core/src/compiler/test_utils.rs @@ -0,0 +1,16 @@ +use crate::compiler::compilable::Compilable; +use crate::compiler::state::State; +use crate::parser::parse; + +pub(crate) fn state_for(src: &str) -> State { + let mut state = State::default(); + parse(src) + .unwrap() + .process(&mut state, None, (0, 0).into()) + .expect("Failed to compile"); + state +} + +pub(crate) fn test_body(state: State) -> String { + state.functions.get("test").unwrap().body.join("\n") +} diff --git a/forge_core/src/compiler/utils.rs b/forge_core/src/compiler/utils.rs new file mode 100644 index 0000000..b0bfe02 --- /dev/null +++ b/forge_core/src/compiler/utils.rs @@ -0,0 +1,31 @@ +use std::collections::BTreeMap; + +/// A variable, of various different types: +/// - Literals have static values and can just be pushed to the stack +/// - IndirectLabels contain the label pointing to the value (think `foo: .db 0`) +/// - Direct labels are the value themselves (think `call foo`) +/// - Locals contain an index into the local frame +#[derive(Clone, PartialEq, Debug)] +pub enum Variable { + Literal(i32), + IndirectLabel(String), + DirectLabel(String), + Local(usize), +} + +/// Maps from names to the variables they represent +pub type Scope = BTreeMap; + +/// So we don't get confused between string-strings and assembly-label strings +pub type Label = String; + +/// Look up a name first in the local scope, and failing that in the global scope. +pub fn lookup<'a>(name: &str, global_scope: &'a Scope, local_scope: &'a Scope) -> Option<&'a Variable> { + if let Some(var) = local_scope.get(name) { + Some(var) + } else if let Some(var) = global_scope.get(name) { + Some(var) + } else { + None + } +} diff --git a/forge_core/src/parser/ast_nodes/conditional.rs b/forge_core/src/parser/ast_nodes/conditional.rs index 0561559..3531872 100644 --- a/forge_core/src/parser/ast_nodes/conditional.rs +++ b/forge_core/src/parser/ast_nodes/conditional.rs @@ -20,7 +20,7 @@ impl AstNode for Conditional { mod test { use crate::ast::Statement; use crate::parser::Parseable; - use crate::parser::ast_nodes::test_utils::*; + use crate::parser::test_utils::*; use super::*; #[test] diff --git a/forge_core/src/parser/ast_nodes/mod.rs b/forge_core/src/parser/ast_nodes/mod.rs index 57dd315..7ca4cf3 100644 --- a/forge_core/src/parser/ast_nodes/mod.rs +++ b/forge_core/src/parser/ast_nodes/mod.rs @@ -10,7 +10,6 @@ mod assignment; mod var_decl; mod block; mod conditional; -mod test_utils; mod while_loop; mod repeat_loop; mod program; diff --git a/forge_core/src/parser/ast_nodes/program.rs b/forge_core/src/parser/ast_nodes/program.rs index 50c5dd6..5a1748b 100644 --- a/forge_core/src/parser/ast_nodes/program.rs +++ b/forge_core/src/parser/ast_nodes/program.rs @@ -17,7 +17,7 @@ impl AstNode for Program { #[cfg(test)] mod test { - use crate::parser::ast_nodes::test_utils::dislocate; + use crate::parser::test_utils::dislocate; use crate::parser::Parseable; use super::*; diff --git a/forge_core/src/parser/ast_nodes/repeat_loop.rs b/forge_core/src/parser/ast_nodes/repeat_loop.rs index b8a7107..04d2a39 100644 --- a/forge_core/src/parser/ast_nodes/repeat_loop.rs +++ b/forge_core/src/parser/ast_nodes/repeat_loop.rs @@ -17,7 +17,7 @@ impl AstNode for RepeatLoop { #[cfg(test)] mod test { use crate::ast::Statement; - use crate::parser::ast_nodes::test_utils::{dislocate, dislocated_block}; + use crate::parser::test_utils::{dislocate, dislocated_block}; use crate::parser::Parseable; use super::*; diff --git a/forge_core/src/parser/ast_nodes/while_loop.rs b/forge_core/src/parser/ast_nodes/while_loop.rs index 5c8d494..4696190 100644 --- a/forge_core/src/parser/ast_nodes/while_loop.rs +++ b/forge_core/src/parser/ast_nodes/while_loop.rs @@ -15,7 +15,7 @@ impl AstNode for WhileLoop { #[cfg(test)] mod test { use crate::ast::Statement; - use crate::parser::ast_nodes::test_utils::{dislocate, dislocated_block}; + use crate::parser::test_utils::{dislocate, dislocated_block}; use crate::parser::Parseable; use super::*; diff --git a/forge_core/src/parser/mod.rs b/forge_core/src/parser/mod.rs index 5cf2503..b81f978 100644 --- a/forge_core/src/parser/mod.rs +++ b/forge_core/src/parser/mod.rs @@ -1,6 +1,17 @@ use pest::pratt_parser::PrattParser; use pest::Parser; +use crate::ast::*; +pub use pairs_ext::*; +pub use parse_error::ParseError; + +mod parse_error; +mod pairs_ext; +mod ast_nodes; + +#[cfg(test)] +mod test_utils; + #[derive(Parser)] #[grammar = "parser/forge.pest"] struct ForgeParser; @@ -33,13 +44,6 @@ pub type PestRule = Rule; pub(crate) type Pair<'a> = pest::iterators::Pair<'a, Rule>; pub(crate) type Pairs<'i, R = Rule> = pest::iterators::Pairs<'i, R>; -mod parse_error; -mod pairs_ext; - -use crate::ast::*; -pub use pairs_ext::*; -pub use parse_error::ParseError; - /// A trait that represents something that can be parsed into an AST node: impl this to turn /// a pair into your node, by from_pair-ing child nodes. trait AstNode: Sized { @@ -77,11 +81,10 @@ pub fn parse(src: &str) -> Result { Program::from_str(src) } -mod ast_nodes; - /////////////////////////////////////////////////////////////////////////////////////////// // todo: can this die? it's only used in compiler tests but needs to be here for visibility reasons +#[cfg(test)] impl Expr { pub(crate) fn parse(src: &str) -> Result { Self::from_str(src) diff --git a/forge_core/src/parser/ast_nodes/test_utils.rs b/forge_core/src/parser/test_utils.rs similarity index 100% rename from forge_core/src/parser/ast_nodes/test_utils.rs rename to forge_core/src/parser/test_utils.rs diff --git a/vcore/src/cpu.rs b/vcore/src/cpu.rs index 6402fb1..4265a5d 100644 --- a/vcore/src/cpu.rs +++ b/vcore/src/cpu.rs @@ -381,9 +381,9 @@ impl CPU { match self.fetch() { Ok(instr) => { - //if cfg!(debug_assertions) { - //self.debug_instr(instr); - //} + if cfg!(debug_assertions) { + self.debug_instr(instr); + } self.pc = if let Some(err) = self.error(instr) { self.handle_error(err) diff --git a/vemu/src/main.rs b/vemu/src/main.rs index de995f8..86fad6f 100644 --- a/vemu/src/main.rs +++ b/vemu/src/main.rs @@ -10,10 +10,8 @@ use winit::{ use crate::keyboard::convert_keycode; use pixels::{Pixels, SurfaceTexture}; use std::collections::VecDeque; -use std::fs::File; -use std::io::Read; use std::time::{Duration, Instant}; -use vasm_core::{assemble_file, assemble_snippet}; +use vasm_core::assemble_snippet; use vcore::cpu::CPU; use vcore::memory::{Memory, PeekPoke}; use vcore::word::Word; diff --git a/vtest/src/forge_tests.rs b/vtest/src/forge_tests.rs index baacdd3..463eda7 100644 --- a/vtest/src/forge_tests.rs +++ b/vtest/src/forge_tests.rs @@ -1,6 +1,5 @@ use vasm_core::assemble_snippet; -use vcore::memory::{Memory, PeekPokeExt}; -use vcore::word::Word; +use vcore::memory::Memory; use vcore::CPU; fn run_forge(src: &str) -> CPU { diff --git a/vtest/src/lib.rs b/vtest/src/lib.rs index 9ad4492..d00509c 100644 --- a/vtest/src/lib.rs +++ b/vtest/src/lib.rs @@ -1,3 +1,5 @@ #[cfg(test)] mod integration_tests; + +#[cfg(test)] mod forge_tests; \ No newline at end of file