refactor: abstract VM

This commit is contained in:
2026-05-13 18:28:18 +08:00
parent 21899f7380
commit 29fab93cd1
42 changed files with 1823 additions and 1410 deletions
+165 -285
View File
@@ -13,222 +13,17 @@ use fix_common::StringId;
use fix_error::{Error, Result, Source};
use gc_arena::arena::CollectionPhase;
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
use hashbrown::{HashMap, HashSet};
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use smallvec::SmallVec;
mod boxing;
mod bytecode_reader;
#[cfg(feature = "tailcall")]
mod dispatch_tailcall;
mod forced;
mod value;
pub use value::StaticValue;
use value::*;
mod helpers;
pub use fix_abstract_vm::*;
mod instructions;
use bytecode_reader::BytecodeReader;
use forced::Forced;
use helpers::*;
mod primops;
type VmResult<T> = std::result::Result<T, VmError>;
#[allow(dead_code)]
enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)]
#[collect(require_static)]
pub enum ForceMode {
#[default]
AsIs,
Shallow,
Deep,
}
pub trait VmContext {
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx);
}
pub trait VmRuntimeCtx {
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId;
fn resolve_string(&self, id: StringId) -> &str;
fn get_const(&self, id: u32) -> StaticValue;
fn add_const(&mut self, val: StaticValue) -> u32;
}
pub trait VmCode {
fn bytecode(&self) -> &[u8];
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<ExtraScope>,
ctx: &mut impl VmRuntimeCtx,
) -> fix_error::Result<InstructionPtr>;
}
/// Extra scope passed to a re-entrant compile from inside a running VM.
///
/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin),
/// but the variant is kept open so REPL bindings could later land here too.
pub enum ExtraScope {
ScopedImport {
keys: HashSet<StringId>,
slot_id: u32,
},
}
trait VmRuntimeCtxExt: VmRuntimeCtx {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>;
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType>;
fn convert_value(&self, val: Value) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
/// Like `get_string`, but also accepts `Path` values (returning their
/// underlying canonical-path string). Use this in places where Nix
/// would coerce a path to a string (string interpolation, file IO
/// builtins, etc.).
fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(p) = val.as_inline::<Path>() {
Some(self.resolve_string(p.0))
} else {
self.get_string(val)
}
}
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType> {
if let Some(sid) = val.as_inline::<StringId>() {
Ok(sid)
} else if let Some(s) = val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str()) {
Ok(self.intern_string(s))
} else {
Err(val.ty())
}
}
fn convert_value(&self, val: Value) -> fix_common::Value {
self.convert_value_with_seen(val, &mut HashSet::new())
}
}
trait ConvertValueWithSeen: VmRuntimeCtx {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value {
use fix_common::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(p) = val.as_inline::<Path>() {
Value::Path(self.resolve_string(p.0).to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
let bits = val.to_bits();
if attrs.entries.is_empty() {
return Value::AttrSet(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let mut map = std::collections::BTreeMap::new();
for &(key, val) in attrs.entries.iter() {
let key = self.resolve_string(key).to_owned();
let converted = self.convert_value_with_seen(val, seen);
map.insert(fix_common::Symbol::from(key), converted);
}
Value::AttrSet(fix_common::AttrSet::new(map))
} else if let Some(list) = val.as_gc::<List>() {
let bits = val.to_bits();
if list.inner.borrow().is_empty() {
return Value::List(Default::default());
}
if !seen.insert(bits) {
return Value::Repeated;
}
let items: Vec<_> = list
.inner
.borrow()
.iter()
.copied()
.map(|v| self.convert_value_with_seen(v, seen))
.collect();
Value::List(fix_common::List::new(items))
} else if val.is::<Closure>() {
Value::Func
} else if let Some(thunk) = val.as_gc::<Thunk>() {
if let ThunkState::Evaluated(v) = *thunk.borrow() {
self.convert_value_with_seen(v.relax(), seen)
} else {
Value::Thunk
}
} else if let Some(primop) = val.as_inline::<PrimOp>() {
let name = fix_builtins::BUILTINS[primop.id as usize].0;
Value::PrimOp(name.strip_prefix("__").unwrap_or(name))
} else if let Some(app) = val.as_gc::<PrimOpApp>() {
let name = fix_builtins::BUILTINS[app.primop.id as usize].0;
Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name))
} else {
Value::Null
}
}
}
#[repr(u8)]
enum Break {
Force,
Done,
LoadFile,
}
type Step = std::ops::ControlFlow<Break>;
#[derive(Collect)]
#[collect(no_drop)]
pub struct Vm<'gc> {
@@ -259,66 +54,6 @@ pub struct Vm<'gc> {
functor_sym: StringId,
}
#[derive(Debug)]
pub(crate) struct PendingLoad {
pub path: PathBuf,
pub scope: Option<PendingScope>,
}
#[derive(Debug)]
pub(crate) struct PendingScope {
pub keys: HashSet<StringId>,
pub slot_id: u32,
}
enum OperandData {
Const(StaticValue),
BigInt(i64),
Local { layer: u8, idx: u32 },
BuiltinConst(StringId),
Builtins,
ReplBinding(StringId),
ScopedImportBinding { slot_id: u32, name: StringId },
}
impl OperandData {
fn resolve<'gc>(&self, mc: &Mutation<'gc>, root: &Vm<'gc>) -> Value<'gc> {
use OperandData::*;
match *self {
Const(sv) => sv.into(),
BigInt(val) => Value::new_gc(Gc::new(mc, val)),
Local { layer, idx } => {
let mut cur = root.env;
for _ in 0..layer {
let prev = cur.borrow().prev.expect("env chain too short");
cur = prev;
}
cur.borrow().locals[idx as usize]
}
#[allow(clippy::unwrap_used)]
BuiltinConst(id) => root
.builtins
.as_gc::<AttrSet>()
.unwrap()
.lookup(id)
.unwrap(),
Builtins => root.builtins,
ReplBinding(_id) => todo!(),
ScopedImportBinding { slot_id, name } => {
#[allow(clippy::unwrap_used)]
let scope = root
.scope_slots
.get(slot_id as usize)
.expect("invalid scope slot");
#[allow(clippy::unwrap_used)]
let attrs = scope.as_gc::<AttrSet>().expect("scope must be attrset");
#[allow(clippy::unwrap_used)]
attrs.lookup(name).expect("scoped binding not found")
}
}
}
}
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> {
let mut entries = SmallVec::with_capacity(BUILTINS.len());
@@ -441,7 +176,7 @@ impl<'gc> Vm<'gc> {
#[inline(always)]
#[must_use]
fn peek(&mut self, depth: usize) -> Value<'gc> {
fn peek(&self, depth: usize) -> Value<'gc> {
*self
.stack
.get(self.stack.len() - depth - 1)
@@ -450,7 +185,7 @@ impl<'gc> Vm<'gc> {
#[inline(always)]
#[must_use]
fn peek_forced(&mut self, depth: usize) -> StrictValue<'gc> {
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
self.stack
.get(self.stack.len() - depth - 1)
.expect("stack underflow")
@@ -580,18 +315,168 @@ impl<'gc> Vm<'gc> {
}
}
#[allow(dead_code)]
struct ErrorFrame {
span_id: u32,
message: Option<String>,
}
impl<'gc> Machine<'gc> for Vm<'gc> {
#[inline(always)]
fn push(&mut self, val: Value<'gc>) {
self.push(val);
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
struct CallFrame<'gc> {
pc: usize,
thunk: Option<Gc<'gc, Thunk<'gc>>>,
env: Gc<'gc, RefLock<Env<'gc>>>,
#[inline(always)]
fn pop(&mut self) -> Value<'gc> {
self.pop()
}
#[inline(always)]
fn peek(&self, depth: usize) -> Value<'gc> {
Vm::peek(self, depth)
}
#[inline(always)]
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
Vm::peek_forced(self, depth)
}
#[inline(always)]
fn pop_forced(&mut self) -> StrictValue<'gc> {
self.pop_forced()
}
#[inline(always)]
fn replace(&mut self, depth: usize, val: Value<'gc>) {
self.replace(depth, val);
}
#[inline(always)]
fn stack_len(&self) -> usize {
self.stack.len()
}
#[inline(always)]
fn force_slot_to_pc(
&mut self,
depth: usize,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> Step {
self.force_slot_to_pc(depth, reader, mc, resume_pc)
}
#[inline(always)]
fn call(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
arg: Value<'gc>,
resume_pc: usize,
) -> Step {
self.call(reader, mc, arg, resume_pc)
}
#[inline(always)]
fn push_call_frame(&mut self, frame: CallFrame<'gc>) {
self.call_stack.push(frame);
}
#[inline(always)]
fn pop_call_frame(&mut self) -> Option<CallFrame<'gc>> {
self.call_stack.pop()
}
#[inline(always)]
fn call_depth(&self) -> usize {
self.call_depth
}
#[inline(always)]
fn inc_call_depth(&mut self) {
self.call_depth += 1;
}
#[inline(always)]
fn dec_call_depth(&mut self) {
self.call_depth -= 1;
}
#[inline(always)]
fn env(&self) -> GcEnv<'gc> {
self.env
}
#[inline(always)]
fn set_env(&mut self, env: GcEnv<'gc>) {
self.env = env;
}
#[inline(always)]
fn finish_ok(&mut self, val: fix_common::Value) -> Step {
self.finish_ok(val)
}
#[inline(always)]
fn finish_err(&mut self, err: Box<Error>) -> Step {
self.finish_err(err)
}
#[inline(always)]
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
self.finish_type_err(expected, got)
}
#[inline(always)]
fn builtins(&self) -> Value<'gc> {
self.builtins
}
#[inline(always)]
fn functor_sym(&self) -> StringId {
self.functor_sym
}
#[inline(always)]
fn empty_list(&self) -> Value<'gc> {
self.empty_list
}
#[inline(always)]
fn empty_attrs(&self) -> Value<'gc> {
self.empty_attrs
}
#[inline(always)]
fn force_mode(&self) -> ForceMode {
self.force_mode
}
#[inline(always)]
fn import_cache_get(&self, path: &std::path::Path) -> Option<Value<'gc>> {
self.import_cache.get(path).copied()
}
#[inline(always)]
fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>) {
self.import_cache.insert(path, val);
}
#[inline(always)]
fn scope_slot(&self, idx: u32) -> Value<'gc> {
*self
.scope_slots
.get(idx as usize)
.expect("invalid scope slot")
}
#[inline(always)]
fn scope_slots_push(&mut self, val: Value<'gc>) -> u32 {
let idx = self.scope_slots.len() as u32;
self.scope_slots.push(val);
idx
}
#[inline(always)]
fn set_pending_load(&mut self, load: PendingLoad) {
self.pending_load = Some(load);
}
}
enum Action {
@@ -600,11 +485,6 @@ enum Action {
LoadFile(PendingLoad),
}
enum NixNum {
Int(i64),
Float(f64),
}
impl Vm<'_> {
pub fn run<C: VmContext>(
ctx: &mut C,