From 325288d7ba5a4b845ce3424e7add18602a9aa61d Mon Sep 17 00:00:00 2001 From: Ross Andrews Date: Mon, 13 Nov 2023 00:32:57 -0600 Subject: [PATCH] A bunch of prerequisites before we can do arrays, part 3, the pool pointer --- forge_core/src/compiler/ast_nodes/block.rs | 2 +- forge_core/src/compiler/ast_nodes/function.rs | 20 +++++++- forge_core/src/compiler/compiled_fn.rs | 49 +++++++++++-------- forge_core/src/compiler/mod.rs | 18 +++++-- forge_core/src/compiler/test_utils.rs | 4 ++ vtest/src/forge_tests.rs | 6 +-- 6 files changed, 70 insertions(+), 29 deletions(-) diff --git a/forge_core/src/compiler/ast_nodes/block.rs b/forge_core/src/compiler/ast_nodes/block.rs index bfb7d1e..89f1024 100644 --- a/forge_core/src/compiler/ast_nodes/block.rs +++ b/forge_core/src/compiler/ast_nodes/block.rs @@ -8,7 +8,7 @@ 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; + let frame_size_before_block = sig.frame_size(); // Compile each statement: for stmt in self.0 { diff --git a/forge_core/src/compiler/ast_nodes/function.rs b/forge_core/src/compiler/ast_nodes/function.rs index 052849d..d234fbe 100644 --- a/forge_core/src/compiler/ast_nodes/function.rs +++ b/forge_core/src/compiler/ast_nodes/function.rs @@ -46,7 +46,7 @@ impl Compilable for Function { #[cfg(test)] mod test { - use crate::compiler::test_utils::{state_for, test_body, test_preamble}; + use crate::compiler::test_utils::*; #[test] fn test_basic_fns() { @@ -70,6 +70,9 @@ mod test { assert_eq!( test_preamble(state_for("fn test(a, b) { b = 17 + a; }")), vec![ + "dup", // Create pool ptr + "add 6", + "pushr", // Store pool ptr "pushr", // Store frame ptr "peekr", // Capture var b "add 3", @@ -80,4 +83,19 @@ mod test { .join("\n") ) } + + #[test] + fn test_args_outro() { + assert_eq!( + test_outro(state_for("fn test(a, b) { b = 17 + a; }")), + vec![ + "push 0", // Default return value if we fall through + "_forge_gensym_2:", // outro label + "popr", "pop", // Toss the frame ptr + "popr", "pop", // Toss the pool ptr + "ret", // Actually return + ] + .join("\n") + ) + } } \ No newline at end of file diff --git a/forge_core/src/compiler/compiled_fn.rs b/forge_core/src/compiler/compiled_fn.rs index 2dc5510..48f2f02 100644 --- a/forge_core/src/compiler/compiled_fn.rs +++ b/forge_core/src/compiler/compiled_fn.rs @@ -1,3 +1,4 @@ +use std::cmp::max; use std::collections::btree_map::Entry::Vacant; use std::fmt::Display; use crate::compiler::compile_error::CompileError; @@ -23,6 +24,13 @@ use crate::compiler::utils::{Label, Scope, Variable}; /// and put that on the top of the data stack. Functions store their pointer in the top of the /// rstack, and locals can be found by adding some offset from the frame pointer. /// +/// The max_frams_size field is important: this is the most entries that can be in scope at one time. +/// The reason that's important is that anything in the stack _after_ that many slots is available +/// for use by the allocator pool. The second entry in the rstack is the "pool pointer" which is the +/// address of the first byte of the stack that we haven't used yet. When we want to allocate more +/// 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 @@ -48,10 +56,9 @@ use crate::compiler::utils::{Label, Scope, Variable}; pub struct CompiledFn { pub label: Label, pub end_label: Label, - pub frame_size: usize, + pub max_frame_size: usize, pub local_scope: Scope, pub arity: usize, - pub alloc: bool, pub preamble: Vec, pub body: Vec, pub outro: Vec, @@ -62,9 +69,10 @@ impl CompiledFn { /// - 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> { + let frame = self.frame_size(); if let Vacant(e) = self.local_scope.entry(name.into()) { - e.insert(Variable::Local(self.frame_size)); - self.frame_size += 3; + e.insert(Variable::Local(frame)); + self.max_frame_size = max(frame, self.max_frame_size); Ok(()) } else { Err(CompileError(0, 0, format!("Duplicate name {}", name))) @@ -104,8 +112,8 @@ impl CompiledFn { self.preamble.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 + /// The (current) size of the local scope in bytes. This increases as variables are declared, + /// decreases as they leave scope pub(crate) fn frame_size(&self) -> usize { self.local_scope.len() * 3 } @@ -127,14 +135,16 @@ impl CompiledFn { /// depends on knowledge of the body of the fn, but what this produces will be emitted to /// the final listing before the fn body pub(crate) fn generate_preamble_outro(&mut self, args: &Vec) -> Result<(), CompileError> { - // Does our fn make any allocations? If so we need to save the old alloc pool pointer: - if self.alloc { - todo!("alloc pool not actually implemented"); - //self.preamble_emit("loadw pool"); - //self.preamble_emit("pushr"); + // First, we need a pool pointer on the rstack. We know this because it's the current top + // (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); } + self.preamble_emit("pushr"); - // Top argument is the frame ptr; copy it to the rstack: + // The top argument we were sent is the frame ptr, store that also: self.preamble_emit("pushr"); // Add each argument as a local @@ -163,14 +173,12 @@ impl CompiledFn { // Now, the outro label: self.outro_emit(format!("{}:", self.end_label).as_str()); - // The top of the rstack is, of course, the frame ptr. So we need to get rid of that: + // The top of the rstack is, of course, the frame ptr and pool ptr. So we need to get rid of + // those: + self.outro_emit("popr"); + self.outro_emit("pop"); self.outro_emit("popr"); self.outro_emit("pop"); - - // If we alloced anything, we need to drain that pool: - if self.alloc { - todo!("alloc pool not implemented yet"); - } // We're in the same condition we entered in except that our return value is on the stack // (or a default 0 is) so time to actually return: @@ -183,11 +191,10 @@ impl CompiledFn { /// 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 { + if *offset >= new_size { to_remove.push(name.clone()); } } @@ -202,6 +209,6 @@ impl CompiledFn { /// 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); + 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 index f0f1f20..67631d9 100644 --- a/forge_core/src/compiler/mod.rs +++ b/forge_core/src/compiler/mod.rs @@ -76,6 +76,8 @@ mod test { "call _forge_gensym_1", "hlt", "_forge_gensym_1:", + "dup", + "pushr", "pushr", "push 5", "jmpr @_forge_gensym_2", @@ -83,6 +85,8 @@ mod test { "_forge_gensym_2:", "popr", "pop", + "popr", + "pop", "ret", "stack: .db 0", ].join("\n")); @@ -96,6 +100,8 @@ mod test { "hlt", "_forge_gensym_1: .db \"blah\\0\"", "_forge_gensym_2:", + "dup", + "pushr", "pushr", "push _forge_gensym_1", // Push the str "jmpr @_forge_gensym_3", // Return @@ -103,6 +109,8 @@ mod test { "_forge_gensym_3:", // outro "popr", "pop", + "popr", + "pop", "ret", "stack: .db 0", ].join("\n")) @@ -122,13 +130,15 @@ mod test { "call _forge_gensym_3", "hlt", "_forge_gensym_1:", // fn foo() + "dup", "pushr", // capture pool ptr "pushr", // capture frame ptr "push 0", // implicit return "_forge_gensym_2:", - "popr", - "pop", + "popr", "pop", + "popr", "pop", "ret", "_forge_gensym_3:", // fn main() + "dup", "pushr", // capture pool ptr "pushr", // capture frame ptr "peekr", // prep frame ptr to send to foo "push _forge_gensym_1", // load foo @@ -136,7 +146,9 @@ mod test { "pop", // Throw away its return value "push 0", // Implicit return value "_forge_gensym_4:", // Outro start - "popr", // Restore frame ptr + "popr", // Drop frame ptr + "pop", + "popr", // Drop pool ptr "pop", "ret", "stack: .db 0", diff --git a/forge_core/src/compiler/test_utils.rs b/forge_core/src/compiler/test_utils.rs index 0501e09..0786eb0 100644 --- a/forge_core/src/compiler/test_utils.rs +++ b/forge_core/src/compiler/test_utils.rs @@ -18,3 +18,7 @@ pub(crate) fn test_body(state: State) -> String { pub(crate) fn test_preamble(state: State) -> String { state.functions.get("test").unwrap().preamble.join("\n") } + +pub(crate) fn test_outro(state: State) -> String { + state.functions.get("test").unwrap().outro.join("\n") +} diff --git a/vtest/src/forge_tests.rs b/vtest/src/forge_tests.rs index 003ba7d..c55deb9 100644 --- a/vtest/src/forge_tests.rs +++ b/vtest/src/forge_tests.rs @@ -108,9 +108,9 @@ fn cursed_call_test() { "call _forge_gensym_3", "hlt", "_forge_gensym_1:", // foo() - "pushr","push 2","jmpr @_forge_gensym_2","push 0","_forge_gensym_2:","popr","pop","ret", + "dup", "pushr", "pushr","push 2","jmpr @_forge_gensym_2","push 0","_forge_gensym_2:","popr","pop", "popr","pop","ret", "_forge_gensym_3:", // main() - "pushr", // preamble (no args) + "dup", "add 6", "pushr", "pushr", // preamble (no args, but 3 locals) "push 0", "peekr", "storew", // repeat counter var (n) "push 2", // repeat limit "#while", @@ -136,7 +136,7 @@ fn cursed_call_test() { "pop", // Drop the loop limit off "push 0", // Implicit return value "_forge_gensym_4:", - "popr","pop","ret", // Standard outro + "popr","pop","popr","pop","ret", // Standard outro "stack: .db 0", // The stack ].join("\n")) } \ No newline at end of file