175 lines
5.8 KiB
Rust
175 lines
5.8 KiB
Rust
use std::ops::ControlFlow;
|
|
use std::path::{Path, PathBuf};
|
|
|
|
use fix_error::Error;
|
|
use fix_lang::{self, StringId};
|
|
use gc_arena::Mutation;
|
|
|
|
use crate::{
|
|
Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad, Step,
|
|
StrictValue, Value, VmError,
|
|
};
|
|
|
|
/// Abstract VM-side operations consumed by instruction handlers and primops.
|
|
///
|
|
/// Implementors maintain a value stack, a call stack, an environment chain,
|
|
/// pending result/error state, and a set of GC-allocated globals. Methods
|
|
/// fall into a few groups:
|
|
///
|
|
/// - Stack ops (`push` / `pop` / `peek` / `replace` / `pop_forced` / ...)
|
|
/// - Forcing primitives (`force_slot` / `force_slot_to_pc`)
|
|
/// - Calling (`call` / `return_from_primop`)
|
|
/// - Call-frame management (`push_call_frame` / `pop_call_frame` / call-depth)
|
|
/// - Environment access (`env` / `set_env` / `local`)
|
|
/// - Result finalization (`finish_ok` / `finish_err` / ...)
|
|
/// - Global lookup (`builtins` / `empty_list` / `empty_attrs` / ...)
|
|
/// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`)
|
|
pub trait Machine<'gc> {
|
|
fn push(&mut self, val: Value<'gc>);
|
|
fn pop(&mut self) -> Value<'gc>;
|
|
fn peek(&self, depth: usize) -> Value<'gc>;
|
|
fn peek_forced(&self, depth: usize) -> StrictValue<'gc>;
|
|
fn pop_forced(&mut self) -> StrictValue<'gc>;
|
|
fn replace(&mut self, depth: usize, val: Value<'gc>);
|
|
fn stack_len(&self) -> usize;
|
|
|
|
fn force_slot_to_pc(
|
|
&mut self,
|
|
depth: usize,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
resume_pc: usize,
|
|
) -> Step;
|
|
|
|
#[inline(always)]
|
|
fn force_slot(
|
|
&mut self,
|
|
depth: usize,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> Step {
|
|
let pc = reader.inst_start_pc();
|
|
self.force_slot_to_pc(depth, reader, mc, pc)
|
|
}
|
|
|
|
fn call(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
arg: Value<'gc>,
|
|
resume_pc: usize,
|
|
) -> Step;
|
|
|
|
#[inline(always)]
|
|
fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step {
|
|
self.push(val);
|
|
let Some(CallFrame {
|
|
pc: ret_pc,
|
|
thunk: _,
|
|
env,
|
|
}) = self.pop_call_frame()
|
|
else {
|
|
unreachable!()
|
|
};
|
|
reader.set_pc(ret_pc);
|
|
self.dec_call_depth();
|
|
self.set_env(env);
|
|
Step::Continue(())
|
|
}
|
|
|
|
fn push_call_frame(&mut self, frame: CallFrame<'gc>);
|
|
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>>;
|
|
fn call_depth(&self) -> usize;
|
|
fn inc_call_depth(&mut self);
|
|
fn dec_call_depth(&mut self);
|
|
|
|
fn env(&self) -> GcEnv<'gc>;
|
|
fn set_env(&mut self, env: GcEnv<'gc>);
|
|
|
|
#[inline(always)]
|
|
fn local(&self, layer: u8, idx: u32) -> Value<'gc> {
|
|
let mut cur = self.env();
|
|
for _ in 0..layer {
|
|
let prev = cur.borrow().prev.expect("env chain too short");
|
|
cur = prev;
|
|
}
|
|
cur.borrow().locals[idx as usize]
|
|
}
|
|
|
|
fn finish_ok(&mut self, val: fix_lang::Value) -> Step;
|
|
fn finish_err(&mut self, err: Box<Error>) -> Step;
|
|
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step;
|
|
|
|
#[inline(always)]
|
|
fn finish_vm_err(&mut self, err: VmError) -> Step {
|
|
self.finish_err(err.into_error())
|
|
}
|
|
|
|
fn builtins(&self) -> Value<'gc>;
|
|
fn functor_sym(&self) -> StringId;
|
|
fn empty_list(&self) -> Value<'gc>;
|
|
fn empty_attrs(&self) -> Value<'gc>;
|
|
fn force_mode(&self) -> ForceMode;
|
|
|
|
fn import_cache_get(&self, path: &Path) -> Option<Value<'gc>>;
|
|
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>);
|
|
fn scope_slot(&self, idx: u32) -> Value<'gc>;
|
|
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32;
|
|
fn set_pending_load(&mut self, load: PendingLoad);
|
|
}
|
|
|
|
/// Extension trait with convenience helpers built on top of [`Machine`].
|
|
///
|
|
/// Auto-implemented for every `Machine<'gc>` so callers just need to bring
|
|
/// `MachineExt` (or `Machine`) into scope.
|
|
pub trait MachineExt<'gc>: Machine<'gc> {
|
|
/// Force the top `T::WIDTH` stack slots and return them as `T`.
|
|
///
|
|
/// If any slot holds a pending thunk, this method pushes a call frame
|
|
/// whose resume PC is the **start of the current instruction**
|
|
/// (`reader.inst_start_pc()`), enters the thunk, and returns
|
|
/// `Break::Force`. When the thunk eventually returns, the VM will
|
|
/// **re-execute the entire opcode handler from the beginning**.
|
|
///
|
|
/// # Invariants
|
|
///
|
|
/// * **Do not call this method more than once in a single handler.**
|
|
/// If you need to force multiple values, use a tuple type such as
|
|
/// `(StrictValue, StrictValue)` so they are forced and popped in one
|
|
/// atomic operation.
|
|
/// * The stack layout at the call site must be **identical** every time
|
|
/// the handler is re-entered.
|
|
/// * Propagate the return value with `?` so `Break::Force` correctly
|
|
/// unwinds to the dispatch loop.
|
|
#[inline(always)]
|
|
fn force_and_retry<T: Forced<'gc>>(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
) -> ControlFlow<Break, T>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
let pc = reader.inst_start_pc();
|
|
self.force_and_retry_pc(reader, mc, pc)
|
|
}
|
|
|
|
/// Same as [`force_and_retry`](Self::force_and_retry) but allows
|
|
/// specifying a custom resume PC.
|
|
#[inline(always)]
|
|
fn force_and_retry_pc<T: Forced<'gc>>(
|
|
&mut self,
|
|
reader: &mut BytecodeReader<'_>,
|
|
mc: &Mutation<'gc>,
|
|
resume_pc: usize,
|
|
) -> ControlFlow<Break, T>
|
|
where
|
|
Self: Sized,
|
|
{
|
|
T::force_and_check(self, reader, mc, 0, resume_pc)?;
|
|
ControlFlow::Continue(T::pop_converted(self))
|
|
}
|
|
}
|
|
|
|
impl<'gc, M: Machine<'gc>> MachineExt<'gc> for M {}
|