use std::path::PathBuf; use gc_arena::{Collect, Gc, Mutation, RefLock}; use hashbrown::HashMap; use num_enum::TryFromPrimitive; use smallvec::SmallVec; use string_interner::{DefaultStringInterner, Symbol as _}; use super::builtins::{BUILTINS, BuiltinId, PrimOpArgs, PrimOpStrictArgs, is_lazy_builtin}; use super::primops::{BuiltinResult, BuiltinState, PrimOpCtx, dispatch_lazy_builtin, dispatch_strict_builtin}; use super::stack::Stack; use super::value::*; use crate::error::{Error, Result}; use crate::ir::StringId; pub(super) type VmResult = std::result::Result; pub(super) enum VmError { Catchable(String), Uncatchable(Box), } impl From> for VmError { fn from(e: Box) -> Self { VmError::Uncatchable(e) } } #[derive(Collect)] #[collect(no_drop)] pub(super) struct VM<'gc> { stack: Stack<65536, Value<'gc>>, frames: Stack<8192, CallFrame<'gc>>, with_scope: Option>>, error_contexts: Stack<8192, ErrorFrame>, globals: GlobalState<'gc>, import_cache: HashMap>, pc: usize, current_env: Option>>>, started: bool, } #[derive(Collect)] #[collect(no_drop)] struct GlobalState<'gc> { builtins: Value<'gc>, builtin_lookup: HashMap, empty_list: Value<'gc>, empty_attrs: Value<'gc>, } #[derive(Collect)] #[collect(require_static)] struct ErrorFrame { span_id: u32, message: Option, } #[derive(Collect, Debug)] #[collect(no_drop)] pub(super) struct WithScope<'gc> { env: Value<'gc>, prev: Option>>, } #[derive(Collect, Debug)] #[collect(no_drop)] struct CallFrame<'gc> { pc: usize, env: Gc<'gc, RefLock>>, continuation: Continuation<'gc>, span: Option, } #[derive(Collect, Debug)] #[collect(no_drop)] enum Continuation<'gc> { Return, ForceThunk { thunk: Gc<'gc, Thunk<'gc>>, after: AfterForce<'gc>, }, BuiltinReturn(BuiltinState<'gc>), BuiltinCallAndForce(BuiltinState<'gc>), } #[derive(Collect, Debug)] #[collect(no_drop)] pub(super) enum AfterForce<'gc> { Identity, ForceBool, BinOpLhs { rhs: Value<'gc>, #[collect(require_static)] op: BinOpTag, }, BinOpRhs { lhs_forced: StrictValue<'gc>, #[collect(require_static)] op: BinOpTag, }, UnNeg, UnNot, Call { arg: Value<'gc>, span: Option, }, PatternCallArgForce { func: StrictValue<'gc>, span: Option, }, Select { keys: SmallVec<[Value<'gc>; 4]>, remaining: u16, span: u32, default: Option>, }, HasAttr { keys: SmallVec<[Value<'gc>; 4]>, remaining: u16, }, Assert { expr: Value<'gc>, raw_idx: u32, span_id: u32, }, ConcatStrings { forced: SmallVec<[StrictValue<'gc>; 8]>, remaining: SmallVec<[Value<'gc>; 8]>, force_string: bool, }, PushWith, WithLookup { #[collect(require_static)] name: StringId, next: Option>>, }, TopLevelForce, DiscardAndPush { value: Value<'gc>, }, Builtin(BuiltinState<'gc>), BuiltinArgForce { #[collect(require_static)] id: BuiltinId, #[collect(require_static)] arity: u8, #[collect(require_static)] forced_count: u8, args: PrimOpStrictArgs<'gc>, remaining: PrimOpArgs<'gc>, }, } #[derive(Clone, Copy, Debug)] pub(super) enum BinOpTag { Add, Sub, Mul, Div, Eq, Neq, Lt, Gt, Leq, Geq, Concat, Update, } pub(super) enum ForceResult<'gc> { Ready(StrictValue<'gc>), NeedEval { ip: u32, env: Gc<'gc, RefLock>>, thunk: Gc<'gc, Thunk<'gc>>, }, NeedApply(Gc<'gc, Thunk<'gc>>), } pub(crate) enum Action { Continue, Done(Result), NeedGc, IoRequest(()), } pub(super) enum NixNum { Int(i64), Float(f64), } macro_rules! try_vm { ($self:ident, $expr:expr) => { match $expr { Ok(v) => v, Err(e) => return VM::handle_vm_error($self, e), } }; } impl<'gc> VM<'gc> { pub(super) fn new(mc: &Mutation<'gc>, strings: &mut DefaultStringInterner) -> Self { let (builtins, builtin_lookup) = Self::init_builtins(mc, strings); Self { stack: Stack::new(), frames: Stack::new(), with_scope: None, error_contexts: Stack::new(), globals: GlobalState { builtins, builtin_lookup, empty_list: Value::new_gc(Gc::new(mc, List::default())), empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())), }, import_cache: HashMap::new(), pc: 0, current_env: None, started: false, } } fn init_builtins(mc: &Mutation<'gc>, strings: &mut DefaultStringInterner) -> (Value<'gc>, HashMap) { let mut builtin_lookup = HashMap::new(); let mut entries = SmallVec::new(); for (id, &(name, arity)) in BUILTINS.iter().enumerate() { let Some(sym) = strings.get(name) else { continue; }; let sid = StringId(sym); let primop = PrimOp { id: BuiltinId::try_from_primitive(id as u8).expect("invalid BuiltinId??"), arity, }; builtin_lookup.insert(sid, primop); if arity == 0 { // "null" constant entries.push((sid, Value::new_inline(Null))); } else { entries.push((sid, Value::new_inline(primop))); } } // Add constant entries macro_rules! add_const { ($name:expr, $val:expr) => {{ let sym = strings.get_or_intern($name); entries.push((StringId(sym), $val)); }}; } add_const!( "currentSystem", Value::new_gc(Gc::new(mc, NixString::new("x86_64-linux"))) ); add_const!("langVersion", Value::new_inline(6i32)); add_const!( "nixVersion", Value::new_gc(Gc::new(mc, NixString::new("2.24.0"))) ); add_const!( "storeDir", Value::new_gc(Gc::new(mc, NixString::new("/nix/store"))) ); add_const!( "nixPath", Value::new_gc(Gc::new( mc, List { inner: SmallVec::new() } )) ); add_const!("true", Value::new_inline(true)); add_const!("false", Value::new_inline(false)); // Self-reference thunk for builtins.builtins let self_ref_thunk: Gc<'gc, Thunk<'gc>> = Gc::new(mc, RefLock::new(ThunkState::Blackhole)); let sym = strings.get_or_intern("builtins"); entries.push((StringId(sym), Value::new_gc(self_ref_thunk))); entries.sort_by_key(|(k, _)| *k); let builtins_set = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); let builtins_val = Value::new_gc(builtins_set); // Populate the self-reference *self_ref_thunk.borrow_mut(mc) = ThunkState::Evaluated(builtins_val); (builtins_val, builtin_lookup) } #[inline(always)] fn read_array(&mut self, bc: &[u8]) -> [u8; N] { #[cfg(debug_assertions)] let ret = bc[self.pc..self.pc + N] .try_into() .expect("read_array failed"); #[cfg(not(debug_assertions))] let ret = unsafe { bc[self.pc..self.pc + N].try_into().unwrap_unchecked() }; self.pc += N; ret } #[inline(always)] fn read_u8(&mut self, bc: &[u8]) -> u8 { u8::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_u16(&mut self, bc: &[u8]) -> u16 { u16::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_u32(&mut self, bc: &[u8]) -> u32 { u32::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_i32(&mut self, bc: &[u8]) -> i32 { i32::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_i64(&mut self, bc: &[u8]) -> i64 { i64::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_f64(&mut self, bc: &[u8]) -> f64 { f64::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_string_id(&mut self, bc: &[u8]) -> StringId { let raw = self.read_u32(bc); StringId(unsafe { string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap_unchecked() }) } #[inline(always)] fn env(&self) -> Gc<'gc, RefLock>> { self.current_env.expect("no current env") } pub(super) fn force_inline(&self, val: Value<'gc>) -> VmResult> { let mut current = val; loop { let Some(thunk) = current.as_gc::>() else { return Ok(ForceResult::Ready(unsafe { StrictValue::try_from_forced(current).unwrap_unchecked() })); }; let thunk_ref = thunk.borrow(); match &*thunk_ref { ThunkState::Evaluated(v) => { current = *v; drop(thunk_ref); } &ThunkState::Pending { ip, env } => { return Ok(ForceResult::NeedEval { ip, env, thunk }); } ThunkState::Apply { .. } => { return Ok(ForceResult::NeedApply(thunk)); } ThunkState::Blackhole => { return Err(VmError::Uncatchable(Error::eval_error( "infinite recursion encountered", ))); } } } } pub(super) fn push_force_frame( &mut self, thunk: Gc<'gc, Thunk<'gc>>, after: AfterForce<'gc>, ip: u32, env: Gc<'gc, RefLock>>, mc: &Mutation<'gc>, ) { self.frames .push(CallFrame { pc: self.pc, env: self.env(), continuation: Continuation::ForceThunk { thunk, after }, span: None, }) .expect("frame stack overflow"); *thunk.borrow_mut(mc) = ThunkState::Blackhole; self.pc = ip as usize; self.current_env = Some(env); } fn push_apply_force_frame( &mut self, thunk: Gc<'gc, Thunk<'gc>>, after: AfterForce<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { let (func, arg) = match &*thunk.borrow() { &ThunkState::Apply { func, arg } => (func, arg), _ => unreachable!(), }; *thunk.borrow_mut(mc) = ThunkState::Blackhole; self.frames .push(CallFrame { pc: self.pc, env: self.env(), continuation: Continuation::ForceThunk { thunk, after }, span: None, }) .expect("frame stack overflow"); match self.force_inline(func)? { ForceResult::Ready(f) => { self.do_call(f, arg, None, mc, strings)?; } ForceResult::NeedEval { ip, env, thunk: func_thunk, } => { self.push_force_frame( func_thunk, AfterForce::Call { arg, span: None }, ip, env, mc, ); } ForceResult::NeedApply(func_thunk) => { self.push_apply_force_frame( func_thunk, AfterForce::Call { arg, span: None }, mc, strings, )?; } } Ok(()) } fn setup_force( &mut self, result: ForceResult<'gc>, after: AfterForce<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { match result { ForceResult::Ready(_) => unreachable!(), ForceResult::NeedEval { ip, env, thunk } => { self.push_force_frame(thunk, after, ip, env, mc); Ok(()) } ForceResult::NeedApply(thunk) => self.push_apply_force_frame(thunk, after, mc, strings), } } pub(super) fn make_int(val: i64, mc: &Mutation<'gc>) -> Value<'gc> { if val >= i32::MIN as i64 && val <= i32::MAX as i64 { Value::new_inline(val as i32) } else { Value::new_gc(Gc::new(mc, val)) } } pub(super) fn as_num(val: StrictValue<'gc>) -> Option { if let Some(i) = val.as_inline::() { Some(NixNum::Int(i as i64)) } else if let Some(gc_i) = val.as_gc::() { Some(NixNum::Int(*gc_i)) } else { val.as_float().map(NixNum::Float) } } pub(super) fn get_string_id(val: &Value<'gc>) -> Option { val.as_inline::() } pub(super) fn get_string<'a, 'gc1: 'gc + 'a>( val: StrictValue<'gc1>, strings: &'a DefaultStringInterner, ) -> Option<&'a str> { if let Some(sid) = val.as_inline::() { Some(strings.resolve(sid.0)?) } else { val.as_gc::().map(|ns| ns.as_ref().as_str()) } } pub(super) fn err(msg: impl Into) -> VmError { VmError::Uncatchable(Error::eval_error(msg.into())) } pub(super) fn handle_return( &mut self, ret_val: Value<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> Action { let Some(frame) = self.frames.pop() else { return match self.force_inline(ret_val) { Ok(ForceResult::Ready(v)) => Action::Done(Ok(self.convert_value(&v, strings))), Ok(other) => { try_vm!(self, self.setup_force(other, AfterForce::TopLevelForce, mc, strings)); Action::Continue } Err(e) => self.handle_vm_error(e), }; }; self.pc = frame.pc; self.current_env = Some(frame.env); match frame.continuation { Continuation::Return => { self.push_stack(ret_val); Action::Continue } Continuation::ForceThunk { thunk, after } => { *thunk.borrow_mut(mc) = ThunkState::Evaluated(ret_val); match self.force_inline(ret_val) { Ok(ForceResult::Ready(strict)) => { self.resume_after_force(strict, after, mc, strings) } Ok(other) => { try_vm!(self, self.setup_force(other, after, mc, strings)); Action::Continue } Err(e) => self.handle_vm_error(e), } } Continuation::BuiltinReturn(state) => { let ctx = PrimOpCtx { vm: self, mc, strings, }; match ctx.vm.force_inline(ret_val) { Ok(ForceResult::Ready(strict)) => { let result = state.resume(strict, &ctx); self.process_builtin_result(result, mc, strings) } Ok(other) => { try_vm!( self, self.setup_force(other, AfterForce::Builtin(state), mc, strings) ); Action::Continue } Err(e) => self.handle_vm_error(e), } } Continuation::BuiltinCallAndForce(state) => { match self.force_inline(ret_val) { Ok(ForceResult::Ready(strict)) => { let ctx = PrimOpCtx { vm: self, mc, strings, }; let result = state.resume(strict, &ctx); self.process_builtin_result(result, mc, strings) } Ok(other) => { try_vm!( self, self.setup_force(other, AfterForce::Builtin(state), mc, strings) ); Action::Continue } Err(e) => self.handle_vm_error(e), } } } } pub(super) fn resume_after_force( &mut self, val: StrictValue<'gc>, after: AfterForce<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> Action { match after { AfterForce::Identity => { self.push_stack(val.relax()); Action::Continue } AfterForce::ForceBool => { if val.as_inline::().is_none() { return Action::Done(Err(Error::eval_error("value is not a boolean"))); } self.push_stack(val.relax()); Action::Continue } AfterForce::BinOpLhs { rhs, op } => match self.force_inline(rhs) { Ok(ForceResult::Ready(rhs_forced)) => { match self.compute_binop(op, val, rhs_forced, mc, strings) { Ok(result) => { self.push_stack(result); Action::Continue } Err(e) => self.handle_vm_error(e), } } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::BinOpRhs { lhs_forced: val, op, }, mc, strings, ) ); Action::Continue } Err(e) => self.handle_vm_error(e), }, AfterForce::BinOpRhs { lhs_forced, op } => { match self.compute_binop(op, lhs_forced, val, mc, strings) { Ok(result) => { self.push_stack(result); Action::Continue } Err(e) => self.handle_vm_error(e), } } AfterForce::UnNeg => match Self::as_num(val) { Some(NixNum::Int(i)) => { self.push_stack(Self::make_int(-i, mc)); Action::Continue } Some(NixNum::Float(f)) => { self.push_stack(Value::new_float(-f)); Action::Continue } None => Action::Done(Err(Error::eval_error("cannot negate non-number"))), }, AfterForce::UnNot => match val.as_inline::() { Some(b) => { self.push_stack(Value::new_inline(!b)); Action::Continue } None => Action::Done(Err(Error::eval_error("value is not a boolean"))), }, AfterForce::Call { arg, span } => match self.do_call(val, arg, span, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), }, AfterForce::PatternCallArgForce { func, span } => { if let Some(closure_gc) = func.as_gc::>() { let pattern = closure_gc .pattern .as_ref() .expect("internal: pattern call on non-pattern-closure"); match self.setup_pattern_call( closure_gc.ip, closure_gc.n_locals, closure_gc.env, pattern, val, span, mc, ) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), } } else { Action::Done(Err(Error::eval_error( "internal: pattern call on non-closure", ))) } } AfterForce::Select { keys, remaining, span, default, } => match self.do_select_step(val, keys, remaining, span, default, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), }, AfterForce::HasAttr { keys, remaining } => { match self.do_has_attr_step(val, keys, remaining, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), } } AfterForce::Assert { expr, raw_idx, span_id: _, } => match val.as_inline::() { Some(true) => { self.push_stack(expr); Action::Continue } Some(false) => { let sym = string_interner::symbol::SymbolU32::try_from_usize(raw_idx as usize); let msg = sym.and_then(|s| strings.resolve(s)).unwrap_or(""); Action::Done(Err(Error::eval_error(format!("assertion '{msg}' failed")))) } None => Action::Done(Err(Error::eval_error( "assertion condition must be a boolean", ))), }, AfterForce::ConcatStrings { mut forced, remaining, force_string, } => { forced.push(val); match self.concat_strings_continue(forced, remaining, force_string, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), } } AfterForce::PushWith => { let scope = Gc::new( mc, WithScope { env: val.relax(), prev: self.with_scope, }, ); self.with_scope = Some(scope); Action::Continue } AfterForce::WithLookup { name, next } => { match self.do_with_lookup_step(val, name, next, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), } } AfterForce::TopLevelForce => Action::Done(Ok(self.convert_value(&val, strings))), AfterForce::DiscardAndPush { value } => { self.push_stack(value); Action::Continue } AfterForce::Builtin(state) => { let ctx = PrimOpCtx { vm: self, mc, strings, }; let result = state.resume(val, &ctx); self.process_builtin_result(result, mc, strings) } AfterForce::BuiltinArgForce { id, arity, mut forced_count, mut args, remaining, } => { args[forced_count as usize] = val; forced_count += 1; while forced_count < arity { let next = remaining[forced_count as usize]; match self.force_inline(next) { Ok(ForceResult::Ready(v)) => { args[forced_count as usize] = v; forced_count += 1; } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::BuiltinArgForce { id, arity, forced_count, args, remaining, }, mc, strings, ) ); return Action::Continue; } Err(e) => return self.handle_vm_error(e), } } let ctx = PrimOpCtx { vm: self, mc, strings, }; let result = dispatch_strict_builtin(id, args, arity, &ctx); self.process_builtin_result(result, mc, strings) } } } fn process_builtin_result( &mut self, result: BuiltinResult<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> Action { match result { BuiltinResult::Done(v) => { self.push_stack(v); Action::Continue } BuiltinResult::Force(state, val) => { match self.force_inline(val) { Ok(ForceResult::Ready(strict)) => { let ctx = PrimOpCtx { vm: self, mc, strings, }; let result = state.resume(strict, &ctx); self.process_builtin_result(result, mc, strings) } Ok(other) => { try_vm!( self, self.setup_force(other, AfterForce::Builtin(state), mc, strings) ); Action::Continue } Err(e) => self.handle_vm_error(e), } } BuiltinResult::Call(state, func, arg) => { self.frames .push(CallFrame { pc: self.pc, env: self.env(), continuation: Continuation::BuiltinReturn(state), span: None, }) .expect("frame stack overflow"); match self.do_call(func, arg, None, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), } } BuiltinResult::CallAndForce(state, func, arg) => { self.frames .push(CallFrame { pc: self.pc, env: self.env(), continuation: Continuation::BuiltinCallAndForce(state), span: None, }) .expect("frame stack overflow"); match self.do_call(func, arg, None, mc, strings) { Ok(()) => Action::Continue, Err(e) => self.handle_vm_error(e), } } BuiltinResult::Error(e) => self.handle_vm_error(e), } } fn dispatch_primop( &mut self, id: BuiltinId, arity: u8, args: PrimOpArgs<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { let ctx = PrimOpCtx { vm: self, mc, strings, }; let result = if is_lazy_builtin(id) { dispatch_lazy_builtin(id, &args, arity, &ctx) } else { let mut strict_args: PrimOpStrictArgs<'gc> = [StrictValue::default(); 3]; for i in 0..arity as usize { match self.force_inline(args[i])? { ForceResult::Ready(v) => { strict_args[i] = v; } other => { self.setup_force( other, AfterForce::BuiltinArgForce { id, arity, forced_count: i as u8, args: strict_args, remaining: args, }, mc, strings, )?; return Ok(()); } } } let ctx = PrimOpCtx { vm: self, mc, strings, }; dispatch_strict_builtin(id, strict_args, arity, &ctx) }; match result { BuiltinResult::Done(v) => { self.push_stack(v); Ok(()) } other => { let action = self.process_builtin_result(other, mc, strings); match action { Action::Continue => Ok(()), Action::Done(Err(e)) => Err(VmError::Uncatchable(e)), _ => Ok(()), } } } } pub(super) fn do_call( &mut self, func: StrictValue<'gc>, arg: Value<'gc>, span: Option, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { if let Some(closure_gc) = func.as_gc::>() { let ip = closure_gc.ip; let n_locals = closure_gc.n_locals; let closure_env = closure_gc.env; if let Some(ref pattern) = closure_gc.pattern { match self.force_inline(arg)? { ForceResult::Ready(forced_arg) => { self.setup_pattern_call( ip, n_locals, closure_env, pattern, forced_arg, span, mc, )?; } other => { self.setup_force( other, AfterForce::PatternCallArgForce { func, span }, mc, strings, )?; } } } else { let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, closure_env))); self.frames .push(CallFrame { pc: self.pc, env: self.env(), continuation: Continuation::Return, span, }) .expect("frame stack overflow"); self.pc = ip as usize; self.current_env = Some(new_env); } Ok(()) } else if let Some(po) = func.as_inline::() { if po.arity <= 1 { let mut primop_args: PrimOpArgs<'gc> = [Value::default(); 3]; if po.arity == 1 { primop_args[0] = arg; } self.dispatch_primop(po.id, po.arity, primop_args, mc, strings) } else { let app = Gc::new( mc, PrimOpApp { primop: po, args: SmallVec::from_elem(arg, 1), }, ); self.push_stack(Value::new_gc(app)); Ok(()) } } else if let Some(poa) = func.as_gc::>() { let mut args = poa.args.clone(); args.push(arg); if args.len() >= poa.primop.arity as usize { let mut primop_args: PrimOpArgs<'gc> = [Value::default(); 3]; for (i, a) in args.iter().enumerate() { if i < 3 { primop_args[i] = *a; } } self.dispatch_primop(poa.primop.id, poa.primop.arity, primop_args, mc, strings) } else { let app = Gc::new( mc, PrimOpApp { primop: poa.primop, args, }, ); self.push_stack(Value::new_gc(app)); Ok(()) } } else { Err(Self::err( "attempt to call something which is not a function", )) } } pub(super) fn setup_pattern_call( &mut self, ip: u32, n_locals: u32, closure_env: Gc<'gc, RefLock>>, pattern: &PatternInfo, arg: StrictValue<'gc>, span: Option, mc: &Mutation<'gc>, ) -> VmResult<()> { let Some(attrs) = arg.as_gc::>() else { return Err(Self::err( "function that expected a set received a non-set argument", )); }; for &req in &pattern.required { if !attrs.has(req) { return Err(Self::err("function argument missing required attribute")); } } if !pattern.ellipsis { for (key, _) in attrs.iter() { let is_known = pattern.required.contains(key) || pattern.optional.contains(key); if !is_known { return Err(Self::err("function received unexpected attribute")); } } } let new_env = Gc::new( mc, RefLock::new(Env::with_arg(arg.relax(), n_locals, closure_env)), ); self.frames .push(CallFrame { pc: self.pc, env: self.env(), continuation: Continuation::Return, span, }) .expect("frame stack overflow"); self.pc = ip as usize; self.current_env = Some(new_env); Ok(()) } pub(super) fn compute_binop( &self, op: BinOpTag, lhs: StrictValue<'gc>, rhs: StrictValue<'gc>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult> { match op { BinOpTag::Add => { if let (Some(ls), Some(rs)) = ( Self::get_string(lhs, strings), Self::get_string(rhs, strings), ) { let ns = Gc::new(mc, NixString::new(format!("{ls}{rs}"))); return Ok(Value::new_gc(ns)); } self.numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b) } BinOpTag::Sub => self.numeric_binop(lhs, rhs, mc, i64::wrapping_sub, |a, b| a - b), BinOpTag::Mul => self.numeric_binop(lhs, rhs, mc, i64::wrapping_mul, |a, b| a * b), BinOpTag::Div => match (Self::as_num(lhs), Self::as_num(rhs)) { (_, Some(NixNum::Int(0))) => Err(Self::err("division by zero")), (_, Some(NixNum::Float(0.))) => Err(Self::err("division by zero")), (Some(NixNum::Int(a)), Some(NixNum::Int(b))) => { Ok(Self::make_int(a.wrapping_div(b), mc)) } (Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new_float(a / b)), (Some(NixNum::Int(a)), Some(NixNum::Float(b))) => { Ok(Value::new_float(a as f64 / b)) } (Some(NixNum::Float(a)), Some(NixNum::Int(b))) => { Ok(Value::new_float(a / b as f64)) } _ => Err(Self::err("cannot divide non-numbers")), }, BinOpTag::Eq => Ok(Value::new_inline(self.values_equal(lhs, rhs, strings))), BinOpTag::Neq => Ok(Value::new_inline(!self.values_equal(lhs, rhs, strings))), BinOpTag::Lt => self.compare_values(lhs, rhs, strings, |o| o.is_lt()), BinOpTag::Gt => self.compare_values(lhs, rhs, strings, |o| o.is_gt()), BinOpTag::Leq => self.compare_values(lhs, rhs, strings, |o| o.is_le()), BinOpTag::Geq => self.compare_values(lhs, rhs, strings, |o| o.is_ge()), BinOpTag::Concat => { let Some(l) = lhs.as_gc::>() else { return Err(Self::err("cannot concatenate: left operand is not a list")); }; let Some(r) = rhs.as_gc::>() else { return Err(Self::err("cannot concatenate: right operand is not a list")); }; let mut items = SmallVec::new(); items.extend(l.inner.iter().cloned()); items.extend(r.inner.iter().cloned()); Ok(Value::new_gc(Gc::new(mc, List { inner: items }))) } BinOpTag::Update => { let Some(l) = lhs.as_gc::>() else { return Err(Self::err("cannot update: left operand is not a set")); }; let Some(r) = rhs.as_gc::>() else { return Err(Self::err("cannot update: right operand is not a set")); }; Ok(Value::new_gc(l.merge(&r, mc))) } } } pub(super) fn numeric_binop( &self, lhs: StrictValue<'gc>, rhs: StrictValue<'gc>, mc: &Mutation<'gc>, int_op: fn(i64, i64) -> i64, float_op: fn(f64, f64) -> f64, ) -> VmResult> { match (Self::as_num(lhs), Self::as_num(rhs)) { (Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Self::make_int(int_op(a, b), mc)), (Some(NixNum::Float(a)), Some(NixNum::Float(b))) => { Ok(Value::new_float(float_op(a, b))) } (Some(NixNum::Int(a)), Some(NixNum::Float(b))) => { Ok(Value::new_float(float_op(a as f64, b))) } (Some(NixNum::Float(a)), Some(NixNum::Int(b))) => { Ok(Value::new_float(float_op(a, b as f64))) } _ => Err(Self::err("cannot perform arithmetic on non-numbers")), } } pub(super) fn values_equal( &self, lhs: StrictValue<'gc>, rhs: StrictValue<'gc>, strings: &DefaultStringInterner, ) -> bool { if let (Some(a), Some(b)) = (Self::as_num(lhs), Self::as_num(rhs)) { return match (a, b) { (NixNum::Int(a), NixNum::Int(b)) => a == b, (NixNum::Float(a), NixNum::Float(b)) => a == b, (NixNum::Int(a), NixNum::Float(b)) => a as f64 == b, (NixNum::Float(a), NixNum::Int(b)) => a == b as f64, }; } if let (Some(a), Some(b)) = (lhs.as_inline::(), rhs.as_inline::()) { return a == b; } if lhs.is::() && rhs.is::() { return true; } if let (Some(a), Some(b)) = ( Self::get_string(lhs, strings), Self::get_string(rhs, strings), ) { return a == b; } if let (Some(a), Some(b)) = (lhs.as_gc::>(), rhs.as_gc::>()) { if a.inner.len() != b.inner.len() { return false; } return a.inner.iter().zip(b.inner.iter()).all(|(x, y)| { let (Ok(ForceResult::Ready(x)), Ok(ForceResult::Ready(y))) = (self.force_inline(*x), self.force_inline(*y)) else { return false; }; self.values_equal(x, y, strings) }); } if let (Some(a), Some(b)) = (lhs.as_gc::>(), rhs.as_gc::>()) { if a.len() != b.len() { return false; } return a .iter() .zip(b.iter()) .all(|((k1, v1), (k2, v2))| { if k1 != k2 { return false; } let (Ok(ForceResult::Ready(v1)), Ok(ForceResult::Ready(v2))) = (self.force_inline(*v1), self.force_inline(*v2)) else { return false; }; self.values_equal(v1, v2, strings) }); } false } pub(super) fn compare_values( &self, lhs: StrictValue<'gc>, rhs: StrictValue<'gc>, strings: &DefaultStringInterner, pred: impl FnOnce(std::cmp::Ordering) -> bool, ) -> VmResult> { if let (Some(a), Some(b)) = (Self::as_num(lhs), Self::as_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(std::cmp::Ordering::Less) } (NixNum::Int(a), NixNum::Float(b)) => (a as f64) .partial_cmp(&b) .unwrap_or(std::cmp::Ordering::Less), (NixNum::Float(a), NixNum::Int(b)) => a .partial_cmp(&(b as f64)) .unwrap_or(std::cmp::Ordering::Less), }; return Ok(Value::new_inline(pred(ord))); } if let (Some(a), Some(b)) = ( Self::get_string(lhs, strings), Self::get_string(rhs, strings), ) { return Ok(Value::new_inline(pred(a.cmp(b)))); } Err(Self::err("cannot compare these types")) } pub(super) fn do_select_step( &mut self, set_val: StrictValue<'gc>, keys: SmallVec<[Value<'gc>; 4]>, remaining: u16, span: u32, default: Option>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { let Some(attrs) = set_val.as_gc::>() else { if let Some(def) = default { self.push_stack(def); return Ok(()); } return Err(Self::err("cannot select from non-set")); }; let key_idx = keys.len() - remaining as usize; let key = &keys[key_idx]; let Some(key_sid) = Self::get_string_id(key) else { return Err(Self::err("attribute name must be a string")); }; let found = attrs.lookup(key_sid); if remaining <= 1 { match (found, default) { (Some(v), _) => { self.push_stack(v); Ok(()) } (None, Some(default)) => { self.push_stack(default); Ok(()) } (None, None) => { let name = strings.resolve(key_sid.0).unwrap_or(""); Err(Self::err(format!("attribute '{name}' missing"))) } } } else { match (found, default) { (Some(v), default) => match self.force_inline(v)? { ForceResult::Ready(forced) => { self.do_select_step(forced, keys, remaining - 1, span, default, mc, strings) } other => { self.setup_force( other, AfterForce::Select { keys, remaining: remaining - 1, span, default, }, mc, strings, )?; Ok(()) } }, (None, Some(default)) => { self.push_stack(default); Ok(()) } (None, None) => { let name = strings.resolve(key_sid.0).unwrap_or(""); Err(Self::err(format!("attribute '{name}' missing"))) } } } } pub(super) fn do_has_attr_step( &mut self, set_val: StrictValue<'gc>, keys: SmallVec<[Value<'gc>; 4]>, remaining: u16, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { let Some(attrs) = set_val.as_gc::>() else { self.push_stack(Value::new_inline(false)); return Ok(()); }; let key_idx = keys.len() - remaining as usize; let Some(key_sid) = Self::get_string_id(&keys[key_idx]) else { self.push_stack(Value::new_inline(false)); return Ok(()); }; if remaining <= 1 { self.push_stack(Value::new_inline(attrs.has(key_sid))); Ok(()) } else { match attrs.lookup(key_sid) { Some(v) => match self.force_inline(v)? { ForceResult::Ready(forced) => { self.do_has_attr_step(forced, keys, remaining - 1, mc, strings) } other => { self.setup_force( other, AfterForce::HasAttr { keys, remaining: remaining - 1, }, mc, strings, )?; Ok(()) } }, None => { self.push_stack(Value::new_inline(false)); Ok(()) } } } } fn concat_strings_continue( &mut self, mut forced: SmallVec<[StrictValue<'gc>; 8]>, mut remaining: SmallVec<[Value<'gc>; 8]>, force_string: bool, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { while let Some(part) = remaining.pop() { match self.force_inline(part)? { ForceResult::Ready(v) => forced.push(v), other => { self.setup_force( other, AfterForce::ConcatStrings { forced, remaining, force_string, }, mc, strings, )?; return Ok(()); } } } let mut result = String::new(); for part in &forced { if let Some(s) = Self::get_string(*part, strings) { result.push_str(s); } else if let Some(n) = Self::as_num(*part) { match n { NixNum::Int(i) => result.push_str(&i.to_string()), NixNum::Float(f) => result.push_str(&format!("{f}")), } } else if part.is::() { } else if let Some(b) = part.as_inline::() { if force_string { result.push_str(if b { "1" } else { "" }); } else { return Err(Self::err("cannot coerce a boolean to a string")); } } else { return Err(Self::err("cannot coerce value to string")); } } let ns = Gc::new(mc, NixString::new(result)); self.push_stack(Value::new_gc(ns)); Ok(()) } fn do_with_lookup_step( &mut self, scope_val: StrictValue<'gc>, name: StringId, next: Option>>, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { if let Some(attrs) = scope_val.as_gc::>() && let Some(v) = attrs.lookup(name) { self.push_stack(v); return Ok(()); } match next { Some(scope) => { let env_val = scope.env; let next_prev = scope.prev; match self.force_inline(env_val)? { ForceResult::Ready(forced) => { self.do_with_lookup_step(forced, name, next_prev, mc, strings) } other => { self.setup_force( other, AfterForce::WithLookup { name, next: next_prev, }, mc, strings, )?; Ok(()) } } } None => { let name_str = strings.resolve(name.0).unwrap_or(""); Err(Self::err(format!("undefined variable '{name_str}'"))) } } } fn convert_value( &self, val: &Value<'gc>, strings: &DefaultStringInterner, ) -> crate::value::Value { if let Some(i) = val.as_inline::() { crate::value::Value::Int(i as i64) } else if let Some(gc_i) = val.as_gc::() { crate::value::Value::Int(*gc_i) } else if let Some(f) = val.as_float() { crate::value::Value::Float(f) } else if let Some(b) = val.as_inline::() { crate::value::Value::Bool(b) } else if val.is::() { crate::value::Value::Null } else if let Some(sid) = val.as_inline::() { let s = strings.resolve(sid.0).unwrap_or("").to_owned(); crate::value::Value::String(s) } else if let Some(ns) = val.as_gc::() { crate::value::Value::String(ns.as_str().to_owned()) } else if let Some(attrs) = val.as_gc::>() { let mut map = std::collections::BTreeMap::new(); for (key, val) in attrs.iter() { let key_str = strings.resolve(key.0).unwrap_or("").to_owned(); let converted = self.convert_value(val, strings); map.insert(crate::value::Symbol::from(key_str), converted); } crate::value::Value::AttrSet(crate::value::AttrSet::new(map)) } else if let Some(list) = val.as_gc::>() { let items: Vec<_> = list .inner .iter() .map(|v| self.convert_value(v, strings)) .collect(); crate::value::Value::List(crate::value::List::new(items)) } else if val.is::>() { crate::value::Value::Func } else if val.is::>() { crate::value::Value::Thunk } else if val.as_inline::().is_some() { crate::value::Value::PrimOp("primop".into()) } else if val.is::>() { crate::value::Value::PrimOpApp("primop-app".into()) } else { crate::value::Value::Null } } fn execute_one( &mut self, bc: &[u8], mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> Action { use crate::codegen::Op::{self, *}; #[cfg(debug_assertions)] let opcode_byte = bc[self.pc]; #[cfg(not(debug_assertions))] let opcode_byte = unsafe { *bc.get_unchecked(self.pc) }; self.pc += 1; #[cfg(debug_assertions)] let Ok(op) = Op::try_from_primitive(opcode_byte) else { return Action::Done(Err(Error::eval_error(format!( "unknown opcode: {opcode_byte:#04x}" )))); }; #[cfg(not(debug_assertions))] let op = unsafe { Op::try_from_primitive(opcode_byte).unwrap_unchecked() }; match op { PushSmi => { let val = self.read_i32(bc); self.push_stack(Value::new_inline(val)); } PushBigInt => { let val = self.read_i64(bc); self.push_stack(Value::new_gc(Gc::new(mc, val))); } PushFloat => { let val = self.read_f64(bc); self.push_stack(Value::new_float(val)); } PushString => { let sid = self.read_string_id(bc); self.push_stack(Value::new_inline(sid)); } PushNull => self.push_stack(Value::new_inline(Null)), PushTrue => self.push_stack(Value::new_inline(true)), PushFalse => self.push_stack(Value::new_inline(false)), LoadLocal => { let idx = self.read_u32(bc) as usize; let val = self.env().borrow().locals[idx]; self.push_stack(val); } LoadOuter => { let layer = self.read_u8(bc); let idx = self.read_u32(bc) as usize; let mut env = self.env(); for _ in 0..layer { let prev = env.borrow().prev.expect("LoadOuter: env chain too short"); env = prev; } let val = env.borrow().locals[idx]; self.push_stack(val); } StoreLocal => { let idx = self.read_u32(bc) as usize; let val = self.stack.pop().expect("stack underflow"); self.env().borrow_mut(mc).locals[idx] = val; } AllocLocals => { let count = self.read_u32(bc) as usize; self.env() .borrow_mut(mc) .locals .resize(count, Value::default()); } MakeThunk => { let entry_point = self.read_u32(bc); let _label = self.read_string_id(bc); let thunk = Gc::new( mc, RefLock::new(ThunkState::Pending { ip: entry_point, env: self.env(), }), ); self.push_stack(Value::new_gc(thunk)); } MakeClosure => { let entry_point = self.read_u32(bc); let n_locals = self.read_u32(bc); let closure = Gc::new( mc, Closure { ip: entry_point, n_locals, env: self.env(), pattern: None, }, ); self.push_stack(Value::new_gc(closure)); } MakePatternClosure => { let entry_point = self.read_u32(bc); let n_locals = self.read_u32(bc); let req_count = self.read_u16(bc) as usize; let opt_count = self.read_u16(bc) as usize; let has_ellipsis = self.read_u8(bc) != 0; let mut required = SmallVec::new(); for _ in 0..req_count { required.push(self.read_string_id(bc)); } let mut optional = SmallVec::new(); for _ in 0..opt_count { optional.push(self.read_string_id(bc)); } let total = req_count + opt_count; let mut param_spans = Vec::with_capacity(total); for _ in 0..total { let name = self.read_string_id(bc); let span_id = self.read_u32(bc); param_spans.push((name, span_id)); } let pattern = Gc::new( mc, PatternInfo { required, optional, ellipsis: has_ellipsis, param_spans: param_spans.into_boxed_slice(), }, ); let closure = Gc::new( mc, Closure { ip: entry_point, n_locals, env: self.env(), pattern: Some(pattern), }, ); self.push_stack(Value::new_gc(closure)); } Call => { let span_id = self.read_u32(bc); let arg = self.stack.pop().expect("stack underflow"); let func = self.stack.pop().expect("stack underflow"); match self.force_inline(func) { Ok(ForceResult::Ready(f)) => { try_vm!(self, self.do_call(f, arg, Some(span_id), mc, strings)) } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::Call { arg, span: Some(span_id), }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), } } CallNoSpan => { let arg = self.stack.pop().expect("stack underflow"); let func = self.stack.pop().expect("stack underflow"); match self.force_inline(func) { Ok(ForceResult::Ready(f)) => { try_vm!(self, self.do_call(f, arg, None, mc, strings)) } Ok(other) => { try_vm!( self, self.setup_force(other, AfterForce::Call { arg, span: None }, mc, strings) ); } Err(e) => return self.handle_vm_error(e), } } MakeAttrs => { let count = self.read_u32(bc) as usize; let total = 3 * count; let items = self.stack.pop_n::<16>(total); let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new(); for i in 0..count { let key = &items[2 * i]; let val = items[2 * i + 1]; let key_sid = Self::get_string_id(key).expect("MakeAttrs: key must be StringId"); entries.push((key_sid, val)); } entries.sort_by_key(|(k, _)| *k); let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); self.push_stack(Value::new_gc(attrs)); } MakeAttrsDyn => { let static_count = self.read_u32(bc) as usize; let dynamic_count = self.read_u32(bc) as usize; let total = 3 * static_count + 3 * dynamic_count; let items = self.stack.pop_n::<16>(total); let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new(); for i in 0..static_count { let key_sid = Self::get_string_id(&items[2 * i]) .expect("MakeAttrsDyn: static key must be StringId"); entries.push((key_sid, items[2 * i + 1])); } let dyn_base = 3 * static_count; for i in 0..dynamic_count { let key = &items[dyn_base + 3 * i]; let val = items[dyn_base + 3 * i + 1]; if key.is::() { continue; } let Some(key_sid) = Self::get_string_id(key) else { return Action::Done(Err(Error::eval_error( "dynamic attribute name must be a string", ))); }; entries.push((key_sid, val)); } entries.sort_by_key(|(k, _)| *k); // FIXME: incorrect!!! entries.dedup_by_key(|(k, _)| *k); let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); self.push_stack(Value::new_gc(attrs)); } MakeEmptyAttrs => { let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(SmallVec::new()) }); self.push_stack(Value::new_gc(attrs)); } Select => { let n = self.read_u16(bc) as usize; let span_id = self.read_u32(bc); let mut keys = self.stack.pop_n::<4>(n); let expr = self.stack.pop().expect("stack underflow"); keys.reverse(); let remaining = keys.len() as u16; match self.force_inline(expr) { Ok(ForceResult::Ready(forced)) => { try_vm!( self, self.do_select_step( forced, keys, remaining, span_id, None, mc, strings, ) ); } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::Select { keys, remaining, span: span_id, default: None, }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), } } SelectDefault => { let n = self.read_u16(bc) as usize; let span_id = self.read_u32(bc); let default = self.stack.pop().expect("stack underflow"); let mut keys = self.stack.pop_n::<4>(n); let expr = self.stack.pop().expect("stack underflow"); keys.reverse(); let remaining = keys.len() as u16; match self.force_inline(expr) { Ok(ForceResult::Ready(forced)) => { try_vm!( self, self.do_select_step( forced, keys, remaining, span_id, Some(default), mc, strings, ) ); } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::Select { keys, remaining, span: span_id, default: Some(default), }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), } } HasAttr => { let n = self.read_u16(bc) as usize; let mut keys = self.stack.pop_n::<4>(n); let expr = self.stack.pop().expect("stack underflow"); keys.reverse(); let remaining = keys.len() as u16; match self.force_inline(expr) { Ok(ForceResult::Ready(forced)) => { try_vm!(self, self.do_has_attr_step(forced, keys, remaining, mc, strings)); } Ok(ForceResult::NeedEval { ip, env, thunk }) => { self.push_force_frame( thunk, AfterForce::HasAttr { keys, remaining }, ip, env, mc, ); } Ok(ForceResult::NeedApply(thunk)) => { try_vm!( self, self.push_apply_force_frame( thunk, AfterForce::HasAttr { keys, remaining }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), } } MakeList => { let count = self.read_u32(bc) as usize; let items = self.stack.pop_n::<4>(count); let list = Gc::new(mc, List { inner: items }); self.push_stack(Value::new_gc(list)); } OpAdd | OpSub | OpMul | OpDiv | OpEq | OpNeq | OpLt | OpGt | OpLeq | OpGeq | OpConcat | OpUpdate => { let tag = match op { OpAdd => BinOpTag::Add, OpSub => BinOpTag::Sub, OpMul => BinOpTag::Mul, OpDiv => BinOpTag::Div, OpEq => BinOpTag::Eq, OpNeq => BinOpTag::Neq, OpLt => BinOpTag::Lt, OpGt => BinOpTag::Gt, OpLeq => BinOpTag::Leq, OpGeq => BinOpTag::Geq, OpConcat => BinOpTag::Concat, OpUpdate => BinOpTag::Update, _ => unreachable!(), }; let rhs = self.stack.pop().expect("stack underflow"); let lhs = self.stack.pop().expect("stack underflow"); match self.force_inline(lhs) { Ok(ForceResult::Ready(lhs_f)) => match self.force_inline(rhs) { Ok(ForceResult::Ready(rhs_f)) => { match self.compute_binop(tag, lhs_f, rhs_f, mc, strings) { Ok(r) => self.push_stack(r), Err(e) => return self.handle_vm_error(e), } } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::BinOpRhs { lhs_forced: lhs_f, op: tag, }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), }, Ok(other) => { try_vm!( self, self.setup_force(other, AfterForce::BinOpLhs { rhs, op: tag }, mc, strings) ); } Err(e) => return self.handle_vm_error(e), } } OpNeg => { let val = self.stack.pop().expect("stack underflow"); match self.force_inline(val) { Ok(ForceResult::Ready(f)) => match Self::as_num(f) { Some(NixNum::Int(i)) => self .push_stack(Self::make_int(-i, mc)), Some(NixNum::Float(fl)) => self .push_stack(Value::new_float(-fl)), None => { return Action::Done(Err(Error::eval_error( "cannot negate non-number", ))); } }, Ok(other) => { try_vm!(self, self.setup_force(other, AfterForce::UnNeg, mc, strings)); } Err(e) => return self.handle_vm_error(e), } } OpNot => { let val = self.stack.pop().expect("stack underflow"); match self.force_inline(val) { Ok(ForceResult::Ready(f)) => match f.as_inline::() { Some(b) => self .push_stack(Value::new_inline(!b)), None => { return Action::Done(Err(Error::eval_error("value is not a boolean"))); } }, Ok(other) => { try_vm!(self, self.setup_force(other, AfterForce::UnNot, mc, strings)); } Err(e) => return self.handle_vm_error(e), } } ForceBool => { let val = self.stack.pop().expect("stack underflow"); match self.force_inline(val) { Ok(ForceResult::Ready(f)) => { if f.as_inline::().is_none() { return Action::Done(Err(Error::eval_error("value is not a boolean"))); } self.push_stack(f.relax()); } Ok(ForceResult::NeedEval { ip, env, thunk }) => { self.push_force_frame(thunk, AfterForce::ForceBool, ip, env, mc); } Ok(ForceResult::NeedApply(thunk)) => { try_vm!( self, self.push_apply_force_frame(thunk, AfterForce::ForceBool, mc, strings) ); } Err(e) => return self.handle_vm_error(e), } } JumpIfFalse => { let offset = self.read_i32(bc); let val = self.stack.pop().expect("stack underflow"); if let Some(false) = val.as_inline::() { self.pc = ((self.pc as isize) + (offset as isize)) as usize; } } JumpIfTrue => { let offset = self.read_i32(bc); let val = self.stack.pop().expect("stack underflow"); if let Some(true) = val.as_inline::() { self.pc = ((self.pc as isize) + (offset as isize)) as usize; } } Jump => { let offset = self.read_i32(bc); self.pc = ((self.pc as isize) + (offset as isize)) as usize; } ConcatStrings => { let parts_count = self.read_u16(bc) as usize; let force_string = self.read_u8(bc) != 0; let parts = self.stack.pop_n::<8>(parts_count); let mut remaining = parts; remaining.reverse(); let forced = SmallVec::new(); try_vm!( self, self.concat_strings_continue(forced, remaining, force_string, mc, strings,) ); } ResolvePath => { let _val = self.stack.pop().expect("stack underflow"); self.push_stack(Value::new_inline(Null)); } Assert => { let raw_idx = self.read_u32(bc); let span_id = self.read_u32(bc); let expr = self.stack.pop().expect("stack underflow"); let assertion = self.stack.pop().expect("stack underflow"); match self.force_inline(assertion) { Ok(ForceResult::Ready(f)) => match f.as_inline::() { Some(true) => self.push_stack(expr), Some(false) => { let sym = string_interner::symbol::SymbolU32::try_from_usize( raw_idx as usize, ); let msg = sym.and_then(|s| strings.resolve(s)).unwrap_or(""); return self.handle_vm_error(VmError::Uncatchable(Error::eval_error( format!("assertion '{msg}' failed"), ))); } None => { return self.handle_vm_error(Self::err( "assertion condition must be a boolean", )); } }, Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::Assert { expr, raw_idx, span_id, }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), } } PushWith => { let ns = self.stack.pop().expect("stack underflow"); let scope = Gc::new( mc, WithScope { env: ns, prev: self.with_scope, }, ); self.with_scope = Some(scope); } PopWith => { if let Some(scope) = self.with_scope { self.with_scope = scope.prev; } } WithLookup => { let name = self.read_string_id(bc); match self.with_scope { Some(scope_gc) => { let env_val = scope_gc.env; let next = scope_gc.prev; match self.force_inline(env_val) { Ok(ForceResult::Ready(forced)) => { try_vm!( self, self.do_with_lookup_step(forced, name, next, mc, strings) ); } Ok(other) => { try_vm!( self, self.setup_force( other, AfterForce::WithLookup { name, next }, mc, strings, ) ); } Err(e) => return self.handle_vm_error(e), } } None => { let name_str = strings.resolve(name.0).unwrap_or(""); return self.handle_vm_error(Self::err(format!( "undefined variable '{name_str}'" ))); } } } LoadBuiltins => { self.push_stack(self.globals.builtins); } LoadBuiltin => { let name_raw = self.read_string_id(bc); if let Some(&primop) = self.globals.builtin_lookup.get(&name_raw) { self.push_stack(Value::new_inline(primop)); } else { return Action::Done(Err(Error::eval_error(format!( "unknown builtin (id {:?})", name_raw )))); } } MkPos => { let _span_id = self.read_u32(bc); todo!("MkPos") } LoadReplBinding => { let _name = self.read_string_id(bc); todo!("LoadReplBinding") } LoadScopedBinding => { let _name = self.read_string_id(bc); todo!("LoadScopedBinding") } Return => { let ret_val = self.stack.pop().expect("stack underflow"); return self.handle_return(ret_val, mc, strings); } } Action::Continue } pub(super) fn run_batch( &mut self, bytecode: &[u8], pc: &mut usize, mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> Action { const COLLECTOR_GRANULARITY: f64 = 1024.0; const BATCH_SIZE: usize = 1024; if !self.started { self.pc = *pc; self.current_env = Some(Gc::new(mc, RefLock::new(Env::empty()))); self.started = true; } for _ in 0..BATCH_SIZE { let action = self.execute_one(bytecode, mc, strings); match action { Action::Continue => {} other => { *pc = self.pc; if matches!(other, Action::Done(_)) { self.started = false; } return other; } } } *pc = self.pc; if mc.metrics().allocation_debt() > COLLECTOR_GRANULARITY { Action::NeedGc } else { Action::Continue } } fn vm_try(&mut self, result: VmResult) -> std::result::Result { match result { Ok(ok) => Ok(ok), Err(err) => Err(self.handle_vm_error(err)), } } fn handle_vm_error(&mut self, e: VmError) -> Action { match e { VmError::Catchable(msg) => { // Check for tryEval catch frames if let Some(catch) = self.find_catch_frame() { self.restore_catch_frame(catch); return Action::Continue; } Action::Done(Err(Error::catchable(msg))) } VmError::Uncatchable(e) => Action::Done(Err(e)), } } fn find_catch_frame(&mut self) -> Option { todo!("find catch frame") } fn restore_catch_frame(&mut self, _catch: CatchFrameInfo) { todo!("restore catch frame") } #[inline(always)] pub(super) fn push_stack(&mut self, val: Value<'gc>) { #[cfg(debug_assertions)] self.stack.push(val).expect("stack overflow"); #[cfg(not(debug_assertions))] unsafe { self.stack.push_unchecked(val); } } #[inline(always)] pub(super) fn push_empty_list(&mut self) { self.push_stack(self.globals.empty_list); } #[inline(always)] pub(super) fn push_empty_attrs(&mut self) { self.push_stack(self.globals.empty_attrs); } } struct CatchFrameInfo;