feat: add experimental tailcall vm backend

This commit is contained in:
2026-04-19 22:13:54 +08:00
parent 800249cb1e
commit 98b07f00e4
16 changed files with 501 additions and 111 deletions
+3
View File
@@ -16,6 +16,9 @@ vim.lsp.config("rust_analyzer", {
settings = { settings = {
["rust-analyzer"] = { ["rust-analyzer"] = {
cargo = { cargo = {
features = {
"tailcall"
}
} }
} }
} }
+5 -4
View File
@@ -3,7 +3,7 @@ use std::fmt::Write;
use colored::Colorize; use colored::Colorize;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use crate::{AttrKeyType, InstructionPtr, OperandType, Op}; use crate::{AttrKeyType, InstructionPtr, Op, OperandType};
pub trait DisassemblerContext { pub trait DisassemblerContext {
fn resolve_string(&self, id: u32) -> &str; fn resolve_string(&self, id: u32) -> &str;
@@ -287,12 +287,13 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
let mut args = format!("size={}", count); let mut args = format!("size={}", count);
for _ in 0..count { for _ in 0..count {
let key_tag = self.read_u8(); let key_tag = self.read_u8();
let key_ty = AttrKeyType::try_from_primitive(key_tag) let key_ty =
.expect("invalid attr key type"); AttrKeyType::try_from_primitive(key_tag).expect("invalid attr key type");
match key_ty { match key_ty {
AttrKeyType::Static => { AttrKeyType::Static => {
let key_id = self.read_u32(); let key_id = self.read_u32();
let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id); let _ =
write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
} }
AttrKeyType::Dynamic => { AttrKeyType::Dynamic => {
let _ = write!(args, " [dyn"); let _ = write!(args, " [dyn");
+3
View File
@@ -3,6 +3,9 @@ name = "fix-vm"
version = "0.1.0" version = "0.1.0"
edition = "2024" edition = "2024"
[features]
tailcall = []
[dependencies] [dependencies]
gc-arena = { workspace = true } gc-arena = { workspace = true }
hashbrown = { workspace = true } hashbrown = { workspace = true }
+17 -4
View File
@@ -12,6 +12,7 @@ pub(crate) struct BytecodeReader<'a> {
} }
impl<'a> BytecodeReader<'a> { impl<'a> BytecodeReader<'a> {
#[cfg_attr(feature = "tailcall", allow(dead_code))]
pub(crate) fn new(bytecode: &'a [u8], pc: usize) -> Self { pub(crate) fn new(bytecode: &'a [u8], pc: usize) -> Self {
Self { Self {
bytecode, bytecode,
@@ -20,6 +21,16 @@ impl<'a> BytecodeReader<'a> {
} }
} }
#[inline(always)]
#[cfg_attr(not(feature = "tailcall"), allow(dead_code))]
pub(crate) fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
Self {
bytecode,
pc: inst_start_pc + 1,
inst_start_pc,
}
}
#[inline(always)] #[inline(always)]
#[cfg_attr(debug_assertions, track_caller)] #[cfg_attr(debug_assertions, track_caller)]
fn read_array<const N: usize>(&mut self) -> [u8; N] { fn read_array<const N: usize>(&mut self) -> [u8; N] {
@@ -31,6 +42,7 @@ impl<'a> BytecodeReader<'a> {
} }
#[inline(always)] #[inline(always)]
#[cfg_attr(feature = "tailcall", allow(dead_code))]
pub(crate) fn read_op(&mut self) -> fix_codegen::Op { pub(crate) fn read_op(&mut self) -> fix_codegen::Op {
use fix_codegen::Op; use fix_codegen::Op;
self.inst_start_pc = self.pc; self.inst_start_pc = self.pc;
@@ -105,16 +117,17 @@ impl<'a> BytecodeReader<'a> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn read_attr_key_data<C: crate::VmContext>(&mut self, ctx: &C) -> crate::AttrKeyData { pub(crate) fn read_attr_key_data<C: crate::VmContext>(
&mut self,
ctx: &C,
) -> crate::AttrKeyData {
use fix_codegen::AttrKeyType; use fix_codegen::AttrKeyType;
let tag = self.read_u8(); let tag = self.read_u8();
let ty = AttrKeyType::try_from_primitive(tag) let ty = AttrKeyType::try_from_primitive(tag)
.unwrap_or_else(|err| panic!("unknown key tag: {:#04x}", err.number)); .unwrap_or_else(|err| panic!("unknown key tag: {:#04x}", err.number));
match ty { match ty {
AttrKeyType::Static => crate::AttrKeyData::Static(self.read_string_id()), AttrKeyType::Static => crate::AttrKeyData::Static(self.read_string_id()),
AttrKeyType::Dynamic => { AttrKeyType::Dynamic => crate::AttrKeyData::Dynamic(self.read_operand_data(ctx)),
crate::AttrKeyData::Dynamic(self.read_operand_data(ctx))
}
} }
} }
+311
View File
@@ -0,0 +1,311 @@
#![cfg(feature = "tailcall")]
use gc_arena::Mutation;
use crate::{BytecodeReader, ForceInfo, StepResult, Vm, VmContext};
pub(crate) enum TailResult<'gc> {
YieldFuel(u32),
ForceThunk(ForceInfo<'gc>),
Done,
}
pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
&mut Vm<'gc>,
&Mutation<'gc>,
&mut C,
&[u8],
&DispatchTable<'gc, C>,
u32,
u32,
) -> TailResult<'gc>;
pub(crate) struct DispatchTable<'gc, C: VmContext>(pub(crate) [OpFn<'gc, C>; 256]);
extern "rust-preserve-none" fn op_illegal<'gc, C: VmContext>(
_vm: &mut Vm<'gc>,
_mc: &Mutation<'gc>,
_ctx: &mut C,
_bc: &[u8],
_table: &DispatchTable<'gc, C>,
pc: u32,
_fuel: u32,
) -> TailResult<'gc> {
panic!("illegal opcode at pc = {pc}");
}
macro_rules! tail_dispatch_after {
($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{
match $result {
StepResult::Continue => {}
StepResult::ForceThunk(info) => return TailResult::ForceThunk(info),
StepResult::Done => return TailResult::Done,
}
let new_pc: u32 = $new_pc;
if $fuel == 0 {
return TailResult::YieldFuel(new_pc);
}
let next_op = $bc[new_pc as usize] as usize;
become $table.0[next_op]($vm, $mc, $ctx, $bc, $table, new_pc, $fuel - 1)
}};
}
macro_rules! tail_fn {
($name:ident, ()) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult<'gc> {
let result = vm.$name();
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (reader)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult<'gc> {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult<'gc> {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(&mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (ctx, reader, mc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult<'gc> {
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
let result = vm.$name(ctx, &mut reader, mc);
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (mc, inst_start_pc)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult<'gc> {
let result = vm.$name(mc, pc as usize);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
($name:ident, (ctx)) => {
extern "rust-preserve-none" fn $name<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
table: &DispatchTable<'gc, C>,
pc: u32,
fuel: u32,
) -> TailResult<'gc> {
let result = vm.$name(ctx);
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
}
};
}
tail_fn!(op_push_smi, (reader));
tail_fn!(op_push_bigint, (reader, mc));
tail_fn!(op_push_float, (reader));
tail_fn!(op_push_string, (reader));
tail_fn!(op_push_null, ());
tail_fn!(op_push_true, ());
tail_fn!(op_push_false, ());
tail_fn!(op_load_local, (reader));
tail_fn!(op_load_outer, (reader));
tail_fn!(op_store_local, (reader, mc));
tail_fn!(op_alloc_locals, (reader, mc));
tail_fn!(op_make_thunk, (reader, mc));
tail_fn!(op_make_closure, (reader, mc));
tail_fn!(op_make_pattern_closure, (reader, mc));
tail_fn!(op_call, (ctx, reader, mc));
tail_fn!(op_return, (ctx, reader, mc));
tail_fn!(op_make_attrs, (ctx, reader, mc));
tail_fn!(op_make_empty_attrs, ());
tail_fn!(op_select_static, (ctx, reader, mc));
tail_fn!(op_select_dynamic, (ctx, reader, mc));
tail_fn!(op_jump_if_select_succeeded, (reader));
tail_fn!(op_has_attr, (reader));
tail_fn!(op_make_list, (ctx, reader, mc));
tail_fn!(op_make_empty_list, ());
tail_fn!(op_add, (ctx, reader, mc));
tail_fn!(op_sub, (reader, mc));
tail_fn!(op_mul, (reader, mc));
tail_fn!(op_div, (reader, mc));
tail_fn!(op_eq, (ctx, reader, mc));
tail_fn!(op_neq, (ctx, reader, mc));
tail_fn!(op_lt, (ctx, reader, mc));
tail_fn!(op_gt, (ctx, reader, mc));
tail_fn!(op_leq, (ctx, reader, mc));
tail_fn!(op_geq, (ctx, reader, mc));
tail_fn!(op_concat, (reader, mc));
tail_fn!(op_update, (mc, inst_start_pc));
tail_fn!(op_neg, ());
tail_fn!(op_not, ());
tail_fn!(op_jump_if_false, (reader, mc));
tail_fn!(op_jump_if_true, (reader, mc));
tail_fn!(op_jump, (reader));
tail_fn!(op_concat_strings, (ctx, reader, mc));
tail_fn!(op_resolve_path, (ctx));
tail_fn!(op_assert, (reader));
tail_fn!(op_push_with, (ctx, reader, mc));
tail_fn!(op_pop_with, ());
tail_fn!(op_lookup_with, (ctx, reader, mc));
tail_fn!(op_prepare_with, ());
tail_fn!(op_load_builtins, ());
tail_fn!(op_load_builtin, (reader));
tail_fn!(op_mk_pos, (reader));
tail_fn!(op_load_repl_binding, (reader));
tail_fn!(op_load_scoped_binding, (reader));
macro_rules! table {
($($variant:ident => $fn:ident),* $(,)?) => {
impl<'gc, C: VmContext> DispatchTable<'gc, C> {
pub(crate) const NEW: Self = {
let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256];
$( arr[fix_codegen::Op::$variant as usize] = $fn; )*
DispatchTable(arr)
};
}
// Exhaustiveness check: fails to compile if `fix_codegen::Op` gains,
// loses, or renames a variant that isn't wired up above.
#[allow(dead_code)]
const _: fn(fix_codegen::Op) = |op| match op {
$( fix_codegen::Op::$variant => (), )*
};
};
}
table! {
PushSmi => op_push_smi,
PushBigInt => op_push_bigint,
PushFloat => op_push_float,
PushString => op_push_string,
PushNull => op_push_null,
PushTrue => op_push_true,
PushFalse => op_push_false,
LoadLocal => op_load_local,
LoadOuter => op_load_outer,
StoreLocal => op_store_local,
AllocLocals => op_alloc_locals,
MakeThunk => op_make_thunk,
MakeClosure => op_make_closure,
MakePatternClosure => op_make_pattern_closure,
Call => op_call,
Return => op_return,
MakeAttrs => op_make_attrs,
MakeEmptyAttrs => op_make_empty_attrs,
SelectStatic => op_select_static,
SelectDynamic => op_select_dynamic,
JumpIfSelectSucceeded => op_jump_if_select_succeeded,
HasAttr => op_has_attr,
MakeList => op_make_list,
MakeEmptyList => op_make_empty_list,
OpAdd => op_add,
OpSub => op_sub,
OpMul => op_mul,
OpDiv => op_div,
OpEq => op_eq,
OpNeq => op_neq,
OpLt => op_lt,
OpGt => op_gt,
OpLeq => op_leq,
OpGeq => op_geq,
OpConcat => op_concat,
OpUpdate => op_update,
OpNeg => op_neg,
OpNot => op_not,
JumpIfFalse => op_jump_if_false,
JumpIfTrue => op_jump_if_true,
Jump => op_jump,
ConcatStrings => op_concat_strings,
ResolvePath => op_resolve_path,
Assert => op_assert,
PushWith => op_push_with,
PopWith => op_pop_with,
LookupWith => op_lookup_with,
PrepareWith => op_prepare_with,
LoadBuiltins => op_load_builtins,
LoadBuiltin => op_load_builtin,
MkPos => op_mk_pos,
LoadReplBinding => op_load_repl_binding,
LoadScopedBinding => op_load_scoped_binding,
Illegal => op_illegal,
}
pub(crate) fn run_tailcall<'gc, C: VmContext>(
vm: &mut Vm<'gc>,
mc: &Mutation<'gc>,
ctx: &mut C,
bc: &[u8],
pc: u32,
) -> TailResult<'gc> {
const FUEL: u32 = 1024;
let table = &DispatchTable::<'gc, C>::NEW;
let op = bc[pc as usize] as usize;
table.0[op](vm, mc, ctx, bc, table, pc, FUEL)
}
+19 -13
View File
@@ -2,8 +2,7 @@ use std::cmp::Ordering;
use gc_arena::{Gc, Mutation}; use gc_arena::{Gc, Mutation};
use crate::{BytecodeReader, NixNum, StepResult, StrictValue, VmError, Value}; use crate::{BytecodeReader, NixNum, StepResult, StrictValue, Value, VmContextExt, VmError};
use crate::VmContextExt;
impl<'gc> crate::Vm<'gc> { impl<'gc> crate::Vm<'gc> {
#[inline(always)] #[inline(always)]
@@ -21,7 +20,10 @@ impl<'gc> crate::Vm<'gc> {
} }
let rhs = self.pop_stack_forced(); let rhs = self.pop_stack_forced();
let lhs = self.pop_stack_forced(); let lhs = self.pop_stack_forced();
if let (Some(ls), Some(rs)) = (VmContextExt::get_string(ctx, lhs), VmContextExt::get_string(ctx, rhs)) { if let (Some(ls), Some(rs)) = (
VmContextExt::get_string(ctx, lhs),
VmContextExt::get_string(ctx, rhs),
) {
let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}"))); let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}")));
self.push_stack(Value::new_gc(ns)); self.push_stack(Value::new_gc(ns));
return StepResult::Continue; return StepResult::Continue;
@@ -300,7 +302,10 @@ impl<'gc> crate::Vm<'gc> {
if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() { if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() {
return Ok(true); return Ok(true);
} }
if let (Some(a), Some(b)) = (VmContextExt::get_string(ctx, lhs), VmContextExt::get_string(ctx, rhs)) { if let (Some(a), Some(b)) = (
VmContextExt::get_string(ctx, lhs),
VmContextExt::get_string(ctx, rhs),
) {
return Ok(a == b); return Ok(a == b);
} }
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::List>(), rhs.as_gc::<crate::List>()) { if let (Some(a), Some(b)) = (lhs.as_gc::<crate::List>(), rhs.as_gc::<crate::List>()) {
@@ -358,20 +363,21 @@ impl<'gc> crate::Vm<'gc> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) { if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
let ord = match (a, b) { let ord = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b), (NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
(NixNum::Float(a), NixNum::Float(b)) => { (NixNum::Float(a), NixNum::Float(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Less),
a.partial_cmp(&b).unwrap_or(Ordering::Less) (NixNum::Int(a), NixNum::Float(b)) => {
(a as f64).partial_cmp(&b).unwrap_or(Ordering::Less)
}
(NixNum::Float(a), NixNum::Int(b)) => {
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
} }
(NixNum::Int(a), NixNum::Float(b)) => (a as f64)
.partial_cmp(&b)
.unwrap_or(Ordering::Less),
(NixNum::Float(a), NixNum::Int(b)) => a
.partial_cmp(&(b as f64))
.unwrap_or(Ordering::Less),
}; };
self.push_stack(Value::new_inline(pred(ord))); self.push_stack(Value::new_inline(pred(ord)));
return Ok(()); return Ok(());
} }
if let (Some(a), Some(b)) = (VmContextExt::get_string(ctx, lhs), VmContextExt::get_string(ctx, rhs)) { if let (Some(a), Some(b)) = (
VmContextExt::get_string(ctx, lhs),
VmContextExt::get_string(ctx, rhs),
) {
self.push_stack(Value::new_inline(pred(a.cmp(b)))); self.push_stack(Value::new_inline(pred(a.cmp(b))));
return Ok(()); return Ok(());
} }
+10 -3
View File
@@ -1,7 +1,8 @@
use crate::{BytecodeReader, PrimOp, StepResult, Value};
use fix_builtins::BuiltinId; use fix_builtins::BuiltinId;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use crate::{BytecodeReader, PrimOp, StepResult, Value};
impl<'gc> crate::Vm<'gc> { impl<'gc> crate::Vm<'gc> {
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_builtins(&mut self) -> StepResult<'gc> { pub(crate) fn op_load_builtins(&mut self) -> StepResult<'gc> {
@@ -27,13 +28,19 @@ impl<'gc> crate::Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_repl_binding(&mut self, reader: &mut BytecodeReader<'_>) -> StepResult<'gc> { pub(crate) fn op_load_repl_binding(
&mut self,
reader: &mut BytecodeReader<'_>,
) -> StepResult<'gc> {
let _name = reader.read_string_id(); let _name = reader.read_string_id();
todo!("LoadReplBinding"); todo!("LoadReplBinding");
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_load_scoped_binding(&mut self, reader: &mut BytecodeReader<'_>) -> StepResult<'gc> { pub(crate) fn op_load_scoped_binding(
&mut self,
reader: &mut BytecodeReader<'_>,
) -> StepResult<'gc> {
let _name = reader.read_string_id(); let _name = reader.read_string_id();
todo!("LoadScopedBinding"); todo!("LoadScopedBinding");
} }
+7 -20
View File
@@ -1,11 +1,8 @@
use fix_error::Error; use fix_error::Error;
use crate::{
BytecodeReader, CallFrame, Closure, Env, StepResult, ThunkState,
};
use crate::VmContextExt;
use gc_arena::{Gc, Mutation, RefLock}; use gc_arena::{Gc, Mutation, RefLock};
use crate::{BytecodeReader, CallFrame, Closure, Env, StepResult, ThunkState, VmContextExt};
impl<'gc> crate::Vm<'gc> { impl<'gc> crate::Vm<'gc> {
#[inline(always)] #[inline(always)]
pub(crate) fn op_call( pub(crate) fn op_call(
@@ -18,9 +15,7 @@ impl<'gc> crate::Vm<'gc> {
return step; return step;
} }
if self.call_depth > 10000 { if self.call_depth > 10000 {
return self.finish_err(Error::eval_error( return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
"stack overflow; max-call-depth exceeded",
));
} }
self.call_depth += 1; self.call_depth += 1;
let func = self.pop_stack(); let func = self.pop_stack();
@@ -83,11 +78,7 @@ impl<'gc> crate::Vm<'gc> {
match val.restrict() { match val.restrict() {
Ok(val) => { Ok(val) => {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val); *outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if reader if reader.bytecode().get(ret_pc).copied() == Some(fix_codegen::Op::Return as u8)
.bytecode()
.get(ret_pc)
.copied()
== Some(fix_codegen::Op::Return as u8)
{ {
self.push_stack(val.relax()); self.push_stack(val.relax());
} }
@@ -122,10 +113,7 @@ impl<'gc> crate::Vm<'gc> {
} }
ThunkState::Evaluated(val) => { ThunkState::Evaluated(val) => {
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val); *outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
if reader if reader.bytecode().get(ret_pc).copied()
.bytecode()
.get(ret_pc)
.copied()
== Some(fix_codegen::Op::Return as u8) == Some(fix_codegen::Op::Return as u8)
{ {
self.push_stack(val.relax()); self.push_stack(val.relax());
@@ -133,9 +121,8 @@ impl<'gc> crate::Vm<'gc> {
} }
ThunkState::Apply { func: _, arg: _ } => todo!("force Apply thunk"), ThunkState::Apply { func: _, arg: _ } => todo!("force Apply thunk"),
ThunkState::Blackhole => { ThunkState::Blackhole => {
return self.finish_err(Error::eval_error( return self
"infinite recursion encountered", .finish_err(Error::eval_error("infinite recursion encountered"));
));
} }
} }
} }
+15 -3
View File
@@ -4,7 +4,11 @@ use crate::{BytecodeReader, StepResult, ThunkState, Value};
impl<'gc> crate::Vm<'gc> { impl<'gc> crate::Vm<'gc> {
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_thunk(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> StepResult<'gc> { pub(crate) fn op_make_thunk(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> StepResult<'gc> {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let thunk = Gc::new( let thunk = Gc::new(
mc, mc,
@@ -19,7 +23,11 @@ impl<'gc> crate::Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_closure(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> StepResult<'gc> { pub(crate) fn op_make_closure(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> StepResult<'gc> {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let n_locals = reader.read_u32(); let n_locals = reader.read_u32();
let closure = Gc::new( let closure = Gc::new(
@@ -36,7 +44,11 @@ impl<'gc> crate::Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_make_pattern_closure(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> StepResult<'gc> { pub(crate) fn op_make_pattern_closure(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> StepResult<'gc> {
let entry_point = reader.read_u32(); let entry_point = reader.read_u32();
let n_locals = reader.read_u32(); let n_locals = reader.read_u32();
let req_count = reader.read_u16() as usize; let req_count = reader.read_u16() as usize;
+21 -25
View File
@@ -72,25 +72,22 @@ impl<'gc> crate::Vm<'gc> {
Some(v) => { Some(v) => {
self.replace_stack(0, v); self.replace_stack(0, v);
} }
None => { None => loop {
loop { let byte = reader.bytecode()[reader.pc()];
let byte = reader.bytecode()[reader.pc()]; if byte == fix_codegen::Op::SelectStatic as u8 {
if byte == fix_codegen::Op::SelectStatic as u8 { reader.set_pc(reader.pc() + 1 + 4 + 4);
reader.set_pc(reader.pc() + 1 + 4 + 4); } else if byte == fix_codegen::Op::SelectDynamic as u8 {
} else if byte == fix_codegen::Op::SelectDynamic as u8 { reader.set_pc(reader.pc() + 1 + 4);
reader.set_pc(reader.pc() + 1 + 4); } else if byte == fix_codegen::Op::JumpIfSelectSucceeded as u8 {
} else if byte == fix_codegen::Op::JumpIfSelectSucceeded as u8 { reader.set_pc(reader.pc() + 1 + 4);
reader.set_pc(reader.pc() + 1 + 4); let _ = self.pop_stack();
let _ = self.pop_stack(); break;
break; } else {
} else { let name = ctx.resolve_string(key);
let name = ctx.resolve_string(key); return self
return self.finish_err(Error::eval_error(format!( .finish_err(Error::eval_error(format!("attribute '{name}' missing")));
"attribute '{name}' missing"
)));
}
} }
} },
} }
StepResult::Continue StepResult::Continue
} }
@@ -119,9 +116,7 @@ impl<'gc> crate::Vm<'gc> {
} else if let Some(ns) = key_val.as_gc::<NixString>() { } else if let Some(ns) = key_val.as_gc::<NixString>() {
ctx.intern_string(ns.as_str()) ctx.intern_string(ns.as_str())
} else { } else {
return self.finish_err(Error::eval_error( return self.finish_err(Error::eval_error("dynamic select key must be a string"));
"dynamic select key must be a string",
));
}; };
let attrset_val = self.stack[self.stack.len() - 2].restrict().expect("forced"); let attrset_val = self.stack[self.stack.len() - 2].restrict().expect("forced");
@@ -138,16 +133,17 @@ impl<'gc> crate::Vm<'gc> {
} }
None => { None => {
let name = ctx.resolve_string(key_sid); let name = ctx.resolve_string(key_sid);
return self.finish_err(Error::eval_error(format!( return self.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
"attribute '{name}' missing"
)));
} }
} }
StepResult::Continue StepResult::Continue
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_jump_if_select_succeeded(&mut self, reader: &mut BytecodeReader<'_>) -> StepResult<'gc> { pub(crate) fn op_jump_if_select_succeeded(
&mut self,
reader: &mut BytecodeReader<'_>,
) -> StepResult<'gc> {
let offset = reader.read_i32(); let offset = reader.read_i32();
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
StepResult::Continue StepResult::Continue
+5 -1
View File
@@ -11,7 +11,11 @@ impl<'gc> crate::Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_push_bigint(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> StepResult<'gc> { pub(crate) fn op_push_bigint(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> StepResult<'gc> {
let val = reader.read_i64(); let val = reader.read_i64();
self.push_stack(Value::new_gc(Gc::new(mc, val))); self.push_stack(Value::new_gc(Gc::new(mc, val)));
StepResult::Continue StepResult::Continue
+6 -6
View File
@@ -1,9 +1,9 @@
pub(crate) mod arithmetic;
pub(crate) mod builtins_misc;
pub(crate) mod calls;
pub(crate) mod closures;
pub(crate) mod collections;
pub(crate) mod control;
pub(crate) mod literals; pub(crate) mod literals;
pub(crate) mod variables; pub(crate) mod variables;
pub(crate) mod closures;
pub(crate) mod calls;
pub(crate) mod collections;
pub(crate) mod arithmetic;
pub(crate) mod control;
pub(crate) mod with_scope; pub(crate) mod with_scope;
pub(crate) mod builtins_misc;
+10 -2
View File
@@ -23,7 +23,11 @@ impl<'gc> crate::Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_store_local(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> StepResult<'gc> { pub(crate) fn op_store_local(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> StepResult<'gc> {
let idx = reader.read_u32() as usize; let idx = reader.read_u32() as usize;
let val = self.pop_stack(); let val = self.pop_stack();
self.env.borrow_mut(mc).locals[idx] = val; self.env.borrow_mut(mc).locals[idx] = val;
@@ -31,7 +35,11 @@ impl<'gc> crate::Vm<'gc> {
} }
#[inline(always)] #[inline(always)]
pub(crate) fn op_alloc_locals(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> StepResult<'gc> { pub(crate) fn op_alloc_locals(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> StepResult<'gc> {
let count = reader.read_u32() as usize; let count = reader.read_u32() as usize;
self.env self.env
.borrow_mut(mc) .borrow_mut(mc)
+2 -2
View File
@@ -1,8 +1,8 @@
use fix_error::Error;
use fix_common::Symbol; use fix_common::Symbol;
use fix_error::Error;
use gc_arena::Gc;
use crate::{BytecodeReader, CallFrame, StepResult, WithEnv}; use crate::{BytecodeReader, CallFrame, StepResult, WithEnv};
use gc_arena::Gc;
impl<'gc> crate::Vm<'gc> { impl<'gc> crate::Vm<'gc> {
#[inline(always)] #[inline(always)]
+60 -21
View File
@@ -1,4 +1,9 @@
#![warn(clippy::unwrap_used)] #![warn(clippy::unwrap_used)]
#![cfg_attr(feature = "tailcall", expect(incomplete_features))]
#![cfg_attr(
feature = "tailcall",
feature(explicit_tail_calls, rust_preserve_none_cc)
)]
use std::path::PathBuf; use std::path::PathBuf;
@@ -12,16 +17,17 @@ use hashbrown::HashMap;
use num_enum::TryFromPrimitive; use num_enum::TryFromPrimitive;
use smallvec::SmallVec; use smallvec::SmallVec;
mod bytecode_reader;
mod boxing; mod boxing;
mod bytecode_reader;
#[cfg(feature = "tailcall")]
mod dispatch_tailcall;
mod value; mod value;
use value::*;
pub use value::StaticValue; pub use value::StaticValue;
pub(crate) mod instructions; use value::*;
mod helpers; mod helpers;
use helpers::*; pub(crate) mod instructions;
pub(crate) use bytecode_reader::BytecodeReader; pub(crate) use bytecode_reader::BytecodeReader;
use helpers::*;
type VmResult<T> = std::result::Result<T, VmError>; type VmResult<T> = std::result::Result<T, VmError>;
@@ -339,11 +345,7 @@ impl<'gc> Vm<'gc> {
if let Some(thunk) = val.as_gc::<Thunk>() { if let Some(thunk) = val.as_gc::<Thunk>() {
let mut state = thunk.borrow_mut(mc); let mut state = thunk.borrow_mut(mc);
match *state { match *state {
ThunkState::Pending { ThunkState::Pending { ip, env, with_env } => {
ip,
env,
with_env,
} => {
*state = ThunkState::Blackhole; *state = ThunkState::Blackhole;
drop(state); drop(state);
StepResult::ForceThunk(ForceInfo { StepResult::ForceThunk(ForceInfo {
@@ -381,6 +383,20 @@ impl<'gc> Vm<'gc> {
other => Some(other), other => Some(other),
} }
} }
#[inline(always)]
pub(crate) fn apply_force_thunk(&mut self, info: ForceInfo<'gc>) -> usize {
self.call_stack.push(CallFrame {
thunk: Some(info.thunk),
stack_depth: info.stack_depth,
pc: info.inst_start_pc,
env: self.env,
with_env: self.with_env,
});
self.env = info.env;
self.with_env = info.with_env;
info.ip
}
} }
#[allow(dead_code)] #[allow(dead_code)]
@@ -423,7 +439,7 @@ impl Vm<'_> {
let mut pc = ip.0; let mut pc = ip.0;
let bytecode: Vec<u8> = ctx.bytecode().to_vec(); let bytecode: Vec<u8> = ctx.bytecode().to_vec();
loop { loop {
match arena.mutate_root(|mc, root| root.execute_batch(&bytecode, &mut ctx, pc, mc)) { match arena.mutate_root(|mc, root| root.dispatch_batch(&bytecode, &mut ctx, pc, mc)) {
Action::Continue { pc: new_pc } => { Action::Continue { pc: new_pc } => {
pc = new_pc; pc = new_pc;
if arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY { if arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY {
@@ -442,6 +458,37 @@ impl Vm<'_> {
impl<'gc> Vm<'gc> { impl<'gc> Vm<'gc> {
#[inline(always)] #[inline(always)]
fn dispatch_batch<C: VmContext>(
&mut self,
bytecode: &[u8],
ctx: &mut C,
pc: usize,
mc: &Mutation<'gc>,
) -> Action {
#[cfg(not(feature = "tailcall"))]
{
self.execute_batch(bytecode, ctx, pc, mc)
}
#[cfg(feature = "tailcall")]
{
use crate::dispatch_tailcall::{TailResult, run_tailcall};
match run_tailcall(self, mc, ctx, bytecode, pc as u32) {
TailResult::YieldFuel(new_pc) => Action::Continue {
pc: new_pc as usize,
},
TailResult::ForceThunk(info) => {
let new_pc = self.apply_force_thunk(info);
Action::Continue { pc: new_pc }
}
TailResult::Done => {
Action::Done(self.result.take().expect("TailResult::Done without result"))
}
}
}
}
#[inline(always)]
#[cfg(not(feature = "tailcall"))]
fn execute_batch( fn execute_batch(
&mut self, &mut self,
bytecode: &[u8], bytecode: &[u8],
@@ -537,16 +584,8 @@ impl<'gc> Vm<'gc> {
match result { match result {
StepResult::Continue => {} StepResult::Continue => {}
StepResult::ForceThunk(info) => { StepResult::ForceThunk(info) => {
self.call_stack.push(CallFrame { let new_pc = self.apply_force_thunk(info);
thunk: Some(info.thunk), reader.set_pc(new_pc);
stack_depth: info.stack_depth,
pc: info.inst_start_pc,
env: self.env,
with_env: self.with_env,
});
reader.set_pc(info.ip);
self.env = info.env;
self.with_env = info.with_env;
} }
StepResult::Done => { StepResult::Done => {
return Action::Done( return Action::Done(