#![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 fix_bytecode::{InstructionPtr, PrimOpPhase}; use fix_error::{Error, Result, Source}; use fix_lang::{BUILTINS, BuiltinId, StringId}; use gc_arena::metrics::Pacing; use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable}; use hashbrown::HashMap; use smallvec::SmallVec; #[cfg(feature = "tailcall")] mod dispatch_tailcall; pub use fix_runtime::*; mod instructions; mod primops; type VmResult = std::result::Result; #[derive(Collect)] #[collect(no_drop)] pub struct Vm<'gc> { stack: Vec>, call_stack: Vec>, call_depth: usize, #[allow(dead_code)] #[collect(require_static)] error_context: Vec, env: GcEnv<'gc>, import_cache: HashMap>, scope_slots: Vec>, builtins: Value<'gc>, empty_list: Value<'gc>, empty_attrs: Value<'gc>, force_mode: ForceMode, #[collect(require_static)] result: Option>, #[collect(require_static)] pending_load: Option, functor_sym: StringId, } fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> { let mut entries = SmallVec::with_capacity(BUILTINS.len()); for (idx, &(name, arity)) in BUILTINS.iter().enumerate() { let id = BuiltinId::try_from(idx as u8).expect("infallible"); let name = name.strip_prefix("__").unwrap_or(name); let name = ctx.intern_string(name); let dispatch_ip = PrimOpPhase::entry_for_builtin(id).ip(); entries.push(( name, Value::new_inline(PrimOp { id, arity, dispatch_ip, }), )); } let consts = [ ( "__currentSystem", Value::new_inline(ctx.intern_string("x86_64-linux")), ), ("__langVersion", Value::new_inline(6i32)), ( "__nixVersion", Value::new_inline(ctx.intern_string("2.24.0")), ), ( "__storeDir", Value::new_inline(ctx.intern_string("/nix/store")), ), ("__nixPath", Value::new_gc(Gc::new(mc, List::default()))), ("null", Value::new_inline(Null)), ("true", Value::new_inline(true)), ("false", Value::new_inline(false)), ]; for (name, val) in consts { let name = name.strip_prefix("__").unwrap_or(name); let name = ctx.intern_string(name); entries.push((name, val)); } let self_ref_thunk = Gc::new(mc, RefLock::new(ThunkState::Blackhole)); let sym = ctx.intern_string("builtins"); entries.push((sym, Value::new_gc(self_ref_thunk))); entries.sort_by_key(|(k, _)| *k); let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries)); let builtins_value = Value::new_gc(builtins_set); *self_ref_thunk.borrow_mut(mc) = ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk")); builtins_value } impl<'gc> Vm<'gc> { fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Self { let builtins = init_builtins(mc, ctx); Vm { stack: Vec::with_capacity(8192), call_stack: Vec::with_capacity(1024), call_depth: 0, error_context: Vec::with_capacity(1024), env: Gc::new(mc, RefLock::new(Env::empty())), import_cache: HashMap::new(), scope_slots: Vec::new(), builtins, empty_list: Value::new_gc(Gc::new(mc, List::default())), empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())), force_mode, result: None, pending_load: None, functor_sym: ctx.intern_string("__functor"), } } } impl<'gc> Machine<'gc> for Vm<'gc> { #[inline(always)] fn push(&mut self, val: Value<'gc>) { self.stack.push(val); } #[inline(always)] fn pop(&mut self) -> Value<'gc> { self.stack.pop().expect("stack underflow") } #[inline(always)] fn peek(&self, depth: usize) -> Value<'gc> { *self .stack .get(self.stack.len() - depth - 1) .expect("stack underflow") } #[inline(always)] fn peek_forced(&self, depth: usize) -> StrictValue<'gc> { self.stack .get(self.stack.len() - depth - 1) .expect("stack underflow") .restrict() .expect("forced") } #[inline(always)] fn pop_forced(&mut self) -> StrictValue<'gc> { self.stack .pop() .expect("stack underflow") .restrict() .expect("forced") } #[inline(always)] fn replace(&mut self, depth: usize, val: Value<'gc>) { let len = self.stack.len(); *self .stack .get_mut(len - depth - 1) .expect("stack underflow") = val; } #[inline(always)] fn drop_n(&mut self, depth: usize) { self.stack.truncate(self.stack.len() - depth); } #[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 { let Some(thunk) = self.peek(depth).as_gc::() else { return Step::Continue(()); }; let mut state = thunk.borrow_mut(mc); match *state { ThunkState::Pending { ip, env } => { *state = ThunkState::Blackhole; self.call_stack.push(CallFrame { thunk: Some(thunk), pc: resume_pc, env: self.env, }); self.env = env; reader.set_pc(ip); Step::Break(Break::Force) } ThunkState::Evaluated(v) => { self.replace(depth, v.relax()); Step::Continue(()) } ThunkState::Apply { func, arg } => { self.call_stack.push(CallFrame { thunk: Some(thunk), pc: resume_pc, env: self.env, }); self.push(func); self.call(reader, mc, arg, resume_pc) } ThunkState::Blackhole => { self.finish_err(Error::eval_error("infinite recursion encountered")) } } } #[inline(always)] fn call( &mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, arg: Value<'gc>, resume_pc: usize, ) -> Step { instructions::call(self, 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> { 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_lang::Value) -> Step { self.result = Some(Ok(val)); Step::Break(Break::Done) } #[inline(always)] fn finish_err(&mut self, err: Box) -> Step { self.result = Some(Err(err)); Step::Break(Break::Done) } #[inline(always)] fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step { self.result = Some(Err(Error::eval_error(format!( "expected {expected}, got {got}" )))); Step::Break(Break::Done) } #[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> { 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 { Continue { pc: usize }, Done(Result), LoadFile(PendingLoad), } /// Compute initial heap size mirroring CppNix's strategy: 25% of physical RAM, /// clamped to [32 MiB, 384 MiB]. Used as `Pacing::min_sleep` so the collector /// defers the first cycle until the heap reaches this size. fn initial_heap_size() -> usize { const MIN_SIZE: usize = 32 * 1024 * 1024; const MAX_SIZE: usize = 384 * 1024 * 1024; let mut sys = sysinfo::System::new(); sys.refresh_memory(); let total = sys.total_memory() as usize; let quarter = total / 4; quarter.clamp(MIN_SIZE, MAX_SIZE) } impl Vm<'_> { pub fn run( ctx: &mut C, ip: InstructionPtr, force_mode: ForceMode, ) -> Result { let (code, runtime) = ctx.split(); let mut arena: Arena]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime)); arena.metrics().set_pacing(Pacing { min_sleep: initial_heap_size(), ..Pacing::STOP_THE_WORLD }); let mut pc = ip.0; loop { let bytecode = code.bytecode(); match arena.mutate_root(|mc, root| root.dispatch_batch(bytecode, runtime, pc, mc)) { Action::Continue { pc: new_pc } => { pc = new_pc; if arena.metrics().allocation_debt() > 0.0 { arena.finish_cycle(); } } Action::LoadFile(load) => { let source = match Source::new_file(load.path) { Ok(src) => src, Err(err) => break Err(Error::eval_error(format!("import failed: {err}"))), }; let extra_scope = load.scope.map(|s| ExtraScope::ScopedImport { keys: s.keys, slot_id: s.slot_id, }); let new_ip = match code.compile_with_scope(source, extra_scope, runtime) { Ok(ip) => ip, Err(err) => break Err(err), }; pc = new_ip.0; arena.mutate_root(|mc, root| { root.env = Gc::new(mc, RefLock::new(Env::empty())); }); } Action::Done(done) => break done, } } } } impl<'gc> Vm<'gc> { const DEFAULT_FUEL_AMOUNT: u32 = 1024; #[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::Done => { Action::Done(self.result.take().expect("TailResult::Done without result")) } TailResult::LoadFile => Action::LoadFile( self.pending_load .take() .expect("TailResult::LoadFile without pending_load"), ), } } } #[inline(always)] #[cfg(not(feature = "tailcall"))] fn execute_batch( &mut self, bytecode: &[u8], ctx: &mut impl VmRuntimeCtx, pc: usize, mc: &Mutation<'gc>, ) -> Action { use fix_bytecode::Op::*; use instructions::*; let mut reader = BytecodeReader::new(bytecode, pc); let mut fuel = Self::DEFAULT_FUEL_AMOUNT; loop { if fuel == 0 { return Action::Continue { pc: reader.pc() }; } fuel -= 1; let op = reader.read_op(); let result = match op { PushSmi => op_push_smi(self, &mut reader), PushBigInt => op_push_bigint(self, &mut reader, mc), PushFloat => op_push_float(self, &mut reader), PushString => op_push_string(self, &mut reader), PushNull => op_push_null(self), PushTrue => op_push_true(self), PushFalse => op_push_false(self), LoadLocal => op_load_local(self, &mut reader), LoadOuter => op_load_outer(self, &mut reader), StoreLocal => op_store_local(self, &mut reader, mc), AllocLocals => op_alloc_locals(self, &mut reader, mc), MakeThunk => op_make_thunk(self, &mut reader, mc), MakeClosure => op_make_closure(self, &mut reader, mc), MakePatternClosure => op_make_pattern_closure(self, &mut reader, mc), Call => op_call(self, ctx, &mut reader, mc), DispatchPrimOp => op_dispatch_primop(self, ctx, &mut reader, mc), Return => op_return(self, ctx, &mut reader, mc), MakeAttrs => op_make_attrs(self, ctx, &mut reader, mc), MakeEmptyAttrs => op_make_empty_attrs(self), SelectStatic => op_select_static(self, ctx, &mut reader, mc), SelectDynamic => op_select_dynamic(self, ctx, &mut reader, mc), HasAttrPathStatic => op_has_attr_path_static(self, ctx, &mut reader, mc), HasAttrPathDynamic => op_has_attr_path_dynamic(self, ctx, &mut reader, mc), HasAttrStatic => op_has_attr_static(self, &mut reader, mc), HasAttrDynamic => op_has_attr_dynamic(self, ctx, &mut reader, mc), HasAttrResolve => op_has_attr_resolve(self), JumpIfSelectFailed => op_jump_if_select_failed(self, &mut reader), JumpIfSelectSucceeded => op_jump_if_select_succeeded(self, &mut reader), MakeList => op_make_list(self, ctx, &mut reader, mc), MakeEmptyList => op_make_empty_list(self), OpAdd => op_add(self, ctx, &mut reader, mc), OpSub => op_sub(self, &mut reader, mc), OpMul => op_mul(self, &mut reader, mc), OpDiv => op_div(self, &mut reader, mc), OpEq => op_eq(self, ctx, &mut reader, mc), OpNeq => op_neq(self, ctx, &mut reader, mc), OpLt => op_lt(self, ctx, &mut reader, mc), OpGt => op_gt(self, ctx, &mut reader, mc), OpLeq => op_leq(self, ctx, &mut reader, mc), OpGeq => op_geq(self, ctx, &mut reader, mc), OpConcat => op_concat(self, &mut reader, mc), OpUpdate => op_update(self, &mut reader, mc), OpNeg => op_neg(self, &mut reader, mc), OpNot => op_not(self, &mut reader, mc), JumpIfFalse => op_jump_if_false(self, &mut reader, mc), JumpIfTrue => op_jump_if_true(self, &mut reader, mc), Jump => op_jump(self, &mut reader), ConcatStrings => op_concat_strings(self, ctx, &mut reader, mc), CoerceToString => op_coerce_to_string(self, &mut reader, mc), ResolvePath => op_resolve_path(self, ctx, &mut reader, mc), Assert => op_assert(self, ctx, &mut reader, mc), LookupWith => op_lookup_with(self, ctx, &mut reader, mc), LoadBuiltins => op_load_builtins(self), LoadBuiltin => op_load_builtin(self, &mut reader), LoadReplBinding => op_load_repl_binding(self, &mut reader), LoadScopedBinding => op_load_scoped_binding(self, ctx, &mut reader, mc), Illegal => unreachable!(), }; match result { Step::Continue(()) | Step::Break(Break::Force) => {} Step::Break(Break::Done) => { return Action::Done(self.result.take().expect("Break::Done without result")); } Step::Break(Break::LoadFile) => { return Action::LoadFile( self.pending_load .take() .expect("Break::LoadFile without pending_load"), ); } } } } }