From dbdafbfd4be1749d40b541320d60d1cda8ab661e Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 13 Nov 2023 12:10:57 -0600 Subject: [PATCH] Arrays (local) now work! --- .../src/compiler/ast_nodes/assignment.rs | 39 +++++++++++ forge_core/src/compiler/ast_nodes/expr.rs | 66 ++++++++++++++++++- forge_core/src/compiler/ast_nodes/global.rs | 2 +- forge_core/src/compiler/ast_nodes/lvalue.rs | 8 ++- forge_core/src/compiler/compiled_fn.rs | 58 ++++++++++++++-- vtest/src/forge_tests.rs | 24 +++++++ 6 files changed, 185 insertions(+), 12 deletions(-) diff --git a/forge_core/src/compiler/ast_nodes/assignment.rs b/forge_core/src/compiler/ast_nodes/assignment.rs index eea0d21..4f34d50 100644 --- a/forge_core/src/compiler/ast_nodes/assignment.rs +++ b/forge_core/src/compiler/ast_nodes/assignment.rs @@ -15,3 +15,42 @@ impl Compilable for Assignment { Ok(()) } } + +#[cfg(test)] +mod test { + use super::*; + use crate::compiler::test_utils::*; + + #[test] + fn test_subscripts() { + assert_eq!( + test_body(state_for("fn test(x) { x[3] = 5; }")), + vec![ + "push 5", // rvalue + "peekr", "loadw", // Base of x + "push 3", // index + "mul 3", // as byte offset + "add", // addr of x[3] + "storew" + ].join("\n") + ) + } + + #[test] + fn test_zero_subscripts() { + assert_eq!( + test_body(state_for("fn test() { var x = new(5); x[0] = 5; }")), + vec![ + "push 5", // size of array + "mul 3","popr","swap","popr","swap","pick 1","add","pushr","swap","pushr", // alloc call + "peekr","storew", // store addr in x + "push 5", // rvalue + "peekr", "loadw", // Base of x + "push 0", // index + "mul 3", // as byte offset + "add", // addr of x[0] + "storew" + ].join("\n") + ) + } +} \ No newline at end of file diff --git a/forge_core/src/compiler/ast_nodes/expr.rs b/forge_core/src/compiler/ast_nodes/expr.rs index 98a36eb..5c56a09 100644 --- a/forge_core/src/compiler/ast_nodes/expr.rs +++ b/forge_core/src/compiler/ast_nodes/expr.rs @@ -86,8 +86,19 @@ impl Compilable for Expr { Ok(()) } Expr::Call(call) => call.process(state, Some(sig), loc), - Expr::New(_size) => todo!("new not yet supported"), - Expr::Subscript(_, _) => todo!("Structs and arrays are not yet supported"), + Expr::New(size_expr) => { + size_expr.0.process(state, Some(sig), loc)?; + compile_alloc(&mut sig); + Ok(()) + }, + Expr::Subscript(array, index) => { + array.0.process(state, Some(sig), loc)?; + index.0.process(state, Some(sig), loc)?; + sig.emit_arg("mul", 3); // Indices are in words, convert to byte offset + sig.emit("add"); // Add offset + sig.emit("loadw"); + Ok(()) + }, Expr::Infix(lhs, op, rhs) => { // Recurse on expressions, handling operators (*lhs.0).process(state, Some(&mut sig), loc)?; @@ -141,6 +152,28 @@ impl Compilable for Expr { } } +/// Compile the code to run an alloc. This is just generated in-place for now. +fn compile_alloc(sig: &mut CompiledFn) { + // We have the size we want to alloc on top of the stack, in words. So first turn it to bytes: + sig.emit_arg("mul", 3); + + // We need to pull off the frame ptr and pool ptr, so the stack looks like ( frame pool size ) + sig.emit("popr"); + sig.emit("swap"); + sig.emit("popr"); + sig.emit("swap"); + + // We need to leave the pool pointer (as it was at the start) on top of the stack. So, copy it + // and move it forward, so the stack is ( frame old-pool new-pool ) + sig.emit_arg("pick", 1); + sig.emit("add"); + + // Put the new pool in place and the frame on top of it, so the stack is just ( old-pool ): + sig.emit("pushr"); + sig.emit("swap"); + sig.emit("pushr"); +} + #[cfg(test)] mod test { use crate::compiler::test_utils::*; @@ -220,4 +253,33 @@ mod test { .join("\n") ) } + + #[test] + fn test_alloc() { + assert_eq!( + test_body(state_for("fn test() { var x = new(10); return x[2]; }")), + vec![ + "push 10", // eval the size + "mul 3", // turn to bytes + "popr", // take out the frame / old-pool + "swap", + "popr", + "swap", + "pick 1", // construct new-pool + "add", + "pushr", // put new-pool in place + "swap", // put frame back in place + "pushr", + "peekr", // store new-pool in x + "storew", + "peekr", // Get x + "loadw", + "push 2", // Index... + "mul 3", // ...to byte offset... + "add", // ..added to base... + "loadw", // ...And loaded + "jmpr @_forge_gensym_2", // (and returned) + ].join("\n") + ) + } } \ 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 index e2a0441..dec3d01 100644 --- a/forge_core/src/compiler/ast_nodes/global.rs +++ b/forge_core/src/compiler/ast_nodes/global.rs @@ -8,7 +8,7 @@ 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") + todo!("Global arrays are not yet supported") } state.add_global(&self.name, |s| Variable::IndirectLabel(s.gensym())) } diff --git a/forge_core/src/compiler/ast_nodes/lvalue.rs b/forge_core/src/compiler/ast_nodes/lvalue.rs index 1fd0b93..9c2c49a 100644 --- a/forge_core/src/compiler/ast_nodes/lvalue.rs +++ b/forge_core/src/compiler/ast_nodes/lvalue.rs @@ -57,8 +57,12 @@ impl Compilable for Lvalue { Expr::Deref(BoxExpr(expr)) => { (*expr).process(state, Some(sig), loc) } - Expr::Subscript(_, _) => { - Err(CompileError(0, 0, String::from("Arrays are not yet supported"))) + Expr::Subscript(array, index) => { + array.0.process(state, Some(sig), loc)?; + index.0.process(state, Some(sig), loc)?; + sig.emit_arg("mul", 3); // Indices are in words, convert to byte offset + sig.emit("add"); // Add offset + Ok(()) } } } diff --git a/forge_core/src/compiler/compiled_fn.rs b/forge_core/src/compiler/compiled_fn.rs index 48f2f02..d0abc6e 100644 --- a/forge_core/src/compiler/compiled_fn.rs +++ b/forge_core/src/compiler/compiled_fn.rs @@ -31,11 +31,6 @@ use crate::compiler::utils::{Label, Scope, Variable}; /// memory for any reason (like, new(), or calling a fn and giving it a valid frame ptr), this is /// what we pass. /// -/// 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 /// @@ -72,6 +67,7 @@ impl CompiledFn { let frame = self.frame_size(); if let Vacant(e) = self.local_scope.entry(name.into()) { e.insert(Variable::Local(frame)); + let frame = self.frame_size(); self.max_frame_size = max(frame, self.max_frame_size); Ok(()) } else { @@ -139,8 +135,8 @@ impl CompiledFn { // (the frame ptr) plus a (known) max scope size: self.preamble_emit("dup"); if self.max_frame_size > 0 { - // Pool ptr is right after the locals, so, max_frame_size + 3 - self.preamble_emit_arg("add", self.max_frame_size + 3); + // Pool ptr is right after the locals, so, add max_frame_size + self.preamble_emit_arg("add", self.max_frame_size); } self.preamble_emit("pushr"); @@ -212,3 +208,51 @@ impl CompiledFn { self.reduce_frame_size_to(self.frame_size() - 3); } } + +#[cfg(test)] +mod test { + use super::*; + use crate::compiler::test_utils::*; + + #[test] + fn max_frame_size_args() { + assert_eq!(test_preamble(state_for("fn test(a) { return 5; }")), + vec![ + "dup", // ( frame frame ) + "add 3", // add max size to frame to get ( frame pool ) + "pushr", // save pool ptr + "pushr", // save frame ptr + "peekr", // store param in 'a' + "storew", + ].join("\n") + ) + } + + #[test] + fn max_frame_size_locals() { + assert_eq!(test_preamble(state_for("fn test() { var x; return 5; }")), + vec![ + "dup", // ( frame frame ) + "add 3", // add max size to frame to get ( frame pool ) + "pushr", // save pool ptr + "pushr", // save frame ptr + ].join("\n") + ) + } + + #[test] + fn max_frame_size_block_scope() { + assert_eq!(test_preamble(state_for("fn test(p) { var x; if(3) { var y; } var z; }")), + vec![ + "dup", // ( frame frame ) + // This is the test: p and x are always in scope; y is created but leaves + // scope before z enters so z can reuse y's slot + "add 9", // add max size to frame to get ( frame pool ) + "pushr", // save pool ptr + "pushr", // save frame ptr + "peekr", // Capture param p + "storew", + ].join("\n") + ) + } +} \ No newline at end of file diff --git a/vtest/src/forge_tests.rs b/vtest/src/forge_tests.rs index a0dc8b7..b3b6416 100644 --- a/vtest/src/forge_tests.rs +++ b/vtest/src/forge_tests.rs @@ -10,6 +10,7 @@ fn run_forge(src: &str) -> CPU { cpu } +#[allow(dead_code)] fn compiler_output(src: &str) -> Vec { forge_core::compiler::build_boot(src).unwrap() } @@ -102,6 +103,29 @@ fn recursion_test() { ) } +#[test] +fn array_test() { + // Declare an array and do things + assert_eq!( + main_return( + "fn sum(arr) { + var i = 0; + var s = 0; + while (arr[i] != -1) { + s = arr[i] + s; + i = i + 1; + } + return s; + } + fn main() { + var a = new(5); + a[0] = 1; a[1] = 3; a[2] = 5; a[3] = -1; a[4] = 10; + return sum(a); + }"), + 9 + ) +} + //#[test] // This is no longer cursed, or a test. The revised calling convention with the pool pointer makes // it now perfectly sane. Left here for posterity.