diff --git a/Cargo.lock b/Cargo.lock index fbabf72..897a276 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -248,10 +248,17 @@ dependencies = [ "anyhow", "indoc", "parser", + "pretty_assertions", "quick-error", "tokenizer", ] +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + [[package]] name = "equivalent" version = "1.0.2" @@ -478,6 +485,16 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "pretty_assertions" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ae130e2f271fbc2ac3a40fb1d07180839cdbbe443c7a27e1e3c13c5cac0116d" +dependencies = [ + "diff", + "yansi", +] + [[package]] name = "prettyplease" version = "0.1.25" @@ -1046,6 +1063,12 @@ dependencies = [ "tap", ] +[[package]] +name = "yansi" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfe53a6657fd280eaa890a3bc59152892ffa3e30101319d168b781ed6529b049" + [[package]] name = "zerocopy" version = "0.8.27" diff --git a/libs/compiler/Cargo.toml b/libs/compiler/Cargo.toml index 122c21c..85434ac 100644 --- a/libs/compiler/Cargo.toml +++ b/libs/compiler/Cargo.toml @@ -11,3 +11,4 @@ tokenizer = { path = "../tokenizer" } [dev-dependencies] anyhow = { version = "1.0" } indoc = { version = "2.0" } +pretty_assertions = "1" diff --git a/libs/compiler/src/lib.rs b/libs/compiler/src/lib.rs index 4d1d570..388d2b2 100644 --- a/libs/compiler/src/lib.rs +++ b/libs/compiler/src/lib.rs @@ -11,7 +11,7 @@ use std::cmp::Ordering; use std::collections::HashMap; use std::io::{BufWriter, Write}; -pub use v2::{Compiler, Error}; +pub use v2::{Compiler, CompilerConfig, Error}; quick_error! { #[derive(Debug)] diff --git a/libs/compiler/src/test/declaration_function_invocation.rs b/libs/compiler/src/test/declaration_function_invocation.rs new file mode 100644 index 0000000..1b1a05c --- /dev/null +++ b/libs/compiler/src/test/declaration_function_invocation.rs @@ -0,0 +1,85 @@ +use crate::compile; +use indoc::indoc; +use pretty_assertions::assert_eq; + +#[test] +fn no_arguments() -> anyhow::Result<()> { + let compiled = compile! { + debug + " + fn doSomething() {}; + let i = doSomething(); + " + }; + + let to_test = indoc! { + " + j main + doSomething: + push ra + sub r0 sp 1 + get ra db r0 + sub sp sp 1 + j ra + main: + jal doSomething + move r8 r15 #i + " + }; + + assert_eq!(compiled, to_test); + + Ok(()) +} + +#[test] +fn let_var_args() -> anyhow::Result<()> { + let compiled = compile! { + debug + " + fn doSomething(arg1) {}; + let arg1 = 123; + let i = doSomething(arg1); + " + }; + + assert_eq!( + compiled, + indoc! { + " + j main + doSomething: + pop r8 #arg1 + push ra + sub r0 sp 1 + get ra db r0 + sub sp sp 1 + j ra + main: + move r8 123 #arg1 + push r8 + push r8 + jal doSomething + sub r0 sp 1 + get r8 db r0 + sub sp sp 1 + move r9 r15 #i + " + } + ); + + Ok(()) +} + +#[test] +fn incorrect_args_count() -> anyhow::Result<()> { + let compiled = compile! { + debug + " + fn doSomething(arg1, arg2){}; + let i = doSomething(); + " + }; + + Ok(()) +} diff --git a/libs/compiler/src/test/declaration_literal.rs b/libs/compiler/src/test/declaration_literal.rs new file mode 100644 index 0000000..5a881a4 --- /dev/null +++ b/libs/compiler/src/test/declaration_literal.rs @@ -0,0 +1,62 @@ +use indoc::indoc; +use pretty_assertions::assert_eq; + +#[test] +fn variable_declaration_numeric_literal() -> anyhow::Result<()> { + let compiled = crate::compile! { + debug r#" + let i = 20c; + "# + }; + + assert_eq!( + compiled, + indoc! { + " + j main + main: + move r8 293.15 #i + " + } + ); + + Ok(()) +} + +#[test] +fn variable_declaration_numeric_literal_stack_spillover() -> anyhow::Result<()> { + let compiled = compile! {debug r#" + let a = 0; + let b = 1; + let c = 2; + let d = 3; + let e = 4; + let f = 5; + let g = 6; + let h = 7; + let i = 8; + let j = 9; + "#}; + + assert_eq!( + compiled, + indoc! { + " + j main + main: + move r8 0 #a + move r9 1 #b + move r10 2 #c + move r11 3 #d + move r12 4 #e + move r13 5 #f + move r14 6 #g + push 7 #h + push 8 #i + push 9 #j + " + } + ); + + Ok(()) +} diff --git a/libs/compiler/src/test/function_declaration.rs b/libs/compiler/src/test/function_declaration.rs new file mode 100644 index 0000000..28200f7 --- /dev/null +++ b/libs/compiler/src/test/function_declaration.rs @@ -0,0 +1,58 @@ +use indoc::indoc; +use pretty_assertions::assert_eq; + +#[test] +fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> { + let compiled = compile!(debug r#" + // we need more than 4 params to 'spill' into a stack var + fn doSomething(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {}; + "#); + + assert_eq!( + compiled, + indoc! {" + j main + doSomething: + pop r8 #arg9 + pop r9 #arg8 + pop r10 #arg7 + pop r11 #arg6 + pop r12 #arg5 + pop r13 #arg4 + pop r14 #arg3 + push ra + sub r0 sp 1 + get ra db r0 + sub sp sp 3 + j ra + "} + ); + + Ok(()) +} + +#[test] +fn test_function_declaration_with_register_params() -> anyhow::Result<()> { + let compiled = compile!(debug r#" + // This is a test function declaration with no body + fn doSomething(arg1, arg2) { + }; + "#); + + assert_eq!( + compiled, + indoc! {" + j main + doSomething: + pop r8 #arg2 + pop r9 #arg1 + push ra + sub r0 sp 1 + get ra db r0 + sub sp sp 1 + j ra + "} + ); + + Ok(()) +} diff --git a/libs/compiler/src/test/mod.rs b/libs/compiler/src/test/mod.rs index 4321378..e439f4d 100644 --- a/libs/compiler/src/test/mod.rs +++ b/libs/compiler/src/test/mod.rs @@ -1,21 +1,15 @@ -use super::v2::Compiler; -use crate::v2::CompilerConfig; -use indoc::indoc; -use parser::Parser; -use std::io::BufWriter; -use tokenizer::Tokenizer; - macro_rules! output { ($input:expr) => { String::from_utf8($input.into_inner()?)? }; } +#[macro_export] macro_rules! compile { ($source:expr) => {{ - let mut writer = BufWriter::new(Vec::new()); - let compiler = Compiler::new( - Parser::new(Tokenizer::from(String::from($source))), + let mut writer = std::io::BufWriter::new(Vec::new()); + let compiler = crate::Compiler::new( + parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))), &mut writer, None, ); @@ -24,73 +18,16 @@ macro_rules! compile { }}; (debug $source:expr) => {{ - let mut writer = BufWriter::new(Vec::new()); - let compiler = Compiler::new( - Parser::new(Tokenizer::from(String::from($source))), + let mut writer = std::io::BufWriter::new(Vec::new()); + let compiler = crate::Compiler::new( + parser::Parser::new(tokenizer::Tokenizer::from(String::from($source))), &mut writer, - Some(CompilerConfig { - debug: true, - ..Default::default() - }), + Some(crate::CompilerConfig { debug: true }), ); compiler.compile()?; output!(writer) }}; } - -#[test] -fn test_function_declaration_with_spillover_params() -> anyhow::Result<()> { - let compiled = compile!(debug r#" - // we need more than 4 params to 'spill' into a stack var - fn doSomething(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {}; - "#); - - assert_eq!( - compiled, - indoc! {" - j main - doSomething: - pop r8 #arg9 - pop r9 #arg8 - pop r10 #arg7 - pop r11 #arg6 - pop r12 #arg5 - pop r13 #arg4 - pop r14 #arg3 - pop r15 #arg2 - push ra - sub r0 sp 1 - get ra db r0 - sub sp sp 2 - j ra - "} - ); - - Ok(()) -} - -#[test] -fn test_function_declaration_with_register_params() -> anyhow::Result<()> { - let compiled = compile!(debug r#" - // This is a test function declaration with no body - fn doSomething(arg1, arg2) { - }; - "#); - - assert_eq!( - compiled, - indoc! {" - j main - doSomething: - pop r8 #arg2 - pop r9 #arg1 - push ra - sub r0 sp 1 - get ra db r0 - sub sp sp 1 - j ra - "} - ); - - Ok(()) -} +mod declaration_function_invocation; +mod declaration_literal; +mod function_declaration; diff --git a/libs/compiler/src/v2.rs b/libs/compiler/src/v2.rs index b184c8b..c411d8c 100644 --- a/libs/compiler/src/v2.rs +++ b/libs/compiler/src/v2.rs @@ -1,7 +1,10 @@ use crate::variable_manager::{self, LocationRequest, VariableLocation, VariableScope}; use parser::{ Parser as ASTParser, - tree_node::{BlockExpression, DeviceDeclarationExpression, Expression, FunctionExpression}, + tree_node::{ + BlockExpression, DeviceDeclarationExpression, Expression, FunctionExpression, + InvocationExpression, Literal, + }, }; use quick_error::quick_error; use std::{ @@ -9,6 +12,16 @@ use std::{ io::{BufWriter, Write}, }; +macro_rules! debug { + ($self: expr, $debug_value: expr) => { + if $self.config.debug { + format!($debug_value) + } else { + "".into() + } + }; +} + quick_error! { #[derive(Debug)] pub enum Error { @@ -22,10 +35,16 @@ quick_error! { from() } DuplicateFunction(func_name: String) { - display("{func_name} has already been defined") + display("`{func_name}` has already been defined") + } + UnknownIdentifier(ident: String) { + display("`{ident}` is not found in the current scope.") } InvalidDevice(device: String) { - display("{device} is not valid") + display("`{device}` is not valid") + } + AgrumentMismatch(func_name: String) { + display("Incorrect number of arguments passed into `{func_name}`") } Unknown(reason: String) { display("{reason}") @@ -41,6 +60,7 @@ pub struct CompilerConfig { pub struct Compiler<'a, W: std::io::Write> { parser: ASTParser, function_locations: HashMap, + function_metadata: HashMap>, devices: HashMap, output: &'a mut BufWriter, current_line: usize, @@ -57,6 +77,7 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { Self { parser, function_locations: HashMap::new(), + function_metadata: HashMap::new(), devices: HashMap::new(), output: writer, current_line: 1, @@ -93,6 +114,9 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { Expression::Declaration(var_name, expr) => { self.expression_declaration(var_name, *expr, scope)? } + Expression::Invocation(expr_invoke) => { + self.expression_function_invocation(expr_invoke, scope)? + } _ => todo!(), }; @@ -105,16 +129,135 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { expr: Expression, scope: &mut VariableScope<'v>, ) -> Result<(), Error> { - scope.add_variable(var_name.clone(), LocationRequest::Persist)?; - match expr { - _ => unimplemented!(), + Expression::Literal(Literal::Number(num)) => { + let var_location = + scope.add_variable(var_name.clone(), LocationRequest::Persist)?; + let num_str = num.to_string(); + + if let VariableLocation::Temporary(reg) | VariableLocation::Persistant(reg) = + var_location + { + self.write_output(format!( + "move r{reg} {num_str} {}", + debug!(self, "#{var_name}") + ))?; + } else { + self.write_output(format!("push {num_str} {}", debug!(self, "#{var_name}")))?; + } + } + Expression::Invocation(invoke_expr) => { + self.expression_function_invocation(invoke_expr, scope)?; + + // Return value _should_ be in VariableScope::RETURN_REGISTER + match scope.add_variable(var_name.clone(), LocationRequest::Persist)? { + VariableLocation::Temporary(reg) | VariableLocation::Persistant(reg) => self + .write_output(format!( + "move r{reg} r{} {}", + VariableScope::RETURN_REGISTER, + debug!(self, "#{var_name}") + ))?, + VariableLocation::Stack(_) => self.write_output(format!( + "push r{} {}", + VariableScope::RETURN_REGISTER, + debug!(self, "#{var_name}") + ))?, + } + } + _ => { + return Err(Error::Unknown( + "`{var_name}` declaration of this type is not supported.".into(), + )); + } } Ok(()) } - fn expression_device<'v>(&mut self, expr: DeviceDeclarationExpression) { + fn expression_function_invocation( + &mut self, + invoke_expr: InvocationExpression, + stack: &mut VariableScope, + ) -> Result<(), Error> { + if !self.function_locations.contains_key(&invoke_expr.name) { + return Err(Error::UnknownIdentifier(invoke_expr.name)); + } + + let Some(args) = self.function_metadata.get(&invoke_expr.name) else { + return Err(Error::UnknownIdentifier(invoke_expr.name)); + }; + + if args.len() != invoke_expr.arguments.len() { + return Err(Error::AgrumentMismatch(invoke_expr.name)); + } + + // backup all used registers to the stack + let active_registers = stack.registers().cloned().collect::>(); + for register in &active_registers { + stack.add_variable(format!("temp_{register}"), LocationRequest::Stack)?; + self.write_output(format!("push r{register}"))?; + } + for arg in invoke_expr.arguments { + match arg { + Expression::Literal(Literal::Number(num)) => { + let num_str = num.to_string(); + self.write_output(format!("push {num_str}"))?; + } + Expression::Variable(var_name) => match stack.get_location_of(var_name)? { + VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => { + self.write_output(format!("push r{reg}"))?; + } + VariableLocation::Stack(stack_offset) => { + self.write_output(format!( + "sub r{0} sp {stack_offset}", + VariableScope::TEMP_STACK_REGISTER + ))?; + self.write_output(format!( + "get r{0} db r{0}", + VariableScope::TEMP_STACK_REGISTER + ))?; + self.write_output(format!( + "push r{0}", + VariableScope::TEMP_STACK_REGISTER + ))?; + } + }, + _ => { + return Err(Error::Unknown(format!( + "Attempted to call `{}` with an unsupported argument", + invoke_expr.name + ))); + } + } + } + + // jump to the function and store current line in ra + self.write_output(format!("jal {}", invoke_expr.name))?; + + for register in active_registers { + let VariableLocation::Stack(stack_offset) = + stack.get_location_of(format!("temp_{register}"))? + else { + return Err(Error::UnknownIdentifier(format!("temp_{register}"))); + }; + self.write_output(format!( + "sub r{0} sp {stack_offset}", + VariableScope::TEMP_STACK_REGISTER + ))?; + self.write_output(format!( + "get r{register} db r{0}", + VariableScope::TEMP_STACK_REGISTER + ))?; + } + + if stack.stack_offset() > 0 { + self.write_output(format!("sub sp sp {}", stack.stack_offset()))?; + } + + Ok(()) + } + + fn expression_device(&mut self, expr: DeviceDeclarationExpression) { self.devices.insert(expr.name, expr.device); } @@ -166,14 +309,17 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { return Err(Error::DuplicateFunction(name)); } - self.function_locations - .insert(name.clone(), self.current_line); + self.function_metadata + .insert(name.clone(), arguments.clone()); // Declare the function as a line identifier self.write_output(format!("{}:", name))?; + self.function_locations + .insert(name.clone(), self.current_line); + // Create a new block scope for the function body - let mut block_scope = VariableScope::scoped(&scope); + let mut block_scope = VariableScope::scoped(scope); let mut saved_variables = 0; @@ -189,14 +335,7 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { match loc { VariableLocation::Persistant(loc) => { - self.write_output(format!( - "pop r{loc} {}", - if self.config.debug { - format!("#{}", var_name) - } else { - "".into() - } - ))?; + self.write_output(format!("pop r{loc} {}", debug!(self, "#{var_name}")))?; } VariableLocation::Stack(_) => { return Err(Error::Unknown( @@ -234,8 +373,14 @@ impl<'a, W: std::io::Write> Compiler<'a, W> { )); }; - self.write_output(format!("sub r0 sp {ra_stack_offset}"))?; - self.write_output("get ra db r0")?; + self.write_output(format!( + "sub r{0} sp {ra_stack_offset}", + VariableScope::TEMP_STACK_REGISTER + ))?; + self.write_output(format!( + "get ra db r{0}", + VariableScope::TEMP_STACK_REGISTER + ))?; if block_scope.stack_offset() > 0 { self.write_output(format!("sub sp sp {}", block_scope.stack_offset()))?; diff --git a/libs/compiler/src/variable_manager.rs b/libs/compiler/src/variable_manager.rs index d1be261..edf42e4 100644 --- a/libs/compiler/src/variable_manager.rs +++ b/libs/compiler/src/variable_manager.rs @@ -6,7 +6,7 @@ use quick_error::quick_error; use std::collections::{HashMap, VecDeque}; const TEMP: [u8; 7] = [1, 2, 3, 4, 5, 6, 7]; -const PERSIST: [u8; 8] = [8, 9, 10, 11, 12, 13, 14, 15]; +const PERSIST: [u8; 7] = [8, 9, 10, 11, 12, 13, 14]; quick_error! { #[derive(Debug)] @@ -72,8 +72,26 @@ impl<'a> Default for VariableScope<'a> { } impl<'a> VariableScope<'a> { - pub const TEMP_REGISTER_COUNT: u8 = 8; - pub const PERSIST_REGISTER_COUNT: u8 = 8; + pub const TEMP_REGISTER_COUNT: u8 = 7; + pub const PERSIST_REGISTER_COUNT: u8 = 7; + + pub const RETURN_REGISTER: u8 = 15; + pub const TEMP_STACK_REGISTER: u8 = 0; + + pub fn registers(&self) -> impl Iterator { + self.var_lookup_table + .values() + .filter(|val| { + matches!( + val, + VariableLocation::Temporary(_) | VariableLocation::Persistant(_) + ) + }) + .map(|loc| match loc { + VariableLocation::Persistant(reg) | VariableLocation::Temporary(reg) => reg, + _ => unreachable!(), + }) + } pub fn scoped(parent: &'a VariableScope<'a>) -> Self { Self {