framy stack

This commit is contained in:
nora 2022-04-26 20:51:36 +02:00
parent 6563a7bd6c
commit cbc60bf472
4 changed files with 146 additions and 98 deletions

View file

@ -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};

View file

@ -1,3 +1,4 @@
pub mod bytecode;
pub mod gc;
mod stack_frame;
pub mod vm;

104
src/runtime/stack_frame.rs Normal file
View file

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

View file

@ -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::<usize>());
type PublicVmError = ActualBackingVmError;
pub(super) struct Vm<'bc, 'io> {
// -- global
blocks: &'bc [FnBlock<'bc>],
_alloc: RtAlloc,
pub stack: Vec<Value>,
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<Value>,
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 {