mirror of
https://github.com/Noratrieb/dilaria.git
synced 2026-01-16 18:35:02 +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
|
//! It is the compilers job to generate the correct loading of the arguments and assure that the arity
|
||||||
//! is correct before the `Call` instruction.
|
//! is correct before the `Call` instruction.
|
||||||
//!
|
//!
|
||||||
//!
|
//! See [`stack_frame`](`super::stack_frame`) for mode details
|
||||||
//! # 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─╯
|
|
||||||
//!
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::fmt::{Debug, Formatter};
|
use std::fmt::{Debug, Formatter};
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
pub mod bytecode;
|
pub mod bytecode;
|
||||||
pub mod gc;
|
pub mod gc;
|
||||||
|
mod stack_frame;
|
||||||
pub mod vm;
|
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::{
|
runtime::{
|
||||||
bytecode::{FnBlock, Function, Instr},
|
bytecode::{FnBlock, Function, Instr},
|
||||||
gc::{Object, RtAlloc, Symbol},
|
gc::{Object, RtAlloc, Symbol},
|
||||||
|
stack_frame::Frame,
|
||||||
},
|
},
|
||||||
util, Config,
|
util, Config,
|
||||||
};
|
};
|
||||||
|
|
@ -28,6 +29,25 @@ util::assert_size!(VmResult <= std::mem::size_of::<usize>());
|
||||||
|
|
||||||
type PublicVmError = ActualBackingVmError;
|
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>(
|
pub fn execute<'bc>(
|
||||||
bytecode: &'bc [FnBlock<'bc>],
|
bytecode: &'bc [FnBlock<'bc>],
|
||||||
alloc: RtAlloc,
|
alloc: RtAlloc,
|
||||||
|
|
@ -37,7 +57,7 @@ pub fn execute<'bc>(
|
||||||
blocks: bytecode,
|
blocks: bytecode,
|
||||||
current: bytecode.first().ok_or("no bytecode found")?,
|
current: bytecode.first().ok_or("no bytecode found")?,
|
||||||
current_block_index: 0,
|
current_block_index: 0,
|
||||||
stack_offset: 0,
|
stack_frame_offset: 0,
|
||||||
pc: 0,
|
pc: 0,
|
||||||
stack: Vec::with_capacity(1024 << 5),
|
stack: Vec::with_capacity(1024 << 5),
|
||||||
_alloc: alloc,
|
_alloc: alloc,
|
||||||
|
|
@ -80,25 +100,6 @@ util::assert_size!(Value <= 24);
|
||||||
const TRUE: Value = Value::Bool(true);
|
const TRUE: Value = Value::Bool(true);
|
||||||
const FALSE: Value = Value::Bool(false);
|
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, '_> {
|
impl<'bc> Vm<'bc, '_> {
|
||||||
fn execute_function(&mut self) -> VmResult {
|
fn execute_function(&mut self) -> VmResult {
|
||||||
loop {
|
loop {
|
||||||
|
|
@ -109,6 +110,7 @@ impl<'bc> Vm<'bc, '_> {
|
||||||
None => return Ok(()),
|
None => return Ok(()),
|
||||||
}
|
}
|
||||||
if self.pc > 0 {
|
if self.pc > 0 {
|
||||||
|
// this must respect stack frame stuff
|
||||||
// debug_assert_eq!(self.current.stack_sizes[self.pc - 1], self.stack.len());
|
// debug_assert_eq!(self.current.stack_sizes[self.pc - 1], self.stack.len());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -123,9 +125,10 @@ impl<'bc> Vm<'bc, '_> {
|
||||||
Instr::Nop => {}
|
Instr::Nop => {}
|
||||||
Instr::Store(index) => {
|
Instr::Store(index) => {
|
||||||
let val = self.stack.pop().unwrap();
|
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::PushVal(value) => self.stack.push(value),
|
||||||
Instr::Neg => {
|
Instr::Neg => {
|
||||||
let val = self.stack.pop().unwrap();
|
let val = self.stack.pop().unwrap();
|
||||||
|
|
@ -239,21 +242,16 @@ impl<'bc> Vm<'bc, '_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn call(&mut self) -> VmResult {
|
fn call(&mut self) -> VmResult {
|
||||||
let old_offset = self.stack_offset;
|
// save the function to be called
|
||||||
let old_idx = self.current_block_index;
|
let to_be_called_fn = self.stack.pop().unwrap().unwrap_function();
|
||||||
let function = self.stack.pop().unwrap();
|
let to_be_called_fn_block = &self.blocks[to_be_called_fn];
|
||||||
let function = function.unwrap_function();
|
|
||||||
let fn_block = &self.blocks[function];
|
|
||||||
|
|
||||||
let new_stack_frame_start = self.stack.len();
|
// create a new frame (the params are already pushed)
|
||||||
self.stack_offset = new_stack_frame_start;
|
let new_stack_frame_start = Frame::create(self, to_be_called_fn_block.arity);
|
||||||
|
|
||||||
self.stack.push(Value::NativeU(old_offset));
|
self.stack_frame_offset = new_stack_frame_start;
|
||||||
self.stack.push(Value::NativeU(self.pc));
|
self.current_block_index = to_be_called_fn;
|
||||||
self.stack.push(Value::Function(old_idx));
|
self.current = to_be_called_fn_block;
|
||||||
|
|
||||||
self.current_block_index = function;
|
|
||||||
self.current = fn_block;
|
|
||||||
|
|
||||||
self.pc = 0;
|
self.pc = 0;
|
||||||
|
|
||||||
|
|
@ -263,29 +261,27 @@ impl<'bc> Vm<'bc, '_> {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn ret(&mut self) -> VmResult {
|
fn ret(&mut self) -> VmResult {
|
||||||
let current_arity: usize = self.current.arity.try_into().unwrap();
|
|
||||||
|
|
||||||
// we save the return value first.
|
// we save the return value first.
|
||||||
let return_value = self.stack.pop().expect("return value");
|
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
|
// now, we get all the bookkeeping info out
|
||||||
let old_stack_offset = self.stack[bookkeeping_offset].unwrap_native_int();
|
let old_stack_offset = frame.old_stack_offset();
|
||||||
let old_pc = self.stack[bookkeeping_offset + 1].unwrap_native_int();
|
let old_pc = frame.old_pc();
|
||||||
let old_function = self.stack[bookkeeping_offset + 2].unwrap_function();
|
let old_function = frame.old_fn_block();
|
||||||
|
|
||||||
// get the interpreter back to the nice state
|
// 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.pc = old_pc;
|
||||||
self.current_block_index = old_function;
|
self.current_block_index = old_function;
|
||||||
self.current = &self.blocks[old_function];
|
self.current = &self.blocks[old_function];
|
||||||
|
|
||||||
// and kill the function stack frame
|
// and kill the function stack frame
|
||||||
// note: don't emit a return instruction from the whole global script.
|
// 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...
|
// everything that remains...
|
||||||
self.stack.push(return_value);
|
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 {
|
impl Display for Value {
|
||||||
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue