mirror of
https://github.com/Noratrieb/dilaria.git
synced 2026-01-14 17:35:03 +01:00
framy stack
This commit is contained in:
parent
6563a7bd6c
commit
cbc60bf472
4 changed files with 146 additions and 98 deletions
|
|
@ -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};
|
||||
|
||||
|
|
|
|||
|
|
@ -1,3 +1,4 @@
|
|||
pub mod bytecode;
|
||||
pub mod gc;
|
||||
mod stack_frame;
|
||||
pub mod vm;
|
||||
|
|
|
|||
104
src/runtime/stack_frame.rs
Normal file
104
src/runtime/stack_frame.rs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue