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") ); } }