diff --git a/.lazy.lua b/.lazy.lua index aa6b3ab..48d4e1a 100644 --- a/.lazy.lua +++ b/.lazy.lua @@ -16,6 +16,9 @@ vim.lsp.config("rust_analyzer", { settings = { ["rust-analyzer"] = { cargo = { + features = { + "tailcall" + } } } } diff --git a/fix-codegen/src/disassembler.rs b/fix-codegen/src/disassembler.rs index 042eefc..e16601a 100644 --- a/fix-codegen/src/disassembler.rs +++ b/fix-codegen/src/disassembler.rs @@ -3,7 +3,7 @@ use std::fmt::Write; use colored::Colorize; use num_enum::TryFromPrimitive; -use crate::{AttrKeyType, InstructionPtr, OperandType, Op}; +use crate::{AttrKeyType, InstructionPtr, Op, OperandType}; pub trait DisassemblerContext { fn resolve_string(&self, id: u32) -> &str; @@ -287,12 +287,13 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { let mut args = format!("size={}", count); for _ in 0..count { let key_tag = self.read_u8(); - let key_ty = AttrKeyType::try_from_primitive(key_tag) - .expect("invalid attr key type"); + let key_ty = + AttrKeyType::try_from_primitive(key_tag).expect("invalid attr key type"); match key_ty { AttrKeyType::Static => { 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 => { let _ = write!(args, " [dyn"); diff --git a/fix-vm/Cargo.toml b/fix-vm/Cargo.toml index 5c69607..edeb8a9 100644 --- a/fix-vm/Cargo.toml +++ b/fix-vm/Cargo.toml @@ -3,6 +3,9 @@ name = "fix-vm" version = "0.1.0" edition = "2024" +[features] +tailcall = [] + [dependencies] gc-arena = { workspace = true } hashbrown = { workspace = true } diff --git a/fix-vm/src/bytecode_reader.rs b/fix-vm/src/bytecode_reader.rs index 4e05fb0..8261e2e 100644 --- a/fix-vm/src/bytecode_reader.rs +++ b/fix-vm/src/bytecode_reader.rs @@ -12,6 +12,7 @@ pub(crate) struct BytecodeReader<'a> { } impl<'a> BytecodeReader<'a> { + #[cfg_attr(feature = "tailcall", allow(dead_code))] pub(crate) fn new(bytecode: &'a [u8], pc: usize) -> Self { Self { 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)] #[cfg_attr(debug_assertions, track_caller)] fn read_array(&mut self) -> [u8; N] { @@ -31,6 +42,7 @@ impl<'a> BytecodeReader<'a> { } #[inline(always)] + #[cfg_attr(feature = "tailcall", allow(dead_code))] pub(crate) fn read_op(&mut self) -> fix_codegen::Op { use fix_codegen::Op; self.inst_start_pc = self.pc; @@ -105,16 +117,17 @@ impl<'a> BytecodeReader<'a> { } #[inline(always)] - pub(crate) fn read_attr_key_data(&mut self, ctx: &C) -> crate::AttrKeyData { + pub(crate) fn read_attr_key_data( + &mut self, + ctx: &C, + ) -> crate::AttrKeyData { use fix_codegen::AttrKeyType; let tag = self.read_u8(); let ty = AttrKeyType::try_from_primitive(tag) .unwrap_or_else(|err| panic!("unknown key tag: {:#04x}", err.number)); match ty { AttrKeyType::Static => crate::AttrKeyData::Static(self.read_string_id()), - AttrKeyType::Dynamic => { - crate::AttrKeyData::Dynamic(self.read_operand_data(ctx)) - } + AttrKeyType::Dynamic => crate::AttrKeyData::Dynamic(self.read_operand_data(ctx)), } } diff --git a/fix-vm/src/dispatch_tailcall.rs b/fix-vm/src/dispatch_tailcall.rs new file mode 100644 index 0000000..0ed564c --- /dev/null +++ b/fix-vm/src/dispatch_tailcall.rs @@ -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) +} diff --git a/fix-vm/src/helpers.rs b/fix-vm/src/helpers.rs index d466c01..ce0705e 100644 --- a/fix-vm/src/helpers.rs +++ b/fix-vm/src/helpers.rs @@ -4,4 +4,4 @@ use crate::VmError; pub(crate) fn vm_err(msg: impl Into) -> VmError { VmError::Uncatchable(Error::eval_error(msg.into())) -} \ No newline at end of file +} diff --git a/fix-vm/src/instructions/arithmetic.rs b/fix-vm/src/instructions/arithmetic.rs index c37eb16..6bff782 100644 --- a/fix-vm/src/instructions/arithmetic.rs +++ b/fix-vm/src/instructions/arithmetic.rs @@ -2,8 +2,7 @@ use std::cmp::Ordering; use gc_arena::{Gc, Mutation}; -use crate::{BytecodeReader, NixNum, StepResult, StrictValue, VmError, Value}; -use crate::VmContextExt; +use crate::{BytecodeReader, NixNum, StepResult, StrictValue, Value, VmContextExt, VmError}; impl<'gc> crate::Vm<'gc> { #[inline(always)] @@ -21,7 +20,10 @@ impl<'gc> crate::Vm<'gc> { } let rhs = 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}"))); self.push_stack(Value::new_gc(ns)); return StepResult::Continue; @@ -300,7 +302,10 @@ impl<'gc> crate::Vm<'gc> { if lhs.is::() && rhs.is::() { 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); } if let (Some(a), Some(b)) = (lhs.as_gc::(), rhs.as_gc::()) { @@ -358,20 +363,21 @@ impl<'gc> crate::Vm<'gc> { if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) { let ord = match (a, b) { (NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b), - (NixNum::Float(a), NixNum::Float(b)) => { - a.partial_cmp(&b).unwrap_or(Ordering::Less) + (NixNum::Float(a), NixNum::Float(b)) => 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))); 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)))); return Ok(()); } diff --git a/fix-vm/src/instructions/builtins_misc.rs b/fix-vm/src/instructions/builtins_misc.rs index bdea43f..ffdea44 100644 --- a/fix-vm/src/instructions/builtins_misc.rs +++ b/fix-vm/src/instructions/builtins_misc.rs @@ -1,7 +1,8 @@ -use crate::{BytecodeReader, PrimOp, StepResult, Value}; use fix_builtins::BuiltinId; use num_enum::TryFromPrimitive; +use crate::{BytecodeReader, PrimOp, StepResult, Value}; + impl<'gc> crate::Vm<'gc> { #[inline(always)] pub(crate) fn op_load_builtins(&mut self) -> StepResult<'gc> { @@ -27,13 +28,19 @@ impl<'gc> crate::Vm<'gc> { } #[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(); todo!("LoadReplBinding"); } #[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(); todo!("LoadScopedBinding"); } @@ -59,4 +66,4 @@ impl<'gc> crate::Vm<'gc> { pub(crate) fn op_resolve_path(&mut self, _ctx: &mut impl crate::VmContext) -> StepResult<'gc> { todo!("implement ResolvePath"); } -} \ No newline at end of file +} diff --git a/fix-vm/src/instructions/calls.rs b/fix-vm/src/instructions/calls.rs index 5b06bdf..f308304 100644 --- a/fix-vm/src/instructions/calls.rs +++ b/fix-vm/src/instructions/calls.rs @@ -1,11 +1,8 @@ use fix_error::Error; - -use crate::{ - BytecodeReader, CallFrame, Closure, Env, StepResult, ThunkState, -}; -use crate::VmContextExt; use gc_arena::{Gc, Mutation, RefLock}; +use crate::{BytecodeReader, CallFrame, Closure, Env, StepResult, ThunkState, VmContextExt}; + impl<'gc> crate::Vm<'gc> { #[inline(always)] pub(crate) fn op_call( @@ -18,9 +15,7 @@ impl<'gc> crate::Vm<'gc> { return step; } if self.call_depth > 10000 { - return self.finish_err(Error::eval_error( - "stack overflow; max-call-depth exceeded", - )); + return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded")); } self.call_depth += 1; let func = self.pop_stack(); @@ -83,11 +78,7 @@ impl<'gc> crate::Vm<'gc> { match val.restrict() { Ok(val) => { *outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val); - if reader - .bytecode() - .get(ret_pc) - .copied() - == Some(fix_codegen::Op::Return as u8) + if reader.bytecode().get(ret_pc).copied() == Some(fix_codegen::Op::Return as u8) { self.push_stack(val.relax()); } @@ -122,10 +113,7 @@ impl<'gc> crate::Vm<'gc> { } ThunkState::Evaluated(val) => { *outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val); - if reader - .bytecode() - .get(ret_pc) - .copied() + if reader.bytecode().get(ret_pc).copied() == Some(fix_codegen::Op::Return as u8) { self.push_stack(val.relax()); @@ -133,9 +121,8 @@ impl<'gc> crate::Vm<'gc> { } ThunkState::Apply { func: _, arg: _ } => todo!("force Apply thunk"), ThunkState::Blackhole => { - return self.finish_err(Error::eval_error( - "infinite recursion encountered", - )); + return self + .finish_err(Error::eval_error("infinite recursion encountered")); } } } @@ -147,4 +134,4 @@ impl<'gc> crate::Vm<'gc> { self.with_env = with_env; StepResult::Continue } -} \ No newline at end of file +} diff --git a/fix-vm/src/instructions/closures.rs b/fix-vm/src/instructions/closures.rs index 14a3e94..b07da19 100644 --- a/fix-vm/src/instructions/closures.rs +++ b/fix-vm/src/instructions/closures.rs @@ -4,7 +4,11 @@ use crate::{BytecodeReader, StepResult, ThunkState, Value}; impl<'gc> crate::Vm<'gc> { #[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 thunk = Gc::new( mc, @@ -19,7 +23,11 @@ impl<'gc> crate::Vm<'gc> { } #[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 n_locals = reader.read_u32(); let closure = Gc::new( @@ -36,7 +44,11 @@ impl<'gc> crate::Vm<'gc> { } #[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 n_locals = reader.read_u32(); let req_count = reader.read_u16() as usize; @@ -80,4 +92,4 @@ impl<'gc> crate::Vm<'gc> { self.push_stack(Value::new_gc(closure)); StepResult::Continue } -} \ No newline at end of file +} diff --git a/fix-vm/src/instructions/collections.rs b/fix-vm/src/instructions/collections.rs index 5750b23..b64bb51 100644 --- a/fix-vm/src/instructions/collections.rs +++ b/fix-vm/src/instructions/collections.rs @@ -72,25 +72,22 @@ impl<'gc> crate::Vm<'gc> { Some(v) => { self.replace_stack(0, v); } - None => { - loop { - let byte = reader.bytecode()[reader.pc()]; - if byte == fix_codegen::Op::SelectStatic as u8 { - reader.set_pc(reader.pc() + 1 + 4 + 4); - } else if byte == fix_codegen::Op::SelectDynamic as u8 { - reader.set_pc(reader.pc() + 1 + 4); - } else if byte == fix_codegen::Op::JumpIfSelectSucceeded as u8 { - reader.set_pc(reader.pc() + 1 + 4); - let _ = self.pop_stack(); - break; - } else { - let name = ctx.resolve_string(key); - return self.finish_err(Error::eval_error(format!( - "attribute '{name}' missing" - ))); - } + None => loop { + let byte = reader.bytecode()[reader.pc()]; + if byte == fix_codegen::Op::SelectStatic as u8 { + reader.set_pc(reader.pc() + 1 + 4 + 4); + } else if byte == fix_codegen::Op::SelectDynamic as u8 { + reader.set_pc(reader.pc() + 1 + 4); + } else if byte == fix_codegen::Op::JumpIfSelectSucceeded as u8 { + reader.set_pc(reader.pc() + 1 + 4); + let _ = self.pop_stack(); + break; + } else { + let name = ctx.resolve_string(key); + return self + .finish_err(Error::eval_error(format!("attribute '{name}' missing"))); } - } + }, } StepResult::Continue } @@ -119,9 +116,7 @@ impl<'gc> crate::Vm<'gc> { } else if let Some(ns) = key_val.as_gc::() { ctx.intern_string(ns.as_str()) } else { - return self.finish_err(Error::eval_error( - "dynamic select key must be a string", - )); + return self.finish_err(Error::eval_error("dynamic select key must be a string")); }; let attrset_val = self.stack[self.stack.len() - 2].restrict().expect("forced"); @@ -138,16 +133,17 @@ impl<'gc> crate::Vm<'gc> { } None => { let name = ctx.resolve_string(key_sid); - return self.finish_err(Error::eval_error(format!( - "attribute '{name}' missing" - ))); + return self.finish_err(Error::eval_error(format!("attribute '{name}' missing"))); } } StepResult::Continue } #[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(); reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize); StepResult::Continue @@ -186,4 +182,4 @@ impl<'gc> crate::Vm<'gc> { pub(crate) struct AttrEntry { pub(crate) key: AttrKeyData, pub(crate) val: OperandData, -} \ No newline at end of file +} diff --git a/fix-vm/src/instructions/literals.rs b/fix-vm/src/instructions/literals.rs index 25a73aa..019f234 100644 --- a/fix-vm/src/instructions/literals.rs +++ b/fix-vm/src/instructions/literals.rs @@ -11,7 +11,11 @@ impl<'gc> crate::Vm<'gc> { } #[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(); self.push_stack(Value::new_gc(Gc::new(mc, val))); StepResult::Continue @@ -48,4 +52,4 @@ impl<'gc> crate::Vm<'gc> { self.push_stack(Value::new_inline(false)); StepResult::Continue } -} \ No newline at end of file +} diff --git a/fix-vm/src/instructions/mod.rs b/fix-vm/src/instructions/mod.rs index d0c8da5..a4878d2 100644 --- a/fix-vm/src/instructions/mod.rs +++ b/fix-vm/src/instructions/mod.rs @@ -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 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 builtins_misc; \ No newline at end of file diff --git a/fix-vm/src/instructions/variables.rs b/fix-vm/src/instructions/variables.rs index 175a5f5..9ebfdb9 100644 --- a/fix-vm/src/instructions/variables.rs +++ b/fix-vm/src/instructions/variables.rs @@ -23,7 +23,11 @@ impl<'gc> crate::Vm<'gc> { } #[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 val = self.pop_stack(); self.env.borrow_mut(mc).locals[idx] = val; @@ -31,7 +35,11 @@ impl<'gc> crate::Vm<'gc> { } #[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; self.env .borrow_mut(mc) diff --git a/fix-vm/src/instructions/with_scope.rs b/fix-vm/src/instructions/with_scope.rs index 59b82df..685b781 100644 --- a/fix-vm/src/instructions/with_scope.rs +++ b/fix-vm/src/instructions/with_scope.rs @@ -1,8 +1,8 @@ -use fix_error::Error; use fix_common::Symbol; +use fix_error::Error; +use gc_arena::Gc; use crate::{BytecodeReader, CallFrame, StepResult, WithEnv}; -use gc_arena::Gc; impl<'gc> crate::Vm<'gc> { #[inline(always)] @@ -83,4 +83,4 @@ impl<'gc> crate::Vm<'gc> { self.with_env = with_env; StepResult::Continue } -} \ No newline at end of file +} diff --git a/fix-vm/src/lib.rs b/fix-vm/src/lib.rs index 69fa6eb..54c0f0a 100644 --- a/fix-vm/src/lib.rs +++ b/fix-vm/src/lib.rs @@ -1,4 +1,9 @@ #![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; @@ -12,16 +17,17 @@ use hashbrown::HashMap; use num_enum::TryFromPrimitive; use smallvec::SmallVec; -mod bytecode_reader; mod boxing; +mod bytecode_reader; +#[cfg(feature = "tailcall")] +mod dispatch_tailcall; mod value; -use value::*; pub use value::StaticValue; -pub(crate) mod instructions; +use value::*; mod helpers; -use helpers::*; - +pub(crate) mod instructions; pub(crate) use bytecode_reader::BytecodeReader; +use helpers::*; type VmResult = std::result::Result; @@ -339,11 +345,7 @@ impl<'gc> Vm<'gc> { if let Some(thunk) = val.as_gc::() { let mut state = thunk.borrow_mut(mc); match *state { - ThunkState::Pending { - ip, - env, - with_env, - } => { + ThunkState::Pending { ip, env, with_env } => { *state = ThunkState::Blackhole; drop(state); StepResult::ForceThunk(ForceInfo { @@ -381,6 +383,20 @@ impl<'gc> Vm<'gc> { 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)] @@ -423,7 +439,7 @@ impl Vm<'_> { let mut pc = ip.0; let bytecode: Vec = ctx.bytecode().to_vec(); 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 } => { pc = new_pc; if arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY { @@ -442,6 +458,37 @@ impl Vm<'_> { impl<'gc> Vm<'gc> { #[inline(always)] + fn dispatch_batch( + &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( &mut self, bytecode: &[u8], @@ -537,16 +584,8 @@ impl<'gc> Vm<'gc> { match result { StepResult::Continue => {} StepResult::ForceThunk(info) => { - 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, - }); - reader.set_pc(info.ip); - self.env = info.env; - self.with_env = info.with_env; + let new_pc = self.apply_force_thunk(info); + reader.set_pc(new_pc); } StepResult::Done => { return Action::Done(