diff --git a/src/runtime/bytecode.rs b/src/runtime/bytecode.rs index 867f8ea..4043aa7 100644 --- a/src/runtime/bytecode.rs +++ b/src/runtime/bytecode.rs @@ -23,40 +23,7 @@ //! It is the compilers job to generate the correct loading of the arguments and assure that the arity //! is correct before the `Call` instruction. //! -//! -//! # ABI -//! Function arguments are passed on the stack and can be loaded just like local variables. They belong -//! to the stack frame of the new function and are cleaned up after returning, leaving the return value where -//! the stack frame was -//! -//! When a call happens, the current stack offset is pushed onto the stack as a `Value::Native` and -//! the element before it is stored as the new offset. -//! Then all parameters are pushed onto the stack, from first to last -//! Afterwards, execution of the code is started. A function always has to return, and compiler -//! inserts `return null` at the end of every function implicitly. -//! -//! If a return happens, the VM loads the current value on the stack. It then goes to the start -//! of the stack frame and saves the `Value::Native` that stores the old stack offset and loads that -//! into its stack offset. It then removes the whole stack frame from the stack, and pushes the -//! returned value. -//! -//! ```text -//! old stack offset─╮ -//! ╭─Parameters─╮ │ old Function─╮ local─╮ -//! v v v v v -//! ───────┬─────────┬──────────┬─────────────┬────────────┬──────────┬─────────╮ -//! Num(6) │ Num(5) │ Num(6) │ NativeU(20) │ NativeU(4) │ Function │ Num(5) │ -//! ───────┴─────────┴──────────┴─────────────┴────────────┴──────────┴─────────╯ -//! ^ ╰────────────────────────────────────────────────────────────────── current stack frame -//! │ ^ -//! ╰─ old local ╰─old PC -//! -//! ^ -//! Vm ╰────────────╮ -//! │ -//! Current stack offset─╯ -//! -//! ``` +//! See [`stack_frame`](`super::stack_frame`) for mode details use std::fmt::{Debug, Formatter}; diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 36ee8b3..03dabe3 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -1,3 +1,4 @@ pub mod bytecode; pub mod gc; +mod stack_frame; pub mod vm; diff --git a/src/runtime/stack_frame.rs b/src/runtime/stack_frame.rs new file mode 100644 index 0000000..68b7b43 --- /dev/null +++ b/src/runtime/stack_frame.rs @@ -0,0 +1,104 @@ +//! # ABI +//! Function arguments are passed on the stack and can be loaded just like local variables. They belong +//! to the stack frame of the new function and are cleaned up after returning, leaving the return value where +//! the stack frame was +//! +//! When a call happens, the current stack offset is pushed onto the stack as a `Value::Native` and +//! the element before it is stored as the new offset. +//! Then all parameters are pushed onto the stack, from first to last +//! Afterwards, execution of the code is started. A function always has to return, and compiler +//! inserts `return null` at the end of every function implicitly. +//! +//! If a return happens, the VM loads the current value on the stack. It then goes to the start +//! of the stack frame and saves the `Value::Native` that stores the old stack offset and loads that +//! into its stack offset. It then removes the whole stack frame from the stack, and pushes the +//! returned value. +//! +//! ```text +//! old stack frame offset─╮ +//! ╭─Parameters─╮ │ old Function─╮ local─╮ +//! v v v v v +//! ───────┬─────────┬──────────┬─────────────┬────────────┬──────────┬─────────╮ +//! Num(6) │ Num(5) │ Num(6) │ NativeU(20) │ NativeU(4) │ Function │ Num(5) │ +//! ───────┴─────────┴──────────┴─────────────┴────────────┴──────────┴─────────╯ +//! ^ ╰────────────────────────────────────────────────────────────────── current stack frame +//! │ ^ +//! ╰─ old local ╰─old PC +//! +//! ^ +//! Vm ╰──────────────────╮ +//! │ +//! Current stack frame offset─╯ +//! +//! ``` + +use crate::runtime::{ + bytecode::Function, + vm::{Value, Vm}, +}; + +pub struct Frame<'s> { + frame_slice: &'s [Value], + params: u32, +} + +impl<'s> Frame<'s> { + /// Create a new stack frame with the VM state. The VM must set its state to the new function itself. + /// The parameters need to be pushed already and be the topmost values on the stack. + /// + /// Returns the new stack frame offset + pub(super) fn create(vm_state: &'s mut Vm, params: u32) -> usize { + let new_frame_offset = vm_state.stack.len() - (params as usize); + + let old_stack_offset = vm_state.stack_frame_offset; + let old_fn_block = vm_state.current_block_index; + let old_pc = vm_state.pc; + + vm_state.stack.push(Value::NativeU(old_stack_offset)); + vm_state.stack.push(Value::NativeU(old_pc)); + vm_state.stack.push(Value::Function(old_fn_block)); + + let frame_slice = &vm_state.stack[new_frame_offset..]; + + new_frame_offset + } + + pub fn new(frame_slice: &'s [Value], params: u32) -> Self { + Self { + frame_slice, + params, + } + } + + pub fn old_stack_offset(&self) -> usize { + self.frame_slice[self.params as usize].unwrap_native_int() + } + + pub fn old_pc(&self) -> usize { + self.frame_slice[self.params as usize + 1].unwrap_native_int() + } + + pub fn old_fn_block(&self) -> Function { + self.frame_slice[self.params as usize + 2].unwrap_function() + } +} + +impl Value { + /// Unwrap the Value into a `usize` expecting the `NativeU` variant + fn unwrap_native_int(&self) -> usize { + if let Value::NativeU(n) = self { + *n + } else { + unreachable!("expected native int, got {:?}", self); + } + } + + /// Unwrap the Value into a `Function` expecting the `Function` variant + pub fn unwrap_function(&self) -> Function { + if let Value::Function(fun) = self { + *fun + } else { + unreachable!("expected function, got {:?}", self); + } + } +} diff --git a/src/runtime/vm.rs b/src/runtime/vm.rs index 0c6c77d..bba47aa 100644 --- a/src/runtime/vm.rs +++ b/src/runtime/vm.rs @@ -7,6 +7,7 @@ use crate::{ runtime::{ bytecode::{FnBlock, Function, Instr}, gc::{Object, RtAlloc, Symbol}, + stack_frame::Frame, }, util, Config, }; @@ -28,6 +29,25 @@ util::assert_size!(VmResult <= std::mem::size_of::()); type PublicVmError = ActualBackingVmError; +pub(super) struct Vm<'bc, 'io> { + // -- global + blocks: &'bc [FnBlock<'bc>], + _alloc: RtAlloc, + pub stack: Vec, + stdout: &'io mut dyn Write, + step: bool, + + // -- local to the current function + /// The current function + current: &'bc FnBlock<'bc>, + pub current_block_index: usize, + /// The offset of the first parameter of the current function + pub stack_frame_offset: usize, + /// Index of the next instruction being executed. is out of bounds if the current + /// instruction is the last one + pub pc: usize, +} + pub fn execute<'bc>( bytecode: &'bc [FnBlock<'bc>], alloc: RtAlloc, @@ -37,7 +57,7 @@ pub fn execute<'bc>( blocks: bytecode, current: bytecode.first().ok_or("no bytecode found")?, current_block_index: 0, - stack_offset: 0, + stack_frame_offset: 0, pc: 0, stack: Vec::with_capacity(1024 << 5), _alloc: alloc, @@ -80,25 +100,6 @@ util::assert_size!(Value <= 24); const TRUE: Value = Value::Bool(true); const FALSE: Value = Value::Bool(false); -struct Vm<'bc, 'io> { - // -- global - blocks: &'bc [FnBlock<'bc>], - _alloc: RtAlloc, - stack: Vec, - stdout: &'io mut dyn Write, - step: bool, - - // -- local to the current function - /// The current function - current: &'bc FnBlock<'bc>, - current_block_index: usize, - /// The offset of the first parameter of the current function - stack_offset: usize, - /// Index of the next instruction being executed. is out of bounds if the current - /// instruction is the last one - pc: usize, -} - impl<'bc> Vm<'bc, '_> { fn execute_function(&mut self) -> VmResult { loop { @@ -109,6 +110,7 @@ impl<'bc> Vm<'bc, '_> { None => return Ok(()), } if self.pc > 0 { + // this must respect stack frame stuff // debug_assert_eq!(self.current.stack_sizes[self.pc - 1], self.stack.len()); } } @@ -123,9 +125,10 @@ impl<'bc> Vm<'bc, '_> { Instr::Nop => {} Instr::Store(index) => { let val = self.stack.pop().unwrap(); - self.stack[self.stack_offset + index] = val; + self.stack[self.stack_frame_offset + index] = val; } - Instr::Load(index) => self.stack.push(self.stack[self.stack_offset + index]), + // todo: no no no no no no this is wrong + Instr::Load(index) => self.stack.push(self.stack[self.stack_frame_offset + index]), Instr::PushVal(value) => self.stack.push(value), Instr::Neg => { let val = self.stack.pop().unwrap(); @@ -239,21 +242,16 @@ impl<'bc> Vm<'bc, '_> { } fn call(&mut self) -> VmResult { - let old_offset = self.stack_offset; - let old_idx = self.current_block_index; - let function = self.stack.pop().unwrap(); - let function = function.unwrap_function(); - let fn_block = &self.blocks[function]; + // save the function to be called + let to_be_called_fn = self.stack.pop().unwrap().unwrap_function(); + let to_be_called_fn_block = &self.blocks[to_be_called_fn]; - let new_stack_frame_start = self.stack.len(); - self.stack_offset = new_stack_frame_start; + // create a new frame (the params are already pushed) + let new_stack_frame_start = Frame::create(self, to_be_called_fn_block.arity); - self.stack.push(Value::NativeU(old_offset)); - self.stack.push(Value::NativeU(self.pc)); - self.stack.push(Value::Function(old_idx)); - - self.current_block_index = function; - self.current = fn_block; + self.stack_frame_offset = new_stack_frame_start; + self.current_block_index = to_be_called_fn; + self.current = to_be_called_fn_block; self.pc = 0; @@ -263,29 +261,27 @@ impl<'bc> Vm<'bc, '_> { } fn ret(&mut self) -> VmResult { - let current_arity: usize = self.current.arity.try_into().unwrap(); - // we save the return value first. let return_value = self.stack.pop().expect("return value"); - let bookkeeping_offset = self.stack_offset + current_arity; + let frame = Frame::new(&self.stack[self.stack_frame_offset..], self.current.arity); - let inner_stack_offset = self.stack_offset; + let inner_stack_frame_start = self.stack_frame_offset; // now, we get all the bookkeeping info out - let old_stack_offset = self.stack[bookkeeping_offset].unwrap_native_int(); - let old_pc = self.stack[bookkeeping_offset + 1].unwrap_native_int(); - let old_function = self.stack[bookkeeping_offset + 2].unwrap_function(); + let old_stack_offset = frame.old_stack_offset(); + let old_pc = frame.old_pc(); + let old_function = frame.old_fn_block(); // get the interpreter back to the nice state - self.stack_offset = old_stack_offset; + self.stack_frame_offset = old_stack_offset; self.pc = old_pc; self.current_block_index = old_function; self.current = &self.blocks[old_function]; // and kill the function stack frame // note: don't emit a return instruction from the whole global script. - unsafe { self.stack.set_len(inner_stack_offset) }; + unsafe { self.stack.set_len(inner_stack_frame_start) }; // everything that remains... self.stack.push(return_value); @@ -311,26 +307,6 @@ Stack: {:?}", } } -impl Value { - /// Unwrap the Value into a `usize` expecting the `NativeU` variant - fn unwrap_native_int(&self) -> usize { - if let Value::NativeU(n) = self { - *n - } else { - unreachable!("expected native int, got {:?}", self); - } - } - - /// Unwrap the Value into a `Function` expecting the `Function` variant - fn unwrap_function(&self) -> Function { - if let Value::Function(fun) = self { - *fun - } else { - unreachable!("expected function, got {:?}", self); - } - } -} - impl Display for Value { fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { match self {