From 1950d4de6c41e3ead4655cad9bf1240046b3a5a1 Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Mon, 16 Mar 2026 18:03:40 +0800 Subject: [PATCH] init builtins --- Cargo.lock | 9 +- Cargo.toml | 1 - boxing/Cargo.toml | 8 - boxing/src/lib.rs | 2 - boxing/src/nan.rs | 7 - boxing/src/utils.rs | 16 - fix/Cargo.toml | 2 +- boxing/src/nan/raw.rs => fix/src/boxing.rs | 99 +- fix/src/error.rs | 7 +- fix/src/lib.rs | 1 + fix/src/main.rs | 6 +- fix/src/runtime.rs | 18 +- fix/src/runtime/builtins.rs | 196 +++ fix/src/runtime/primops.rs | 929 +++++++++++++ fix/src/runtime/stack.rs | 10 +- fix/src/runtime/value.rs | 76 +- fix/src/runtime/vm.rs | 1365 +++++++++++++------- fix/src/store.rs | 1 - fix/src/store/daemon.rs | 11 +- fix/src/store/validation.rs | 71 +- fix/src/value.rs | 9 + flake.lock | 18 +- 22 files changed, 2197 insertions(+), 665 deletions(-) delete mode 100644 boxing/Cargo.toml delete mode 100644 boxing/src/lib.rs delete mode 100644 boxing/src/nan.rs delete mode 100644 boxing/src/utils.rs rename boxing/src/nan/raw.rs => fix/src/boxing.rs (82%) create mode 100644 fix/src/runtime/primops.rs diff --git a/Cargo.lock b/Cargo.lock index 0eee692..6a66e0f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -188,13 +188,6 @@ dependencies = [ "generic-array", ] -[[package]] -name = "boxing" -version = "0.1.3" -dependencies = [ - "sptr", -] - [[package]] name = "bstr" version = "1.12.1" @@ -798,7 +791,6 @@ version = "0.1.0" dependencies = [ "anyhow", "base64", - "boxing", "bumpalo", "bzip2", "clap", @@ -833,6 +825,7 @@ dependencies = [ "sha2", "small-map", "smallvec", + "sptr", "string-interner", "tap", "tar", diff --git a/Cargo.toml b/Cargo.toml index deba82d..8dc1a68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,6 @@ resolver = "3" members = [ "fix", - "boxing", ] [profile.profiling] diff --git a/boxing/Cargo.toml b/boxing/Cargo.toml deleted file mode 100644 index fc0becf..0000000 --- a/boxing/Cargo.toml +++ /dev/null @@ -1,8 +0,0 @@ -[package] -name = "boxing" -version = "0.1.3" -edition = "2021" -description = "NaN-boxing primitives (local fork with bool fix)" - -[dependencies] -sptr = "0.3" diff --git a/boxing/src/lib.rs b/boxing/src/lib.rs deleted file mode 100644 index 0bb480f..0000000 --- a/boxing/src/lib.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod nan; -mod utils; diff --git a/boxing/src/nan.rs b/boxing/src/nan.rs deleted file mode 100644 index d653ded..0000000 --- a/boxing/src/nan.rs +++ /dev/null @@ -1,7 +0,0 @@ -pub mod raw; - -pub use raw::RawBox; - -const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF; -const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000; -const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000; diff --git a/boxing/src/utils.rs b/boxing/src/utils.rs deleted file mode 100644 index 2fe7ee3..0000000 --- a/boxing/src/utils.rs +++ /dev/null @@ -1,16 +0,0 @@ -pub trait ArrayExt { - type Elem; - - fn truncate_to(self) -> [Self::Elem; M]; -} - -impl ArrayExt for [T; N] { - type Elem = T; - - fn truncate_to(self) -> [Self::Elem; M] { - let copy_len = usize::min(N, M); - let mut out = [T::default(); M]; - out[0..copy_len].copy_from_slice(&self[0..copy_len]); - out - } -} diff --git a/fix/Cargo.toml b/fix/Cargo.toml index 2234b4a..7e81134 100644 --- a/fix/Cargo.toml +++ b/fix/Cargo.toml @@ -79,7 +79,7 @@ tap = "1.0.1" ghost-cell = "0.2" colored = "3.1" -boxing = { path = "../boxing" } +sptr = "0.3" sealed = "0.6" small-map = "0.1" smallvec = "1.15" diff --git a/boxing/src/nan/raw.rs b/fix/src/boxing.rs similarity index 82% rename from boxing/src/nan/raw.rs rename to fix/src/boxing.rs index 1d70fcd..7bd70bb 100644 --- a/boxing/src/nan/raw.rs +++ b/fix/src/boxing.rs @@ -1,11 +1,28 @@ -use super::{NEG_QUIET_NAN, QUIET_NAN, SIGN_MASK}; -use crate::utils::ArrayExt; -use sptr::Strict; use std::fmt; -use std::mem::ManuallyDrop; use std::num::NonZeroU8; -pub trait RawStore: Sized { +use sptr::Strict; + +const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF; +const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000; +const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000; + +pub(crate) trait ArrayExt { + type Elem; + fn truncate_to(self) -> [Self::Elem; M]; +} + +impl ArrayExt for [T; N] { + type Elem = T; + fn truncate_to(self) -> [Self::Elem; M] { + let copy_len = usize::min(N, M); + let mut out = [T::default(); M]; + out[0..copy_len].copy_from_slice(&self[0..copy_len]); + out + } +} + +pub(crate) trait RawStore: Sized { fn to_val(self, value: &mut Value); fn from_val(value: &Value) -> Self; } @@ -138,18 +155,18 @@ enum TagVal { } #[derive(Copy, Clone, PartialEq, Eq)] -pub struct RawTag(TagVal); +pub(crate) struct RawTag(TagVal); impl RawTag { #[inline] #[must_use] - pub fn new(neg: bool, val: NonZeroU8) -> RawTag { + pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag { unsafe { Self::new_unchecked(neg, val.get() & 0x07) } } #[inline] #[must_use] - pub fn new_checked(neg: bool, val: u8) -> Option { + pub(crate) fn new_checked(neg: bool, val: u8) -> Option { Some(RawTag(match (neg, val) { (false, 1) => TagVal::_P1, (false, 2) => TagVal::_P2, @@ -176,7 +193,7 @@ impl RawTag { /// `val` must be in the range `1..8` #[inline] #[must_use] - pub unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag { + pub(crate) unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag { RawTag(match (neg, val) { (false, 1) => TagVal::_P1, (false, 2) => TagVal::_P2, @@ -200,7 +217,7 @@ impl RawTag { #[inline] #[must_use] - pub fn is_neg(self) -> bool { + pub(crate) fn is_neg(self) -> bool { matches!(self.0, |TagVal::_N1| TagVal::_N2 | TagVal::_N3 | TagVal::_N4 @@ -211,7 +228,7 @@ impl RawTag { #[inline] #[must_use] - pub fn val(self) -> NonZeroU8 { + pub(crate) fn val(self) -> NonZeroU8 { match self.0 { TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN, TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1), @@ -225,7 +242,7 @@ impl RawTag { #[inline] #[must_use] - pub fn neg_val(self) -> (bool, u8) { + pub(crate) fn neg_val(self) -> (bool, u8) { match self.0 { TagVal::_P1 => (false, 1), TagVal::_P2 => (false, 2), @@ -286,9 +303,9 @@ impl Header { } } -#[derive(Clone, Debug, PartialEq)] +#[derive(Copy, Clone, Debug, PartialEq)] #[repr(C, align(8))] -pub struct Value { +pub(crate) struct Value { #[cfg(target_endian = "big")] header: Header, data: [u8; 6], @@ -298,7 +315,7 @@ pub struct Value { impl Value { #[inline] - pub fn new(tag: RawTag, data: [u8; 6]) -> Value { + pub(crate) fn new(tag: RawTag, data: [u8; 6]) -> Value { Value { header: Header::new(tag), data, @@ -306,23 +323,23 @@ impl Value { } #[inline] - pub fn empty(tag: RawTag) -> Value { + pub(crate) fn empty(tag: RawTag) -> Value { Value::new(tag, [0; 6]) } - pub fn store(tag: RawTag, val: T) -> Value { + pub(crate) fn store(tag: RawTag, val: T) -> Value { let mut v = Value::new(tag, [0; 6]); T::to_val(val, &mut v); v } - pub fn load(self) -> T { + pub(crate) fn load(self) -> T { T::from_val(&self) } #[inline] #[must_use] - pub fn tag(&self) -> RawTag { + pub(crate) fn tag(&self) -> RawTag { self.header.tag() } @@ -332,41 +349,42 @@ impl Value { } #[inline] - pub fn set_data(&mut self, val: [u8; 6]) { + pub(crate) fn set_data(&mut self, val: [u8; 6]) { self.data = val; } #[inline] #[must_use] - pub fn data(&self) -> &[u8; 6] { + pub(crate) fn data(&self) -> &[u8; 6] { &self.data } #[inline] #[must_use] - pub fn data_mut(&mut self) -> &mut [u8; 6] { + pub(crate) fn data_mut(&mut self) -> &mut [u8; 6] { &mut self.data } #[inline] #[must_use] - pub unsafe fn whole(&self) -> &[u8; 8] { + unsafe fn whole(&self) -> &[u8; 8] { let ptr = (self as *const Value).cast::<[u8; 8]>(); unsafe { &*ptr } } #[inline] #[must_use] - pub unsafe fn whole_mut(&mut self) -> &mut [u8; 8] { + unsafe fn whole_mut(&mut self) -> &mut [u8; 8] { let ptr = (self as *mut Value).cast::<[u8; 8]>(); unsafe { &mut *ptr } } } #[repr(C)] -pub union RawBox { +#[derive(Copy, Clone)] +pub(crate) union RawBox { float: f64, - value: ManuallyDrop, + value: Value, bits: u64, #[cfg(target_pointer_width = "64")] ptr: *const (), @@ -377,7 +395,7 @@ pub union RawBox { impl RawBox { #[inline] #[must_use] - pub fn from_float(val: f64) -> RawBox { + pub(crate) fn from_float(val: f64) -> RawBox { match (val.is_nan(), val.is_sign_positive()) { (true, true) => RawBox { float: f64::from_bits(QUIET_NAN), @@ -391,15 +409,13 @@ impl RawBox { #[inline] #[must_use] - pub fn from_value(value: Value) -> RawBox { - RawBox { - value: ManuallyDrop::new(value), - } + pub(crate) fn from_value(value: Value) -> RawBox { + RawBox { value } } #[inline] #[must_use] - pub fn tag(&self) -> Option { + pub(crate) fn tag(&self) -> Option { if self.is_value() { Some(unsafe { self.value.tag() }) } else { @@ -409,19 +425,19 @@ impl RawBox { #[inline] #[must_use] - pub fn is_float(&self) -> bool { + pub(crate) fn is_float(&self) -> bool { (unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN }) } #[inline] #[must_use] - pub fn is_value(&self) -> bool { + pub(crate) fn is_value(&self) -> bool { (unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN }) } #[inline] #[must_use] - pub fn float(&self) -> Option<&f64> { + pub(crate) fn float(&self) -> Option<&f64> { if self.is_float() { Some(unsafe { &self.float }) } else { @@ -431,7 +447,7 @@ impl RawBox { #[inline] #[must_use] - pub fn value(&self) -> Option<&Value> { + pub(crate) fn value(&self) -> Option<&Value> { if self.is_value() { Some(unsafe { &self.value }) } else { @@ -440,20 +456,11 @@ impl RawBox { } #[inline] - pub fn into_float_unchecked(self) -> f64 { + pub(crate) fn into_float_unchecked(self) -> f64 { unsafe { self.float } } } -impl Clone for RawBox { - #[inline] - fn clone(&self) -> Self { - RawBox { - ptr: unsafe { self.ptr }, - } - } -} - impl fmt::Debug for RawBox { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self.float() { diff --git a/fix/src/error.rs b/fix/src/error.rs index 40fdb65..b701cd6 100644 --- a/fix/src/error.rs +++ b/fix/src/error.rs @@ -125,8 +125,6 @@ pub enum Error { #[label("error occurred here")] span: Option, message: String, - #[help] - js_backtrace: Option, #[related] stack_trace: Vec, }, @@ -163,12 +161,11 @@ impl Error { .into() } - pub fn eval_error(msg: String, backtrace: Option) -> Box { + pub fn eval_error(msg: impl Into) -> Box { Error::EvalError { src: None, span: None, - message: msg, - js_backtrace: backtrace, + message: msg.into(), stack_trace: Vec::new(), } .into() diff --git a/fix/src/lib.rs b/fix/src/lib.rs index 0599478..0035996 100644 --- a/fix/src/lib.rs +++ b/fix/src/lib.rs @@ -1,6 +1,7 @@ #![warn(clippy::unwrap_used)] #![allow(dead_code)] +mod boxing; pub mod error; pub mod logging; pub mod runtime; diff --git a/fix/src/main.rs b/fix/src/main.rs index ade11fd..c2b92da 100644 --- a/fix/src/main.rs +++ b/fix/src/main.rs @@ -10,7 +10,7 @@ use rustyline::DefaultEditor; use rustyline::error::ReadlineError; #[derive(Parser)] -#[command(name = "nix-js", about = "Nix expression evaluator")] +#[command(name = "fix", about = "Nix expression evaluator")] struct Cli { #[command(subcommand)] command: Command, @@ -40,8 +40,8 @@ struct ExprSource { file: Option, } -fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> { - let src = if let Some(expr) = src.expr { +fn run_compile(_runtime: &mut Runtime, src: ExprSource, _silent: bool) -> Result<()> { + let _src = if let Some(expr) = src.expr { Source::new_eval(expr)? } else if let Some(file) = src.file { Source::new_file(file)? diff --git a/fix/src/runtime.rs b/fix/src/runtime.rs index 390f169..66d593a 100644 --- a/fix/src/runtime.rs +++ b/fix/src/runtime.rs @@ -16,6 +16,7 @@ use crate::store::{DaemonStore, StoreConfig}; use crate::value::Symbol; mod builtins; +mod primops; mod stack; mod value; mod vm; @@ -43,6 +44,7 @@ impl Runtime { let store = DaemonStore::connect(&config.daemon_socket)?; Ok(Self { + arena: Arena::new(|mc| VM::new(mc, &mut strings)), global_env, store, strings, @@ -50,7 +52,6 @@ impl Runtime { bytecode: Vec::new(), sources: Vec::new(), spans: Vec::new(), - arena: Arena::new(|mc| VM::new(mc)), }) } @@ -108,8 +109,11 @@ impl Runtime { bump, token, strings, - source: sources.last().unwrap().clone(), - scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope.into_iter()).collect(), + source: sources.last().expect("no current source").clone(), + scopes: [Scope::Global(global_env)] + .into_iter() + .chain(extra_scope) + .collect(), with_scope_count: 0, arg_count: 0, thunk_count, @@ -117,7 +121,11 @@ impl Runtime { } } - fn downgrade<'a>(&'a mut self, source: Source, extra_scope: Option>) -> Result { + fn downgrade<'a>( + &'a mut self, + source: Source, + extra_scope: Option>, + ) -> Result { tracing::debug!("Parsing Nix expression"); self.sources.push(source.clone()); @@ -496,7 +504,7 @@ impl OwnedIr { unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self { Self { _bump: bump, - ir: unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) } + ir: unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) }, } } diff --git a/fix/src/runtime/builtins.rs b/fix/src/runtime/builtins.rs index 548c391..381d885 100644 --- a/fix/src/runtime/builtins.rs +++ b/fix/src/runtime/builtins.rs @@ -1,11 +1,204 @@ +use gc_arena::Collect; use hashbrown::HashMap; +use num_enum::TryFromPrimitive; use string_interner::DefaultStringInterner; +use super::value::*; use crate::ir::{Ir, RawIrRef, StringId}; +/// Generates both the BUILTINS const table and the BuiltinId enum +/// from a single source of truth, preventing index desync. +macro_rules! define_builtins { + ($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => { + /// Builtin function registry. + /// Array index IS the PrimOp id. (name, arity) pairs. + pub(super) const BUILTINS: &[(&str, u8)] = &[ + $(($name, $arity),)* + ]; + + #[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, Collect)] + #[repr(u8)] + #[collect(require_static)] + pub(super) enum BuiltinId { + $($variant,)* + } + }; +} + +define_builtins! { + ("abort", Abort, 1), + ("add", Add, 2), + ("addErrorContext", AddErrorContext, 2), + ("all", All, 2), + ("any", Any, 2), + ("appendContext", AppendContext, 2), + ("attrNames", AttrNames, 1), + ("attrValues", AttrValues, 1), + ("baseNameOf", BaseNameOf, 1), + ("bitAnd", BitAnd, 2), + ("bitOr", BitOr, 2), + ("bitXor", BitXor, 2), + ("catAttrs", CatAttrs, 2), + ("ceil", Ceil, 1), + ("compareVersions", CompareVersions, 2), + ("concatLists", ConcatLists, 1), + ("concatMap", ConcatMap, 2), + ("concatStringsSep", ConcatStringsSep, 2), + ("convertHash", ConvertHash, 1), + ("deepSeq", DeepSeq, 2), + ("derivation", Derivation, 1), + ("derivationStrict", DerivationStrict, 1), + ("dirOf", DirOf, 1), + ("div", Div, 2), + ("elem", Elem, 2), + ("elemAt", ElemAt, 2), + ("fetchGit", FetchGit, 1), + ("fetchMercurial", FetchMercurial, 1), + ("fetchTarball", FetchTarball, 1), + ("fetchTree", FetchTree, 1), + ("fetchurl", FetchUrl, 1), + ("filter", Filter, 2), + ("filterSource", FilterSource, 2), + ("findFile", FindFile, 2), + ("floor", Floor, 1), + ("foldl'", FoldlStrict, 3), + ("fromJSON", FromJSON, 1), + ("fromTOML", FromTOML, 1), + ("functionArgs", FunctionArgs, 1), + ("genList", GenList, 2), + ("genericClosure", GenericClosure, 1), + ("getAttr", GetAttr, 2), + ("getContext", GetContext, 1), + ("getEnv", GetEnv, 1), + ("groupBy", GroupBy, 2), + ("hasAttr", HasAttr, 2), + ("hasContext", HasContext, 1), + ("hashFile", HashFile, 2), + ("hashString", HashString, 2), + ("head", Head, 1), + ("import", Import, 1), + ("intersectAttrs", IntersectAttrs, 2), + ("isAttrs", IsAttrs, 1), + ("isBool", IsBool, 1), + ("isFloat", IsFloat, 1), + ("isFunction", IsFunction, 1), + ("isInt", IsInt, 1), + ("isList", IsList, 1), + ("isNull", IsNull, 1), + ("isPath", IsPath, 1), + ("isString", IsString, 1), + ("length", Length, 1), + ("lessThan", LessThan, 2), + ("listToAttrs", ListToAttrs, 1), + ("map", Map, 2), + ("mapAttrs", MapAttrs, 2), + ("match", Match, 2), + ("mul", Mul, 2), + ("null", Null, 0), // constant, not a function + ("parseDrvName", ParseDrvName, 1), + ("partition", Partition, 2), + ("path", Path, 1), + ("pathExists", PathExists, 1), + ("placeholder", Placeholder, 1), + ("readDir", ReadDir, 1), + ("readFile", ReadFile, 1), + ("readFileType", ReadFileType, 1), + ("removeAttrs", RemoveAttrs, 2), + ("replaceStrings", ReplaceStrings, 3), + ("scopedImport", ScopedImport, 2), + ("seq", Seq, 2), + ("sort", Sort, 2), + ("split", Split, 2), + ("splitVersion", SplitVersion, 1), + ("storePath", StorePath, 1), + ("stringLength", StringLength, 1), + ("sub", Sub, 2), + ("substring", Substring, 3), + ("tail", Tail, 1), + ("throw", Throw, 1), + ("toFile", ToFile, 2), + ("toJSON", ToJSON, 1), + ("toPath", ToPath, 1), + ("toString", ToString, 1), + ("toXML", ToXML, 1), + ("trace", Trace, 2), + ("tryEval", TryEval, 1), + ("typeOf", TypeOf, 1), + ("unsafeDiscardStringContext", UnsafeDiscardStringContext, 1), + ("unsafeGetAttrPos", UnsafeGetAttrPos, 2), + ("warn", Warn, 2), + ("zipAttrsWith", ZipAttrsWith, 2), + ("break", Break, 1), +} + +/// Names that need to be pre-interned for builtin implementations. +const EXTRA_INTERN_NAMES: &[&str] = &[ + "builtins", + "currentSystem", + "langVersion", + "nixVersion", + "storeDir", + "nixPath", + "true", + "false", + // typeOf return values + "int", + "float", + "bool", + "string", + "path", + "null", + "set", + "list", + "lambda", + // attrset keys used by builtins + "name", + "value", + "success", + "right", + "wrong", + "key", + "operator", + "startSet", + "__toString", + "outPath", + "__functor", + "drvPath", + "type", + "derivation", + "version", +]; + +/// Returns true if this builtin has lazy argument semantics +/// (not all args should be forced before dispatch). +pub(super) fn is_lazy_builtin(id: BuiltinId) -> bool { + matches!( + id, + BuiltinId::Seq + | BuiltinId::DeepSeq + | BuiltinId::Trace + | BuiltinId::Warn + | BuiltinId::TryEval + | BuiltinId::AddErrorContext + | BuiltinId::Break + ) +} + +/// Intern all builtin names and extra names needed at runtime. +fn intern_all_builtins(interner: &mut DefaultStringInterner) { + for &(name, _) in BUILTINS { + interner.get_or_intern(name); + } + for &name in EXTRA_INTERN_NAMES { + interner.get_or_intern(name); + } +} + pub(super) fn new_builtins_env( interner: &mut DefaultStringInterner, ) -> HashMap>> { + intern_all_builtins(interner); + let mut builtins = HashMap::new(); let builtins_sym = StringId(interner.get_or_intern("builtins")); builtins.insert(builtins_sym, Ir::Builtins); @@ -49,3 +242,6 @@ pub(super) fn new_builtins_env( builtins } + +pub(super) type PrimOpArgs<'gc> = [Value<'gc>; 3]; +pub(super) type PrimOpStrictArgs<'gc> = [StrictValue<'gc>; 3]; diff --git a/fix/src/runtime/primops.rs b/fix/src/runtime/primops.rs new file mode 100644 index 0000000..1e5d034 --- /dev/null +++ b/fix/src/runtime/primops.rs @@ -0,0 +1,929 @@ +use gc_arena::{Collect, Gc, Mutation, RefLock}; +use smallvec::SmallVec; +use string_interner::DefaultStringInterner; + +use super::builtins::{BuiltinId, PrimOpArgs, PrimOpStrictArgs}; +use super::value::*; +use super::vm::{ForceResult, VM, VmError}; +use crate::ir::StringId; + +pub(super) enum BuiltinResult<'gc> { + Done(Value<'gc>), + Force(BuiltinState<'gc>, Value<'gc>), + Call(BuiltinState<'gc>, StrictValue<'gc>, Value<'gc>), + CallAndForce(BuiltinState<'gc>, StrictValue<'gc>, Value<'gc>), + Error(VmError), +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub(super) enum BuiltinState<'gc> { + FoldlStrict(FoldlStrict<'gc>), + // future: Filter, GenericClosure, Sort, All, Any, ConcatMap, ... +} + +impl<'gc> BuiltinState<'gc> { + pub(super) fn resume( + self, + val: StrictValue<'gc>, + ctx: &PrimOpCtx<'_, 'gc>, + ) -> BuiltinResult<'gc> { + match self { + BuiltinState::FoldlStrict(s) => s.resume(val, ctx), + } + } +} + +pub(super) struct PrimOpCtx<'a, 'gc> { + pub(super) vm: &'a VM<'gc>, + pub(super) mc: &'a Mutation<'gc>, + pub(super) strings: &'a DefaultStringInterner, +} + +macro_rules! force_inline_or_err { + ($ctx:expr, $val:expr) => {{ + let val = $val; + match $ctx.vm.force_inline(val) { + Ok(ForceResult::Ready(v)) => v, + Ok(_) => { + return BuiltinResult::Error(VM::err( + "value requires evaluation in non-stateful builtin context", + )); + } + Err(e) => return BuiltinResult::Error(e), + } + }}; +} + +macro_rules! force { + ($ctx:expr, $state:expr, $val:expr) => {{ + let val = $val; + match $ctx.vm.force_inline(val) { + Ok(ForceResult::Ready(v)) => v, + Ok(ForceResult::NeedEval { .. } | ForceResult::NeedApply(_)) => { + return BuiltinResult::Force($state, val); + } + Err(e) => return BuiltinResult::Error(e), + } + }}; +} + +macro_rules! call { + ($ctx:expr, $state:expr, $func:expr, $arg:expr) => {{ + let func = $func; + let arg = $arg; + return BuiltinResult::Call($state, func, arg); + }}; +} + +macro_rules! call_and_force { + ($ctx:expr, $state:expr, $func:expr, $arg:expr) => {{ + let func = $func; + let arg = $arg; + return BuiltinResult::CallAndForce($state, func, arg); + }}; +} + +pub(super) fn dispatch_strict_builtin<'gc>( + id: BuiltinId, + args: PrimOpStrictArgs<'gc>, + _arity: u8, + ctx: &PrimOpCtx<'_, 'gc>, +) -> BuiltinResult<'gc> { + match id { + BuiltinId::TypeOf => { + let val = args[0]; + let name = if val.as_inline::().is_some() || val.as_gc::().is_some() { + "int" + } else if val.as_float().is_some() { + "float" + } else if val.as_inline::().is_some() { + "bool" + } else if VM::get_string(val, ctx.strings).is_some() { + "string" + } else if val.is::() { + "null" + } else if val.as_gc::>().is_some() { + "set" + } else if val.as_gc::>().is_some() { + "list" + } else if val.as_gc::>().is_some() + || val.as_inline::().is_some() + || val.as_gc::>().is_some() + { + "lambda" + } else { + return BuiltinResult::Error(VM::err("typeOf: unknown type")); + }; + let sid = ctx.strings.get(name).expect("typeOf string not interned"); + BuiltinResult::Done(Value::new_inline(StringId(sid))) + } + + BuiltinId::IsNull => BuiltinResult::Done(Value::new_inline(args[0].is::())), + BuiltinId::IsAttrs => { + BuiltinResult::Done(Value::new_inline(args[0].as_gc::>().is_some())) + } + BuiltinId::IsBool => { + BuiltinResult::Done(Value::new_inline(args[0].as_inline::().is_some())) + } + BuiltinId::IsFloat => BuiltinResult::Done(Value::new_inline(args[0].as_float().is_some())), + BuiltinId::IsFunction => { + let v = args[0]; + let is_func = v.as_gc::>().is_some() + || v.as_inline::().is_some() + || v.as_gc::>().is_some(); + BuiltinResult::Done(Value::new_inline(is_func)) + } + BuiltinId::IsInt => { + let v = args[0]; + let is_int = v.as_inline::().is_some() || v.as_gc::().is_some(); + BuiltinResult::Done(Value::new_inline(is_int)) + } + BuiltinId::IsList => { + BuiltinResult::Done(Value::new_inline(args[0].as_gc::>().is_some())) + } + BuiltinId::IsString => { + let v = args[0]; + let is_str = v.as_inline::().is_some() || v.as_gc::().is_some(); + BuiltinResult::Done(Value::new_inline(is_str)) + } + BuiltinId::IsPath => BuiltinResult::Done(Value::new_inline(false)), + + BuiltinId::Length => { + let Some(list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.length: not a list")); + }; + BuiltinResult::Done(VM::make_int(list.inner.len() as i64, ctx.mc)) + } + BuiltinId::Head => { + let Some(list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.head: not a list")); + }; + if list.inner.is_empty() { + return BuiltinResult::Error(VM::err("builtins.head: empty list")); + } + BuiltinResult::Done(list.inner[0]) + } + BuiltinId::Tail => { + let Some(list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.tail: not a list")); + }; + if list.inner.is_empty() { + return BuiltinResult::Error(VM::err("builtins.tail: empty list")); + } + let tail = List { + inner: SmallVec::from_slice(&list.inner[1..]), + }; + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, tail))) + } + + BuiltinId::AttrNames => { + let Some(attrs) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.attrNames: not a set")); + }; + let items: SmallVec<[Value<'gc>; 4]> = + attrs.iter().map(|(k, _)| Value::new_inline(*k)).collect(); + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items }))) + } + BuiltinId::AttrValues => { + let Some(attrs) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.attrValues: not a set")); + }; + let items: SmallVec<[Value<'gc>; 4]> = attrs.iter().map(|(_, v)| *v).collect(); + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items }))) + } + + BuiltinId::Map => { + let f = args[0]; + let Some(list) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.map: second argument is not a list", + )); + }; + if list.inner.is_empty() { + return BuiltinResult::Done(Value::new_gc(Gc::new( + ctx.mc, + List { + inner: SmallVec::new(), + }, + ))); + } + let new_elems: SmallVec<[Value<'gc>; 4]> = list + .inner + .iter() + .map(|elem| { + let thunk: Gc<'gc, Thunk<'gc>> = Gc::new( + ctx.mc, + RefLock::new(ThunkState::Apply { + func: f.relax(), + arg: *elem, + }), + ); + Value::new_gc(thunk) + }) + .collect(); + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: new_elems }))) + } + + BuiltinId::GenList => { + let f = args[0]; + let len_val = args[1]; + let Some(len) = VM::as_num(len_val) else { + return BuiltinResult::Error(VM::err( + "builtins.genList: second argument is not a number", + )); + }; + let super::vm::NixNum::Int(len) = len else { + return BuiltinResult::Error(VM::err( + "builtins.genList: second argument is not an integer", + )); + }; + if len < 0 { + return BuiltinResult::Error(VM::err("builtins.genList: negative length")); + } + let items: SmallVec<[Value<'gc>; 4]> = (0..len) + .map(|i| { + let arg = VM::make_int(i, ctx.mc); + let thunk: Gc<'gc, Thunk<'gc>> = Gc::new( + ctx.mc, + RefLock::new(ThunkState::Apply { + func: f.relax(), + arg, + }), + ); + Value::new_gc(thunk) + }) + .collect(); + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items }))) + } + + BuiltinId::ElemAt => { + let Some(list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.elemAt: not a list")); + }; + let Some(idx) = VM::as_num(args[1]) else { + return BuiltinResult::Error(VM::err("builtins.elemAt: index is not a number")); + }; + let super::vm::NixNum::Int(idx) = idx else { + return BuiltinResult::Error(VM::err("builtins.elemAt: index is not an integer")); + }; + if idx < 0 || idx as usize >= list.inner.len() { + return BuiltinResult::Error(VM::err(format!( + "builtins.elemAt: index {} out of bounds for list of length {}", + idx, + list.inner.len() + ))); + } + BuiltinResult::Done(list.inner[idx as usize]) + } + + BuiltinId::GetAttr => { + let Some(name) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.getAttr: first argument is not a string", + )); + }; + let Some(sid) = ctx.strings.get(name) else { + return BuiltinResult::Error(VM::err(format!( + "builtins.getAttr: attribute '{}' not found", + name + ))); + }; + let Some(attrs) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.getAttr: second argument is not a set", + )); + }; + match attrs.lookup(StringId(sid)) { + Some(v) => BuiltinResult::Done(v), + None => BuiltinResult::Error(VM::err(format!("attribute '{}' missing", name))), + } + } + + BuiltinId::HasAttr => { + let Some(name) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.hasAttr: first argument is not a string", + )); + }; + let Some(attrs) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.hasAttr: second argument is not a set", + )); + }; + let has = ctx + .strings + .get(name) + .map(|sid| attrs.has(StringId(sid))) + .unwrap_or(false); + BuiltinResult::Done(Value::new_inline(has)) + } + + BuiltinId::RemoveAttrs => { + let Some(attrs) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.removeAttrs: first argument is not a set", + )); + }; + let Some(remove_list) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.removeAttrs: second argument is not a list", + )); + }; + let mut to_remove = Vec::new(); + for item in remove_list.inner.iter() { + let sv = force_inline_or_err!(ctx, *item); + if let Some(s) = VM::get_string(sv, ctx.strings) + && let Some(sid) = ctx.strings.get(s) + { + to_remove.push(StringId(sid)); + } + } + let entries: SmallVec<[(StringId, Value<'gc>); 4]> = attrs + .iter() + .filter(|(k, _)| !to_remove.contains(k)) + .cloned() + .collect(); + let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); + BuiltinResult::Done(Value::new_gc(new_attrs)) + } + + BuiltinId::IntersectAttrs => { + let Some(a) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.intersectAttrs: first argument is not a set", + )); + }; + let Some(b) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.intersectAttrs: second argument is not a set", + )); + }; + let entries: SmallVec<[(StringId, Value<'gc>); 4]> = + b.iter().filter(|(k, _)| a.has(*k)).cloned().collect(); + let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); + BuiltinResult::Done(Value::new_gc(new_attrs)) + } + + BuiltinId::ListToAttrs => { + let Some(list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.listToAttrs: not a list")); + }; + let name_sid = ctx.strings.get("name").expect("'name' not interned"); + let value_sid = ctx.strings.get("value").expect("'value' not interned"); + let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new(); + for item in list.inner.iter() { + let sv = force_inline_or_err!(ctx, *item); + let Some(attr_set) = sv.as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.listToAttrs: element is not a set", + )); + }; + let Some(name_val) = attr_set.lookup(StringId(name_sid)) else { + return BuiltinResult::Error(VM::err( + "builtins.listToAttrs: element missing 'name'", + )); + }; + let name_sv = force_inline_or_err!(ctx, name_val); + let Some(name_str) = VM::get_string(name_sv, ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.listToAttrs: 'name' is not a string", + )); + }; + let Some(value_val) = attr_set.lookup(StringId(value_sid)) else { + return BuiltinResult::Error(VM::err( + "builtins.listToAttrs: element missing 'value'", + )); + }; + let Some(key_sym) = ctx.strings.get(name_str) else { + return BuiltinResult::Error(VM::err( + "builtins.listToAttrs: name not interned", + )); + }; + entries.push((StringId(key_sym), value_val)); + } + entries.sort_by_key(|(k, _)| *k); + entries.dedup_by_key(|(k, _)| *k); + let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); + BuiltinResult::Done(Value::new_gc(new_attrs)) + } + + BuiltinId::ConcatLists => { + let Some(list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.concatLists: not a list")); + }; + let mut result = SmallVec::<[Value<'gc>; 4]>::new(); + for item in list.inner.iter() { + let sv = force_inline_or_err!(ctx, *item); + let Some(inner) = sv.as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.concatLists: element is not a list", + )); + }; + result.extend(inner.inner.iter().cloned()); + } + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: result }))) + } + + BuiltinId::LessThan => { + match ctx + .vm + .compare_values(args[0], args[1], ctx.strings, |o| o.is_lt()) + { + Ok(v) => BuiltinResult::Done(v), + Err(e) => BuiltinResult::Error(e), + } + } + + BuiltinId::Add => { + match ctx.vm.compute_binop( + super::vm::BinOpTag::Add, + args[0], + args[1], + ctx.mc, + ctx.strings, + ) { + Ok(v) => BuiltinResult::Done(v), + Err(e) => BuiltinResult::Error(e), + } + } + BuiltinId::Sub => { + match ctx + .vm + .numeric_binop(args[0], args[1], ctx.mc, i64::wrapping_sub, |a, b| a - b) + { + Ok(v) => BuiltinResult::Done(v), + Err(e) => BuiltinResult::Error(e), + } + } + BuiltinId::Mul => { + match ctx + .vm + .numeric_binop(args[0], args[1], ctx.mc, i64::wrapping_mul, |a, b| a * b) + { + Ok(v) => BuiltinResult::Done(v), + Err(e) => BuiltinResult::Error(e), + } + } + BuiltinId::Div => { + match ctx.vm.compute_binop( + super::vm::BinOpTag::Div, + args[0], + args[1], + ctx.mc, + ctx.strings, + ) { + Ok(v) => BuiltinResult::Done(v), + Err(e) => BuiltinResult::Error(e), + } + } + + BuiltinId::BitAnd => { + let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) = + (VM::as_num(args[0]), VM::as_num(args[1])) + else { + return BuiltinResult::Error(VM::err( + "builtins.bitAnd: arguments must be integers", + )); + }; + BuiltinResult::Done(VM::make_int(a & b, ctx.mc)) + } + BuiltinId::BitOr => { + let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) = + (VM::as_num(args[0]), VM::as_num(args[1])) + else { + return BuiltinResult::Error(VM::err("builtins.bitOr: arguments must be integers")); + }; + BuiltinResult::Done(VM::make_int(a | b, ctx.mc)) + } + BuiltinId::BitXor => { + let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) = + (VM::as_num(args[0]), VM::as_num(args[1])) + else { + return BuiltinResult::Error(VM::err( + "builtins.bitXor: arguments must be integers", + )); + }; + BuiltinResult::Done(VM::make_int(a ^ b, ctx.mc)) + } + + BuiltinId::Ceil => { + if let Some(f) = args[0].as_float() { + BuiltinResult::Done(VM::make_int(f.ceil() as i64, ctx.mc)) + } else if VM::as_num(args[0]).is_some() { + BuiltinResult::Done(args[0].relax()) + } else { + BuiltinResult::Error(VM::err("builtins.ceil: not a number")) + } + } + BuiltinId::Floor => { + if let Some(f) = args[0].as_float() { + BuiltinResult::Done(VM::make_int(f.floor() as i64, ctx.mc)) + } else if VM::as_num(args[0]).is_some() { + BuiltinResult::Done(args[0].relax()) + } else { + BuiltinResult::Error(VM::err("builtins.floor: not a number")) + } + } + + BuiltinId::StringLength => { + let Some(s) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err("builtins.stringLength: not a string")); + }; + BuiltinResult::Done(VM::make_int(s.len() as i64, ctx.mc)) + } + + BuiltinId::Substring => { + let Some(super::vm::NixNum::Int(start)) = VM::as_num(args[0]) else { + return BuiltinResult::Error(VM::err( + "builtins.substring: start is not an integer", + )); + }; + let Some(super::vm::NixNum::Int(len)) = VM::as_num(args[1]) else { + return BuiltinResult::Error(VM::err( + "builtins.substring: length is not an integer", + )); + }; + let Some(s) = VM::get_string(args[2], ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.substring: third argument is not a string", + )); + }; + let start = start.max(0) as usize; + if start >= s.len() { + let ns = Gc::new(ctx.mc, NixString::new("")); + return BuiltinResult::Done(Value::new_gc(ns)); + } + let end = if len < 0 { + s.len() + } else { + (start + len as usize).min(s.len()) + }; + let result = &s[start..end]; + let ns = Gc::new(ctx.mc, NixString::new(result)); + BuiltinResult::Done(Value::new_gc(ns)) + } + + BuiltinId::ToString => { + let v = args[0]; + if let Some(s) = VM::get_string(v, ctx.strings) { + let ns = Gc::new(ctx.mc, NixString::new(s)); + BuiltinResult::Done(Value::new_gc(ns)) + } else if let Some(b) = v.as_inline::() { + let s = if b { "1" } else { "" }; + let ns = Gc::new(ctx.mc, NixString::new(s)); + BuiltinResult::Done(Value::new_gc(ns)) + } else if v.is::() { + let ns = Gc::new(ctx.mc, NixString::new("")); + BuiltinResult::Done(Value::new_gc(ns)) + } else if let Some(n) = VM::as_num(v) { + let s = match n { + super::vm::NixNum::Int(i) => i.to_string(), + super::vm::NixNum::Float(f) => format!("{f}"), + }; + let ns = Gc::new(ctx.mc, NixString::new(s)); + BuiltinResult::Done(Value::new_gc(ns)) + } else { + BuiltinResult::Error(VM::err("builtins.toString: cannot coerce to string")) + } + } + + BuiltinId::Abort => { + let Some(msg) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err("builtins.abort: argument is not a string")); + }; + BuiltinResult::Error(VmError::Uncatchable(crate::error::Error::eval_error( + format!("evaluation aborted with the following error message: '{msg}'"), + ))) + } + + BuiltinId::Throw => { + let Some(msg) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err("builtins.throw: argument is not a string")); + }; + BuiltinResult::Error(VmError::Catchable(msg.to_owned())) + } + + BuiltinId::FunctionArgs => { + let v = args[0]; + if let Some(closure) = v.as_gc::>() { + if let Some(ref pattern) = closure.pattern { + let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new(); + for &name in &pattern.required { + entries.push((name, Value::new_inline(false))); + } + for &name in &pattern.optional { + entries.push((name, Value::new_inline(true))); + } + entries.sort_by_key(|(k, _)| *k); + let attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); + BuiltinResult::Done(Value::new_gc(attrs)) + } else { + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, AttrSet::default()))) + } + } else { + BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, AttrSet::default()))) + } + } + + BuiltinId::FoldlStrict => FoldlStrict::call(args, ctx), + + BuiltinId::Elem => { + let Some(list) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.elem: second argument is not a list", + )); + }; + let needle = args[0]; + for item in list.inner.iter() { + let sv = force_inline_or_err!(ctx, *item); + if ctx.vm.values_equal(needle, sv, ctx.strings) { + return BuiltinResult::Done(Value::new_inline(true)); + } + } + BuiltinResult::Done(Value::new_inline(false)) + } + + BuiltinId::ReplaceStrings => { + let Some(from_list) = args[0].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.replaceStrings: first argument is not a list", + )); + }; + let Some(to_list) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.replaceStrings: second argument is not a list", + )); + }; + let Some(s) = VM::get_string(args[2], ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.replaceStrings: third argument is not a string", + )); + }; + if from_list.inner.len() != to_list.inner.len() { + return BuiltinResult::Error(VM::err( + "builtins.replaceStrings: lists must have same length", + )); + } + + let mut from_strs = Vec::new(); + let mut to_strs = Vec::new(); + for (f, t) in from_list.inner.iter().zip(to_list.inner.iter()) { + let fv = force_inline_or_err!(ctx, *f); + let tv = force_inline_or_err!(ctx, *t); + let Some(fs) = VM::get_string(fv, ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.replaceStrings: from element is not a string", + )); + }; + let Some(ts) = VM::get_string(tv, ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.replaceStrings: to element is not a string", + )); + }; + from_strs.push(fs.to_owned()); + to_strs.push(ts.to_owned()); + } + + let s = s.to_owned(); + let mut result = String::new(); + let mut i = 0; + while i < s.len() { + let mut found = false; + for (j, from) in from_strs.iter().enumerate() { + if from.is_empty() { + result.push_str(&to_strs[j]); + result.push(s.as_bytes()[i] as char); + i += 1; + found = true; + break; + } + if s[i..].starts_with(from.as_str()) { + result.push_str(&to_strs[j]); + i += from.len(); + found = true; + break; + } + } + if !found { + result.push(s.as_bytes()[i] as char); + i += 1; + } + } + if from_strs.iter().any(|f| f.is_empty()) { + let j = from_strs + .iter() + .position(|f| f.is_empty()) + .expect("just checked"); + result.push_str(&to_strs[j]); + } + + let ns = Gc::new(ctx.mc, NixString::new(result)); + BuiltinResult::Done(Value::new_gc(ns)) + } + + BuiltinId::ConcatStringsSep => { + let Some(sep) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.concatStringsSep: first argument is not a string", + )); + }; + let sep = sep.to_owned(); + let Some(list) = args[1].as_gc::>() else { + return BuiltinResult::Error(VM::err( + "builtins.concatStringsSep: second argument is not a list", + )); + }; + let mut result = String::new(); + for (i, item) in list.inner.iter().enumerate() { + if i > 0 { + result.push_str(&sep); + } + let sv = force_inline_or_err!(ctx, *item); + let Some(s) = VM::get_string(sv, ctx.strings) else { + return BuiltinResult::Error(VM::err( + "builtins.concatStringsSep: element is not a string", + )); + }; + result.push_str(s); + } + let ns = Gc::new(ctx.mc, NixString::new(result)); + BuiltinResult::Done(Value::new_gc(ns)) + } + + BuiltinId::FromJSON => { + let Some(s) = VM::get_string(args[0], ctx.strings) else { + return BuiltinResult::Error(VM::err("builtins.fromJSON: not a string")); + }; + match serde_json::from_str::(s) { + Ok(json) => { + let v = json_to_nix(&json, ctx.mc, ctx.strings); + BuiltinResult::Done(v) + } + Err(e) => BuiltinResult::Error(VM::err(format!("builtins.fromJSON: {e}"))), + } + } + + BuiltinId::ToJSON => BuiltinResult::Error(VM::err("builtins.toJSON: not yet implemented")), + + BuiltinId::Null => BuiltinResult::Done(Value::new_inline(Null)), + + _ => BuiltinResult::Error(VM::err(format!("builtin {:?} not yet implemented", id))), + } +} + +pub(super) fn dispatch_lazy_builtin<'gc>( + id: BuiltinId, + args: &PrimOpArgs<'gc>, + _arity: u8, + ctx: &PrimOpCtx<'_, 'gc>, +) -> BuiltinResult<'gc> { + match id { + BuiltinId::Seq => { + let _ = force_inline_or_err!(ctx, args[0]); + BuiltinResult::Done(args[1]) + } + BuiltinId::DeepSeq => { + // TODO: deep force + let _ = force_inline_or_err!(ctx, args[0]); + BuiltinResult::Done(args[1]) + } + BuiltinId::Trace => { + let sv = force_inline_or_err!(ctx, args[0]); + if let Some(s) = VM::get_string(sv, ctx.strings) { + eprintln!("trace: {s}"); + } else { + eprintln!("trace: "); + } + BuiltinResult::Done(args[1]) + } + BuiltinId::Warn => { + let sv = force_inline_or_err!(ctx, args[0]); + if let Some(s) = VM::get_string(sv, ctx.strings) { + eprintln!("warning: {s}"); + } else { + eprintln!("warning: "); + } + BuiltinResult::Done(args[1]) + } + BuiltinId::TryEval => BuiltinResult::Error(VM::err( + "builtins.tryEval: requires catch frame support (TODO)", + )), + BuiltinId::AddErrorContext => BuiltinResult::Done(args[1]), + BuiltinId::Break => BuiltinResult::Done(args[0]), + _ => BuiltinResult::Error(VM::err(format!( + "lazy builtin {:?} not yet implemented", + id + ))), + } +} + +fn json_to_nix<'gc>( + json: &serde_json::Value, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, +) -> Value<'gc> { + match json { + serde_json::Value::Null => Value::new_inline(Null), + serde_json::Value::Bool(b) => Value::new_inline(*b), + serde_json::Value::Number(n) => { + if let Some(i) = n.as_i64() { + VM::make_int(i, mc) + } else if let Some(f) = n.as_f64() { + Value::new_float(f) + } else { + Value::new_inline(Null) + } + } + serde_json::Value::String(s) => { + let ns = Gc::new(mc, NixString::new(s.as_str())); + Value::new_gc(ns) + } + serde_json::Value::Array(arr) => { + let items: SmallVec<[Value<'gc>; 4]> = + arr.iter().map(|v| json_to_nix(v, mc, strings)).collect(); + Value::new_gc(Gc::new(mc, List { inner: items })) + } + serde_json::Value::Object(obj) => { + let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new(); + for (k, v) in obj { + if let Some(sym) = strings.get(k.as_str()) { + entries.push((StringId(sym), json_to_nix(v, mc, strings))); + } + } + entries.sort_by_key(|(k, _)| *k); + let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); + Value::new_gc(attrs) + } + } +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub(super) struct FoldlStrict<'gc> { + op: StrictValue<'gc>, + list: Gc<'gc, List<'gc>>, + acc: StrictValue<'gc>, + index: usize, + phase: FoldlPhase<'gc>, +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +enum FoldlPhase<'gc> { + CallOp, + CallPartial(StrictValue<'gc>), +} + +impl<'gc> FoldlStrict<'gc> { + fn call(args: PrimOpStrictArgs<'gc>, ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> { + let op = args[0]; + let nul = args[1]; + let Some(list) = args[2].as_gc::>() else { + return BuiltinResult::Error(VM::err("builtins.foldl': third argument is not a list")); + }; + if list.inner.is_empty() { + return BuiltinResult::Done(nul.relax()); + } + let state = FoldlStrict { + op, + list, + acc: nul, + index: 0, + phase: FoldlPhase::CallOp, + }; + state.step(ctx) + } + + fn step(self, _ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> { + let state = BuiltinState::FoldlStrict(FoldlStrict { + op: self.op, + list: self.list, + acc: self.acc, + index: self.index, + phase: FoldlPhase::CallOp, + }); + call!(ctx, state, self.op, self.acc.relax()) + } + + fn resume(mut self, val: StrictValue<'gc>, ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> { + match self.phase { + FoldlPhase::CallOp => { + let partial = val; + let elem = self.list.inner[self.index]; + self.phase = FoldlPhase::CallPartial(partial); + let state = BuiltinState::FoldlStrict(self); + call_and_force!(ctx, state, partial, elem) + } + FoldlPhase::CallPartial(_) => { + self.acc = val; + self.index += 1; + self.phase = FoldlPhase::CallOp; + if self.index >= self.list.inner.len() { + return BuiltinResult::Done(self.acc.relax()); + } + self.step(ctx) + } + } + } +} diff --git a/fix/src/runtime/stack.rs b/fix/src/runtime/stack.rs index fc43452..24bc9c2 100644 --- a/fix/src/runtime/stack.rs +++ b/fix/src/runtime/stack.rs @@ -3,14 +3,13 @@ use std::mem::MaybeUninit; use gc_arena::Collect; use smallvec::SmallVec; -// FIXME: Drop??? pub(super) struct Stack { inner: Box<[MaybeUninit; N]>, len: usize, } unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack { - const NEEDS_TRACE: bool = true; + const NEEDS_TRACE: bool = T::NEEDS_TRACE; fn trace>(&self, cc: &mut U) { for item in self.inner[..self.len].iter() { unsafe { @@ -34,6 +33,13 @@ impl Stack { } } + pub(super) unsafe fn push_unchecked(&mut self, val: T) { + unsafe { + self.inner.get_unchecked_mut(self.len).write(val); + } + self.len += 1; + } + pub(super) fn push(&mut self, val: T) -> Result<(), T> { if self.len == N { return Err(val); diff --git a/fix/src/runtime/value.rs b/fix/src/runtime/value.rs index cfa7e12..9e94cbe 100644 --- a/fix/src/runtime/value.rs +++ b/fix/src/runtime/value.rs @@ -3,16 +3,19 @@ use std::marker::PhantomData; use std::mem::size_of; use std::ops::Deref; -use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue}; use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace}; +use num_enum::TryFromPrimitive; use sealed::sealed; use smallvec::SmallVec; use string_interner::{Symbol, symbol::SymbolU32}; -use crate::ir::StringId; +use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue}; +use crate::{ir::StringId, runtime::builtins::BuiltinId}; #[sealed] -pub(crate) trait Storable { +/// # Safety +/// TAG must be unique among all implementors. +pub(crate) unsafe trait Storable { const TAG: (bool, u8); } pub(crate) trait InlineStorable: Storable + RawStore {} @@ -25,14 +28,14 @@ macro_rules! define_value_types { ) => { $( #[sealed] - impl Storable for $itype { + unsafe impl Storable for $itype { const TAG: (bool, u8) = $itag; } impl InlineStorable for $itype {} )* $( #[sealed] - impl Storable for $gtype { + unsafe impl Storable for $gtype { const TAG: (bool, u8) = $gtag; } impl GcStorable for $gtype {} @@ -116,22 +119,13 @@ define_value_types! { /// # Nix runtime value representation /// /// NaN-boxed value fitting in 8 bytes. +#[derive(Copy, Clone)] #[repr(transparent)] pub(crate) struct Value<'gc> { raw: RawBox, _marker: PhantomData>, } -impl Clone for Value<'_> { - #[inline] - fn clone(&self) -> Self { - Self { - raw: self.raw.clone(), - _marker: PhantomData, - } - } -} - impl Default for Value<'_> { #[inline(always)] fn default() -> Self { @@ -291,14 +285,23 @@ impl fmt::Debug for NixString { } } -#[derive(Collect, Debug)] +#[derive(Collect, Debug, Default)] #[collect(no_drop)] pub(crate) struct AttrSet<'gc> { - pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>, + entries: SmallVec<[(StringId, Value<'gc>); 4]>, +} + +impl<'gc> Deref for AttrSet<'gc> { + type Target = [(StringId, Value<'gc>)]; + fn deref(&self) -> &Self::Target { + &self.entries + } } impl<'gc> AttrSet<'gc> { - pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self { + pub(crate) unsafe fn from_sorted_unchecked( + entries: SmallVec<[(StringId, Value<'gc>); 4]>, + ) -> Self { debug_assert!(entries.is_sorted_by_key(|(key, _)| *key)); Self { entries } } @@ -307,13 +310,11 @@ impl<'gc> AttrSet<'gc> { self.entries .binary_search_by_key(&key, |(k, _)| *k) .ok() - .map(|i| self.entries[i].1.clone()) + .map(|i| self.entries[i].1) } pub(crate) fn has(&self, key: StringId) -> bool { - self.entries - .binary_search_by_key(&key, |(k, _)| *k) - .is_ok() + self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok() } pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> { @@ -328,15 +329,15 @@ impl<'gc> AttrSet<'gc> { while i < self.entries.len() && j < other.entries.len() { match self.entries[i].0.cmp(&other.entries[j].0) { Less => { - entries.push(self.entries[i].clone()); + entries.push(self.entries[i]); i += 1; } Greater => { - entries.push(other.entries[j].clone()); + entries.push(other.entries[j]); j += 1; } Equal => { - entries.push(other.entries[j].clone()); + entries.push(other.entries[j]); i += 1; j += 1; } @@ -351,7 +352,7 @@ impl<'gc> AttrSet<'gc> { } } -#[derive(Collect, Debug)] +#[derive(Collect, Debug, Default)] #[collect(no_drop)] pub(crate) struct List<'gc> { pub(crate) inner: SmallVec<[Value<'gc>; 4]>, @@ -366,6 +367,10 @@ pub(crate) enum ThunkState<'gc> { ip: u32, env: Gc<'gc, RefLock>>, }, + Apply { + func: Value<'gc>, + arg: Value<'gc>, + }, Blackhole, Evaluated(Value<'gc>), } @@ -420,17 +425,20 @@ pub(crate) struct PatternInfo { #[derive(Clone, Copy, Debug, Collect)] #[collect(require_static)] pub(crate) struct PrimOp { - pub(crate) id: u8, + pub(crate) id: BuiltinId, pub(crate) arity: u8, } impl RawStore for PrimOp { fn to_val(self, value: &mut RawValue) { - value.set_data([0, 0, 0, 0, self.id, self.arity]); + value.set_data([0, 0, 0, 0, self.id as u8, self.arity]); } fn from_val(value: &RawValue) -> Self { let [.., id, arity] = *value.data(); - Self { id, arity } + Self { + id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"), + arity, + } } } @@ -441,6 +449,7 @@ pub(crate) struct PrimOpApp<'gc> { pub(crate) args: SmallVec<[Value<'gc>; 2]>, } +#[derive(Copy, Clone, Default)] #[repr(transparent)] pub(crate) struct StrictValue<'gc>(Value<'gc>); @@ -455,7 +464,7 @@ impl<'gc> StrictValue<'gc> { } #[inline] - pub(crate) fn into_relaxed(self) -> Value<'gc> { + pub(crate) fn relax(self) -> Value<'gc> { self.0 } } @@ -468,13 +477,6 @@ impl<'gc> Deref for StrictValue<'gc> { } } -impl Clone for StrictValue<'_> { - #[inline] - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - impl fmt::Debug for StrictValue<'_> { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fmt::Debug::fmt(&self.0, f) diff --git a/fix/src/runtime/vm.rs b/fix/src/runtime/vm.rs index a884aa4..316f503 100644 --- a/fix/src/runtime/vm.rs +++ b/fix/src/runtime/vm.rs @@ -6,16 +6,18 @@ 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; -type VmResult = std::result::Result; +pub(super) type VmResult = std::result::Result; -enum VmError { +pub(super) enum VmError { Catchable(String), - Uncatchable(Box), + Uncatchable(Box), } impl From> for VmError { @@ -31,7 +33,7 @@ pub(super) struct VM<'gc> { frames: Stack<8192, CallFrame<'gc>>, with_scope: Option>>, error_contexts: Stack<8192, ErrorFrame>, - globals: Gc<'gc, GlobalState<'gc>>, + globals: GlobalState<'gc>, import_cache: HashMap>, pc: usize, current_env: Option>>>, @@ -42,10 +44,13 @@ pub(super) struct VM<'gc> { #[collect(no_drop)] struct GlobalState<'gc> { builtins: Value<'gc>, + builtin_lookup: HashMap, + empty_list: Value<'gc>, + empty_attrs: Value<'gc>, } #[derive(Collect)] -#[collect(no_drop)] +#[collect(require_static)] struct ErrorFrame { span_id: u32, message: Option, @@ -53,7 +58,7 @@ struct ErrorFrame { #[derive(Collect, Debug)] #[collect(no_drop)] -struct WithScope<'gc> { +pub(super) struct WithScope<'gc> { env: Value<'gc>, prev: Option>>, } @@ -75,19 +80,23 @@ enum Continuation<'gc> { thunk: Gc<'gc, Thunk<'gc>>, after: AfterForce<'gc>, }, + BuiltinReturn(BuiltinState<'gc>), + BuiltinCallAndForce(BuiltinState<'gc>), } #[derive(Collect, Debug)] #[collect(no_drop)] -enum AfterForce<'gc> { +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, @@ -122,15 +131,29 @@ enum AfterForce<'gc> { }, 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, Collect)] -#[collect(require_static)] -enum BinOpTag { +#[derive(Clone, Copy, Debug)] +pub(super) enum BinOpTag { Add, Sub, Mul, @@ -145,13 +168,14 @@ enum BinOpTag { Update, } -enum ForceResult<'gc> { +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 { @@ -161,33 +185,34 @@ pub(crate) enum Action { IoRequest(()), } -enum NixNum { +pub(super) enum NixNum { Int(i64), Float(f64), } macro_rules! try_vm { - ($expr:expr) => { + ($self:ident, $expr:expr) => { match $expr { Ok(v) => v, - Err(e) => return VM::vm_err_to_action(e), + Err(e) => return VM::handle_vm_error($self, e), } }; } impl<'gc> VM<'gc> { - pub(super) fn new(mc: &Mutation<'gc>) -> Self { + 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: Gc::new( - mc, - GlobalState { - builtins: Value::new_inline(Null), - }, - ), + 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, @@ -195,52 +220,127 @@ impl<'gc> VM<'gc> { } } + 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 { - let v = bc[self.pc]; - self.pc += 1; - v + u8::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_u16(&mut self, bc: &[u8]) -> u16 { - let v = u16::from_le_bytes(bc[self.pc..self.pc + 2].try_into().unwrap()); - self.pc += 2; - v + u16::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_u32(&mut self, bc: &[u8]) -> u32 { - let v = u32::from_le_bytes(bc[self.pc..self.pc + 4].try_into().unwrap()); - self.pc += 4; - v + u32::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_i32(&mut self, bc: &[u8]) -> i32 { - let v = i32::from_le_bytes(bc[self.pc..self.pc + 4].try_into().unwrap()); - self.pc += 4; - v + i32::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_i64(&mut self, bc: &[u8]) -> i64 { - let v = i64::from_le_bytes(bc[self.pc..self.pc + 8].try_into().unwrap()); - self.pc += 8; - v + i64::from_le_bytes(self.read_array(bc)) } #[inline(always)] fn read_f64(&mut self, bc: &[u8]) -> f64 { - let v = f64::from_le_bytes(bc[self.pc..self.pc + 8].try_into().unwrap()); - self.pc += 8; - v + 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(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap()) + StringId(unsafe { + string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap_unchecked() + }) } #[inline(always)] @@ -248,36 +348,36 @@ impl<'gc> VM<'gc> { self.current_env.expect("no current env") } - fn force_inline(&self, val: Value<'gc>) -> VmResult> { + pub(super) fn force_inline(&self, val: Value<'gc>) -> VmResult> { let mut current = val; loop { - let Some(thunk_gc) = current.as_gc::>() else { - return Ok(ForceResult::Ready(unsafe { StrictValue::try_from_forced(current).unwrap_unchecked() })); + let Some(thunk) = current.as_gc::>() else { + return Ok(ForceResult::Ready(unsafe { + StrictValue::try_from_forced(current).unwrap_unchecked() + })); }; - let thunk_ref = thunk_gc.borrow(); + let thunk_ref = thunk.borrow(); match &*thunk_ref { ThunkState::Evaluated(v) => { - current = v.clone(); + current = *v; drop(thunk_ref); } - ThunkState::Pending { ip, env } => { - return Ok(ForceResult::NeedEval { - ip: *ip, - env: *env, - thunk: thunk_gc, - }); + &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".into(), - None, + "infinite recursion encountered", ))); } } } } - fn push_force_frame( + pub(super) fn push_force_frame( &mut self, thunk: Gc<'gc, Thunk<'gc>>, after: AfterForce<'gc>, @@ -298,7 +398,75 @@ impl<'gc> VM<'gc> { self.current_env = Some(env); } - fn make_int(val: i64, mc: &Mutation<'gc>) -> Value<'gc> { + 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 { @@ -306,7 +474,7 @@ impl<'gc> VM<'gc> { } } - fn as_num(val: StrictValue<'gc>) -> Option { + 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::() { @@ -316,30 +484,26 @@ impl<'gc> VM<'gc> { } } - fn get_string_id(val: &Value<'gc>) -> Option { + pub(super) fn get_string_id(val: &Value<'gc>) -> Option { val.as_inline::() } - fn get_string(val: StrictValue<'gc>, strings: &DefaultStringInterner) -> Option { + 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)?.to_owned()) + Some(strings.resolve(sid.0)?) } else { - val.as_gc::().map(|ns| ns.as_str().to_owned()) + val.as_gc::().map(|ns| ns.as_ref().as_str()) } } - fn err(msg: impl Into) -> VmError { - VmError::Uncatchable(Error::eval_error(msg.into(), None)) + pub(super) fn err(msg: impl Into) -> VmError { + VmError::Uncatchable(Error::eval_error(msg.into())) } - fn vm_err_to_action(e: VmError) -> Action { - match e { - VmError::Uncatchable(e) => Action::Done(Err(e)), - VmError::Catchable(msg) => Action::Done(Err(Error::catchable(msg))), - } - } - - fn handle_return( + pub(super) fn handle_return( &mut self, ret_val: Value<'gc>, mc: &Mutation<'gc>, @@ -348,11 +512,11 @@ impl<'gc> VM<'gc> { 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(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame(thunk, AfterForce::TopLevelForce, ip, env, mc); + Ok(other) => { + try_vm!(self, self.setup_force(other, AfterForce::TopLevelForce, mc, strings)); Action::Continue } - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), }; }; @@ -361,30 +525,68 @@ impl<'gc> VM<'gc> { match frame.continuation { Continuation::Return => { - self.stack.push(ret_val).expect("stack overflow"); + self.push_stack(ret_val); Action::Continue } Continuation::ForceThunk { thunk, after } => { - *thunk.borrow_mut(mc) = ThunkState::Evaluated(ret_val.clone()); + *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(ForceResult::NeedEval { - ip, - env, - thunk: inner, - }) => { - self.push_force_frame(inner, after, ip, env, mc); + Ok(other) => { + try_vm!(self, self.setup_force(other, after, mc, strings)); Action::Continue } - Err(e) => Self::vm_err_to_action(e), + 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), } } } } - fn resume_after_force( + pub(super) fn resume_after_force( &mut self, val: StrictValue<'gc>, after: AfterForce<'gc>, @@ -393,81 +595,80 @@ impl<'gc> VM<'gc> { ) -> Action { match after { AfterForce::Identity => { - self.stack.push(val.into_relaxed()).expect("stack overflow"); + self.push_stack(val.relax()); Action::Continue } AfterForce::ForceBool => { if val.as_inline::().is_none() { - return Self::vm_err_to_action(Self::err("value is not a boolean")); + return Action::Done(Err(Error::eval_error("value is not a boolean"))); } - self.stack.push(val.into_relaxed()).expect("stack overflow"); + 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.stack.push(result).expect("stack overflow"); + self.push_stack(result); Action::Continue } - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), } } - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::BinOpRhs { - lhs_forced: val, - op, - }, - ip, - env, - mc, + Ok(other) => { + try_vm!( + self, + self.setup_force( + other, + AfterForce::BinOpRhs { + lhs_forced: val, + op, + }, + mc, + strings, + ) ); Action::Continue } - Err(e) => Self::vm_err_to_action(e), + 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.stack.push(result).expect("stack overflow"); + self.push_stack(result); Action::Continue } - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), } } AfterForce::UnNeg => match Self::as_num(val) { Some(NixNum::Int(i)) => { - self.stack - .push(Self::make_int(-i, mc)) - .expect("stack overflow"); + self.push_stack(Self::make_int(-i, mc)); Action::Continue } Some(NixNum::Float(f)) => { - self.stack - .push(Value::new_float(-f)) - .expect("stack overflow"); + self.push_stack(Value::new_float(-f)); Action::Continue } - None => Self::vm_err_to_action(Self::err("cannot negate non-number")), + None => Action::Done(Err(Error::eval_error("cannot negate non-number"))), }, AfterForce::UnNot => match val.as_inline::() { Some(b) => { - self.stack - .push(Value::new_inline(!b)) - .expect("stack overflow"); + self.push_stack(Value::new_inline(!b)); Action::Continue } - None => Self::vm_err_to_action(Self::err("value is not a boolean")), + None => Action::Done(Err(Error::eval_error("value is not a boolean"))), }, - AfterForce::Call { arg, span } => match self.do_call(val, arg, span, mc) { + AfterForce::Call { arg, span } => match self.do_call(val, arg, span, mc, strings) { Ok(()) => Action::Continue, - Err(e) => Self::vm_err_to_action(e), + 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().unwrap(); + 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, @@ -478,10 +679,12 @@ impl<'gc> VM<'gc> { mc, ) { Ok(()) => Action::Continue, - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), } } else { - Self::vm_err_to_action(Self::err("internal: pattern call on non-closure")) + Action::Done(Err(Error::eval_error( + "internal: pattern call on non-closure", + ))) } } AfterForce::Select { @@ -491,12 +694,12 @@ impl<'gc> VM<'gc> { default, } => match self.do_select_step(val, keys, remaining, span, default, mc, strings) { Ok(()) => Action::Continue, - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), }, AfterForce::HasAttr { keys, remaining } => { - match self.do_has_attr_step(val, keys, remaining, mc) { + match self.do_has_attr_step(val, keys, remaining, mc, strings) { Ok(()) => Action::Continue, - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), } } AfterForce::Assert { @@ -505,18 +708,17 @@ impl<'gc> VM<'gc> { span_id: _, } => match val.as_inline::() { Some(true) => { - self.stack.push(expr).expect("stack overflow"); + 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(""); - Self::vm_err_to_action(VmError::Uncatchable(Error::eval_error( - format!("assertion '{msg}' failed"), - None, - ))) + Action::Done(Err(Error::eval_error(format!("assertion '{msg}' failed")))) } - None => Self::vm_err_to_action(Self::err("assertion condition must be a boolean")), + None => Action::Done(Err(Error::eval_error( + "assertion condition must be a boolean", + ))), }, AfterForce::ConcatStrings { mut forced, @@ -526,14 +728,14 @@ impl<'gc> VM<'gc> { forced.push(val); match self.concat_strings_continue(forced, remaining, force_string, mc, strings) { Ok(()) => Action::Continue, - Err(e) => Self::vm_err_to_action(e), + Err(e) => self.handle_vm_error(e), } } AfterForce::PushWith => { let scope = Gc::new( mc, WithScope { - env: val.into_relaxed(), + env: val.relax(), prev: self.with_scope, }, ); @@ -543,19 +745,208 @@ impl<'gc> VM<'gc> { AfterForce::WithLookup { name, next } => { match self.do_with_lookup_step(val, name, next, mc, strings) { Ok(()) => Action::Continue, - Err(e) => Self::vm_err_to_action(e), + 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 do_call( + 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; @@ -563,7 +954,7 @@ impl<'gc> VM<'gc> { let closure_env = closure_gc.env; if let Some(ref pattern) = closure_gc.pattern { - match self.force_inline(arg.clone())? { + match self.force_inline(arg)? { ForceResult::Ready(forced_arg) => { self.setup_pattern_call( ip, @@ -575,21 +966,13 @@ impl<'gc> VM<'gc> { mc, )?; } - ForceResult::NeedEval { - ip: thunk_ip, - env: thunk_env, - thunk, - } => { - self.push_force_frame( - thunk, - AfterForce::PatternCallArgForce { - func: func.clone(), - span, - }, - thunk_ip, - thunk_env, + other => { + self.setup_force( + other, + AfterForce::PatternCallArgForce { func, span }, mc, - ); + strings, + )?; } } } else { @@ -607,8 +990,12 @@ impl<'gc> VM<'gc> { } Ok(()) } else if let Some(po) = func.as_inline::() { - if po.arity == 1 { - Err(Self::err("builtins not yet implemented")) + 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, @@ -617,14 +1004,20 @@ impl<'gc> VM<'gc> { args: SmallVec::from_elem(arg, 1), }, ); - self.stack.push(Value::new_gc(app)).expect("stack overflow"); + 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 { - Err(Self::err("builtins not yet implemented")) + 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, @@ -633,7 +1026,7 @@ impl<'gc> VM<'gc> { args, }, ); - self.stack.push(Value::new_gc(app)).expect("stack overflow"); + self.push_stack(Value::new_gc(app)); Ok(()) } } else { @@ -643,7 +1036,7 @@ impl<'gc> VM<'gc> { } } - fn setup_pattern_call( + pub(super) fn setup_pattern_call( &mut self, ip: u32, n_locals: u32, @@ -666,7 +1059,7 @@ impl<'gc> VM<'gc> { } if !pattern.ellipsis { - for (key, _) in attrs.entries.iter() { + 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")); @@ -676,11 +1069,7 @@ impl<'gc> VM<'gc> { let new_env = Gc::new( mc, - RefLock::new(Env::with_arg( - arg.clone().into_relaxed(), - n_locals, - closure_env, - )), + RefLock::new(Env::with_arg(arg.relax(), n_locals, closure_env)), ); self.frames @@ -696,7 +1085,7 @@ impl<'gc> VM<'gc> { Ok(()) } - fn compute_binop( + pub(super) fn compute_binop( &self, op: BinOpTag, lhs: StrictValue<'gc>, @@ -707,8 +1096,8 @@ impl<'gc> VM<'gc> { match op { BinOpTag::Add => { if let (Some(ls), Some(rs)) = ( - Self::get_string(lhs.clone(), strings), - Self::get_string(rhs.clone(), strings), + 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)); @@ -719,7 +1108,7 @@ impl<'gc> VM<'gc> { 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(f))) if f == 0.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)) } @@ -762,7 +1151,7 @@ impl<'gc> VM<'gc> { } } - fn numeric_binop( + pub(super) fn numeric_binop( &self, lhs: StrictValue<'gc>, rhs: StrictValue<'gc>, @@ -785,13 +1174,13 @@ impl<'gc> VM<'gc> { } } - fn values_equal( + 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.clone()), Self::as_num(rhs.clone())) { + 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, @@ -806,8 +1195,8 @@ impl<'gc> VM<'gc> { return true; } if let (Some(a), Some(b)) = ( - Self::get_string(lhs.clone(), strings), - Self::get_string(rhs.clone(), strings), + Self::get_string(lhs, strings), + Self::get_string(rhs, strings), ) { return a == b; } @@ -815,33 +1204,28 @@ impl<'gc> VM<'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.clone()), self.force_inline(y.clone())) - else { - return false; - }; - self.values_equal(x, y, strings) - }); + 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.entries.len() != b.entries.len() { + if a.len() != b.len() { return false; } return a - .entries .iter() - .zip(b.entries.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.clone()), self.force_inline(v2.clone())) + (self.force_inline(*v1), self.force_inline(*v2)) else { return false; }; @@ -851,14 +1235,14 @@ impl<'gc> VM<'gc> { false } - fn compare_values( + 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.clone()), Self::as_num(rhs.clone())) { + 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)) => { @@ -877,12 +1261,12 @@ impl<'gc> VM<'gc> { Self::get_string(lhs, strings), Self::get_string(rhs, strings), ) { - return Ok(Value::new_inline(pred(a.cmp(&b)))); + return Ok(Value::new_inline(pred(a.cmp(b)))); } Err(Self::err("cannot compare these types")) } - fn do_select_step( + pub(super) fn do_select_step( &mut self, set_val: StrictValue<'gc>, keys: SmallVec<[Value<'gc>; 4]>, @@ -894,7 +1278,7 @@ impl<'gc> VM<'gc> { ) -> VmResult<()> { let Some(attrs) = set_val.as_gc::>() else { if let Some(def) = default { - self.stack.push(def).expect("stack overflow"); + self.push_stack(def); return Ok(()); } return Err(Self::err("cannot select from non-set")); @@ -909,47 +1293,46 @@ impl<'gc> VM<'gc> { let found = attrs.lookup(key_sid); if remaining <= 1 { - match found { - Some(v) => { - self.stack.push(v).expect("stack overflow"); + match (found, default) { + (Some(v), _) => { + self.push_stack(v); Ok(()) } - None if default.is_some() => { - self.stack.push(default.unwrap()).expect("stack overflow"); + (None, Some(default)) => { + self.push_stack(default); Ok(()) } - None => { + (None, None) => { let name = strings.resolve(key_sid.0).unwrap_or(""); Err(Self::err(format!("attribute '{name}' missing"))) } } } else { - match found { - Some(v) => match self.force_inline(v)? { + 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) } - ForceResult::NeedEval { ip, env, thunk } => { - self.push_force_frame( - thunk, + other => { + self.setup_force( + other, AfterForce::Select { keys, remaining: remaining - 1, span, default, }, - ip, - env, mc, - ); + strings, + )?; Ok(()) } }, - None if default.is_some() => { - self.stack.push(default.unwrap()).expect("stack overflow"); + (None, Some(default)) => { + self.push_stack(default); Ok(()) } - None => { + (None, None) => { let name = strings.resolve(key_sid.0).unwrap_or(""); Err(Self::err(format!("attribute '{name}' missing"))) } @@ -957,57 +1340,49 @@ impl<'gc> VM<'gc> { } } - fn do_has_attr_step( + 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.stack - .push(Value::new_inline(false)) - .expect("stack overflow"); + 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.stack - .push(Value::new_inline(false)) - .expect("stack overflow"); + self.push_stack(Value::new_inline(false)); return Ok(()); }; if remaining <= 1 { - self.stack - .push(Value::new_inline(attrs.has(key_sid))) - .expect("stack overflow"); + 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) + self.do_has_attr_step(forced, keys, remaining - 1, mc, strings) } - ForceResult::NeedEval { ip, env, thunk } => { - self.push_force_frame( - thunk, + other => { + self.setup_force( + other, AfterForce::HasAttr { keys, remaining: remaining - 1, }, - ip, - env, mc, - ); + strings, + )?; Ok(()) } }, None => { - self.stack - .push(Value::new_inline(false)) - .expect("stack overflow"); + self.push_stack(Value::new_inline(false)); Ok(()) } } @@ -1025,18 +1400,17 @@ impl<'gc> VM<'gc> { while let Some(part) = remaining.pop() { match self.force_inline(part)? { ForceResult::Ready(v) => forced.push(v), - ForceResult::NeedEval { ip, env, thunk } => { - self.push_force_frame( - thunk, + other => { + self.setup_force( + other, AfterForce::ConcatStrings { forced, remaining, force_string, }, - ip, - env, - mc, - ); + mc, + strings, + )?; return Ok(()); } } @@ -1044,9 +1418,9 @@ impl<'gc> VM<'gc> { let mut result = String::new(); for part in &forced { - if let Some(s) = Self::get_string(part.clone(), strings) { - result.push_str(&s); - } else if let Some(n) = Self::as_num(part.clone()) { + 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}")), @@ -1064,7 +1438,7 @@ impl<'gc> VM<'gc> { } let ns = Gc::new(mc, NixString::new(result)); - self.stack.push(Value::new_gc(ns)).expect("stack overflow"); + self.push_stack(Value::new_gc(ns)); Ok(()) } @@ -1076,32 +1450,31 @@ impl<'gc> VM<'gc> { mc: &Mutation<'gc>, strings: &DefaultStringInterner, ) -> VmResult<()> { - if let Some(attrs) = scope_val.as_gc::>() { - if let Some(v) = attrs.lookup(name) { - self.stack.push(v).expect("stack overflow"); - return Ok(()); - } + 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.clone(); + 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) } - ForceResult::NeedEval { ip, env, thunk } => { - self.push_force_frame( - thunk, + other => { + self.setup_force( + other, AfterForce::WithLookup { name, next: next_prev, }, - ip, - env, mc, - ); + strings, + )?; Ok(()) } } @@ -1135,9 +1508,9 @@ impl<'gc> VM<'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.entries.iter() { + for (key, val) in attrs.iter() { let key_str = strings.resolve(key.0).unwrap_or("").to_owned(); - let converted = self.convert_value(&val, strings); + 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)) @@ -1145,7 +1518,7 @@ impl<'gc> VM<'gc> { let items: Vec<_> = list .inner .iter() - .map(|v| self.convert_value(&v, strings)) + .map(|v| self.convert_value(v, strings)) .collect(); crate::value::Value::List(crate::value::List::new(items)) } else if val.is::>() { @@ -1172,65 +1545,43 @@ impl<'gc> VM<'gc> { #[cfg(debug_assertions)] let opcode_byte = bc[self.pc]; #[cfg(not(debug_assertions))] - let opcode_byte = unsafe { - *bc.get_unchecked(self.pc) - }; + 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}"), - None, - ))); + 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() - }; + let op = unsafe { Op::try_from_primitive(opcode_byte).unwrap_unchecked() }; match op { PushSmi => { let val = self.read_i32(bc); - self.stack - .push(Value::new_inline(val)) - .expect("stack overflow"); + self.push_stack(Value::new_inline(val)); } PushBigInt => { let val = self.read_i64(bc); - self.stack - .push(Value::new_gc(Gc::new(mc, val))) - .expect("stack overflow"); + self.push_stack(Value::new_gc(Gc::new(mc, val))); } PushFloat => { let val = self.read_f64(bc); - self.stack - .push(Value::new_float(val)) - .expect("stack overflow"); + self.push_stack(Value::new_float(val)); } PushString => { let sid = self.read_string_id(bc); - self.stack - .push(Value::new_inline(sid)) - .expect("stack overflow"); + self.push_stack(Value::new_inline(sid)); } - PushNull => self - .stack - .push(Value::new_inline(Null)) - .expect("stack overflow"), - PushTrue => self - .stack - .push(Value::new_inline(true)) - .expect("stack overflow"), - PushFalse => self - .stack - .push(Value::new_inline(false)) - .expect("stack overflow"), + 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].clone(); - self.stack.push(val).expect("stack overflow"); + let val = self.env().borrow().locals[idx]; + self.push_stack(val); } LoadOuter => { let layer = self.read_u8(bc); @@ -1240,8 +1591,8 @@ impl<'gc> VM<'gc> { let prev = env.borrow().prev.expect("LoadOuter: env chain too short"); env = prev; } - let val = env.borrow().locals[idx].clone(); - self.stack.push(val).expect("stack overflow"); + let val = env.borrow().locals[idx]; + self.push_stack(val); } StoreLocal => { let idx = self.read_u32(bc) as usize; @@ -1266,9 +1617,7 @@ impl<'gc> VM<'gc> { env: self.env(), }), ); - self.stack - .push(Value::new_gc(thunk)) - .expect("stack overflow"); + self.push_stack(Value::new_gc(thunk)); } MakeClosure => { let entry_point = self.read_u32(bc); @@ -1282,9 +1631,7 @@ impl<'gc> VM<'gc> { pattern: None, }, ); - self.stack - .push(Value::new_gc(closure)) - .expect("stack overflow"); + self.push_stack(Value::new_gc(closure)); } MakePatternClosure => { let entry_point = self.read_u32(bc); @@ -1327,9 +1674,7 @@ impl<'gc> VM<'gc> { pattern: Some(pattern), }, ); - self.stack - .push(Value::new_gc(closure)) - .expect("stack overflow"); + self.push_stack(Value::new_gc(closure)); } Call => { @@ -1337,109 +1682,108 @@ impl<'gc> VM<'gc> { 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.do_call(f, arg, Some(span_id), mc)), - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::Call { - arg, - span: Some(span_id), - }, - ip, - env, + 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::vm_err_to_action(e), + 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.do_call(f, arg, None, mc)), - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::Call { arg, span: None }, - ip, - env, - mc, + 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::vm_err_to_action(e), + Err(e) => return self.handle_vm_error(e), } } MakeAttrs => { let count = self.read_u32(bc) as usize; let total = 3 * count; - let items: SmallVec<[Value<'gc>; 16]> = self.stack.pop_n::<16>(total); + 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].clone(); + 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, AttrSet::from_sorted(entries)); - self.stack - .push(Value::new_gc(attrs)) - .expect("stack overflow"); + 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: SmallVec<[Value<'gc>; 16]> = self.stack.pop_n::<16>(total); + 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].clone())); + 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].clone(); + 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".into(), - None, + "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, AttrSet::from_sorted(entries)); - self.stack - .push(Value::new_gc(attrs)) - .expect("stack overflow"); + let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) }); + self.push_stack(Value::new_gc(attrs)); } MakeEmptyAttrs => { - let attrs = Gc::new(mc, AttrSet::from_sorted(SmallVec::new())); - self.stack - .push(Value::new_gc(attrs)) - .expect("stack overflow"); + 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: SmallVec<[Value<'gc>; 4]> = self.stack.pop_n::<4>(n); + let mut keys = self.stack.pop_n::<4>(n); let expr = self.stack.pop().expect("stack underflow"); keys.reverse(); @@ -1447,76 +1791,84 @@ impl<'gc> VM<'gc> { 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(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::Select { - keys, - remaining, - span: span_id, - default: None, - }, - ip, - env, + Ok(other) => { + try_vm!( + self, + self.setup_force( + other, + AfterForce::Select { + keys, + remaining, + span: span_id, + default: None, + }, mc, + strings, + ) ); } - Err(e) => return Self::vm_err_to_action(e), + 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: SmallVec<[Value<'gc>; 4]> = self.stack.pop_n::<4>(n); + 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.do_select_step( - forced, - keys, - remaining, - span_id, - Some(default), - mc, - strings, - )); - } - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::Select { + try_vm!( + self, + self.do_select_step( + forced, keys, remaining, - span: span_id, - default: Some(default), - }, - ip, - env, - mc, + span_id, + Some(default), + mc, + strings, + ) ); } - Err(e) => return Self::vm_err_to_action(e), + 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: SmallVec<[Value<'gc>; 4]> = self.stack.pop_n::<4>(n); + 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.do_has_attr_step(forced, keys, remaining, mc)); + try_vm!(self, self.do_has_attr_step(forced, keys, remaining, mc, strings)); } Ok(ForceResult::NeedEval { ip, env, thunk }) => { self.push_force_frame( @@ -1527,18 +1879,26 @@ impl<'gc> VM<'gc> { mc, ); } - Err(e) => return Self::vm_err_to_action(e), + 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 mut items: SmallVec<[Value<'gc>; 4]> = self.stack.pop_n::<4>(count); - items.reverse(); + let items = self.stack.pop_n::<4>(count); let list = Gc::new(mc, List { inner: items }); - self.stack - .push(Value::new_gc(list)) - .expect("stack overflow"); + self.push_stack(Value::new_gc(list)); } OpAdd | OpSub | OpMul | OpDiv | OpEq | OpNeq | OpLt | OpGt | OpLeq | OpGeq @@ -1565,34 +1925,33 @@ impl<'gc> VM<'gc> { 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.stack.push(r).expect("stack overflow"), - Err(e) => return Self::vm_err_to_action(e), + Ok(r) => self.push_stack(r), + Err(e) => return self.handle_vm_error(e), } } - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::BinOpRhs { - lhs_forced: lhs_f, - op: tag, - }, - ip, - env, - mc, + Ok(other) => { + try_vm!( + self, + self.setup_force( + other, + AfterForce::BinOpRhs { + lhs_forced: lhs_f, + op: tag, + }, + mc, + strings, + ) ); } - Err(e) => return Self::vm_err_to_action(e), + Err(e) => return self.handle_vm_error(e), }, - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::BinOpLhs { rhs, op: tag }, - ip, - env, - mc, + Ok(other) => { + try_vm!( + self, + self.setup_force(other, AfterForce::BinOpLhs { rhs, op: tag }, mc, strings) ); } - Err(e) => return Self::vm_err_to_action(e), + Err(e) => return self.handle_vm_error(e), } } @@ -1601,21 +1960,19 @@ impl<'gc> VM<'gc> { match self.force_inline(val) { Ok(ForceResult::Ready(f)) => match Self::as_num(f) { Some(NixNum::Int(i)) => self - .stack - .push(Self::make_int(-i, mc)) - .expect("stack overflow"), + .push_stack(Self::make_int(-i, mc)), Some(NixNum::Float(fl)) => self - .stack - .push(Value::new_float(-fl)) - .expect("stack overflow"), + .push_stack(Value::new_float(-fl)), None => { - return Self::vm_err_to_action(Self::err("cannot negate non-number")); + return Action::Done(Err(Error::eval_error( + "cannot negate non-number", + ))); } }, - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame(thunk, AfterForce::UnNeg, ip, env, mc); + Ok(other) => { + try_vm!(self, self.setup_force(other, AfterForce::UnNeg, mc, strings)); } - Err(e) => return Self::vm_err_to_action(e), + Err(e) => return self.handle_vm_error(e), } } OpNot => { @@ -1623,15 +1980,15 @@ impl<'gc> VM<'gc> { match self.force_inline(val) { Ok(ForceResult::Ready(f)) => match f.as_inline::() { Some(b) => self - .stack - .push(Value::new_inline(!b)) - .expect("stack overflow"), - None => return Self::vm_err_to_action(Self::err("value is not a boolean")), + .push_stack(Value::new_inline(!b)), + None => { + return Action::Done(Err(Error::eval_error("value is not a boolean"))); + } }, - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame(thunk, AfterForce::UnNot, ip, env, mc); + Ok(other) => { + try_vm!(self, self.setup_force(other, AfterForce::UnNot, mc, strings)); } - Err(e) => return Self::vm_err_to_action(e), + Err(e) => return self.handle_vm_error(e), } } @@ -1640,14 +1997,20 @@ impl<'gc> VM<'gc> { match self.force_inline(val) { Ok(ForceResult::Ready(f)) => { if f.as_inline::().is_none() { - return Self::vm_err_to_action(Self::err("value is not a boolean")); + return Action::Done(Err(Error::eval_error("value is not a boolean"))); } - self.stack.push(f.into_relaxed()).expect("stack overflow"); + self.push_stack(f.relax()); } Ok(ForceResult::NeedEval { ip, env, thunk }) => { self.push_force_frame(thunk, AfterForce::ForceBool, ip, env, mc); } - Err(e) => return Self::vm_err_to_action(e), + 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 => { @@ -1672,19 +2035,18 @@ impl<'gc> VM<'gc> { ConcatStrings => { let parts_count = self.read_u16(bc) as usize; let force_string = self.read_u8(bc) != 0; - let parts: SmallVec<[Value<'gc>; 8]> = self.stack.pop_n::<8>(parts_count); + 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.stack - .push(Value::new_inline(Null)) - .expect("stack overflow"); + self.push_stack(Value::new_inline(Null)); } Assert => { @@ -1695,36 +2057,38 @@ impl<'gc> VM<'gc> { match self.force_inline(assertion) { Ok(ForceResult::Ready(f)) => match f.as_inline::() { - Some(true) => self.stack.push(expr).expect("stack overflow"), + 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::vm_err_to_action(VmError::Uncatchable( - Error::eval_error(format!("assertion '{msg}' failed"), None), - )); + return self.handle_vm_error(VmError::Uncatchable(Error::eval_error( + format!("assertion '{msg}' failed"), + ))); } None => { - return Self::vm_err_to_action(Self::err( + return self.handle_vm_error(Self::err( "assertion condition must be a boolean", )); } }, - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::Assert { - expr, - raw_idx, - span_id, - }, - ip, - env, + Ok(other) => { + try_vm!( + self, + self.setup_force( + other, + AfterForce::Assert { + expr, + raw_idx, + span_id, + }, mc, + strings, + ) ); } - Err(e) => return Self::vm_err_to_action(e), + Err(e) => return self.handle_vm_error(e), } } @@ -1748,27 +2112,32 @@ impl<'gc> VM<'gc> { let name = self.read_string_id(bc); match self.with_scope { Some(scope_gc) => { - let env_val = scope_gc.env.clone(); + let env_val = scope_gc.env; let next = scope_gc.prev; match self.force_inline(env_val) { Ok(ForceResult::Ready(forced)) => { - try_vm!(self.do_with_lookup_step(forced, name, next, mc, strings)); - } - Ok(ForceResult::NeedEval { ip, env, thunk }) => { - self.push_force_frame( - thunk, - AfterForce::WithLookup { name, next }, - ip, - env, - mc, + try_vm!( + self, + self.do_with_lookup_step(forced, name, next, mc, strings) ); } - Err(e) => return Self::vm_err_to_action(e), + 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::vm_err_to_action(Self::err(format!( + return self.handle_vm_error(Self::err(format!( "undefined variable '{name_str}'" ))); } @@ -1776,35 +2145,32 @@ impl<'gc> VM<'gc> { } LoadBuiltins => { - self.stack - .push(self.globals.builtins.clone()) - .expect("stack overflow"); + self.push_stack(self.globals.builtins); } LoadBuiltin => { - let _name = self.read_u32(bc); - self.stack - .push(Value::new_inline(Null)) - .expect("stack overflow"); + 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); - self.stack - .push(Value::new_inline(Null)) - .expect("stack overflow"); + todo!("MkPos") } LoadReplBinding => { let _name = self.read_string_id(bc); - self.stack - .push(Value::new_inline(Null)) - .expect("stack overflow"); + todo!("LoadReplBinding") } LoadScopedBinding => { let _name = self.read_string_id(bc); - self.stack - .push(Value::new_inline(Null)) - .expect("stack overflow"); + todo!("LoadScopedBinding") } Return => { @@ -1823,13 +2189,16 @@ impl<'gc> VM<'gc> { 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..1024 { + for _ in 0..BATCH_SIZE { let action = self.execute_one(bytecode, mc, strings); match action { Action::Continue => {} @@ -1844,6 +2213,62 @@ impl<'gc> VM<'gc> { } *pc = self.pc; - Action::NeedGc + + 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; diff --git a/fix/src/store.rs b/fix/src/store.rs index 4fbcc00..62df2f0 100644 --- a/fix/src/store.rs +++ b/fix/src/store.rs @@ -7,7 +7,6 @@ mod validation; pub use config::StoreConfig; pub use daemon::DaemonStore; -pub use validation::validate_store_path; pub trait Store: Send + Sync { fn get_store_dir(&self) -> &str; diff --git a/fix/src/store/daemon.rs b/fix/src/store/daemon.rs index 8ce4705..915efa7 100644 --- a/fix/src/store/daemon.rs +++ b/fix/src/store/daemon.rs @@ -69,13 +69,10 @@ impl Store for DaemonStore { fn ensure_path(&self, path: &str) -> Result<()> { self.block_on(async { self.connection.ensure_path(path).await.map_err(|e| { - Error::eval_error( - format!( - "builtins.storePath: path '{}' is not valid in nix store: {}", - path, e - ), - None, - ) + Error::eval_error(format!( + "builtins.storePath: path '{}' is not valid in nix store: {}", + path, e + )) }) }) } diff --git a/fix/src/store/validation.rs b/fix/src/store/validation.rs index 6ea4a4b..74380ce 100644 --- a/fix/src/store/validation.rs +++ b/fix/src/store/validation.rs @@ -2,75 +2,72 @@ use crate::error::{Error, Result}; pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> { if !path.starts_with(store_dir) { - return Err(Error::eval_error( - format!("path '{}' is not in the Nix store", path), - None, - )); + return Err(Error::eval_error(format!( + "path '{}' is not in the Nix store", + path + ))); } let relative = path .strip_prefix(store_dir) .and_then(|s| s.strip_prefix('/')) - .ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path), None))?; + .ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path)))?; if relative.is_empty() { - return Err(Error::eval_error( - format!("store path cannot be store directory itself: {}", path), - None, - )); + return Err(Error::eval_error(format!( + "store path cannot be store directory itself: {}", + path + ))); } let parts: Vec<&str> = relative.splitn(2, '-').collect(); if parts.len() != 2 { - return Err(Error::eval_error( - format!("invalid store path format (missing name): {}", path), - None, - )); + return Err(Error::eval_error(format!( + "invalid store path format (missing name): {}", + path + ))); } let hash = parts[0]; let name = parts[1]; if hash.len() != 32 { - return Err(Error::eval_error( - format!( - "invalid store path hash length (expected 32, got {}): {}", - hash.len(), - hash - ), - None, - )); + return Err(Error::eval_error(format!( + "invalid store path hash length (expected 32, got {}): {}", + hash.len(), + hash + ))); } for ch in hash.chars() { if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') { - return Err(Error::eval_error( - format!("invalid character '{}' in store path hash: {}", ch, hash), - None, - )); + return Err(Error::eval_error(format!( + "invalid character '{}' in store path hash: {}", + ch, hash + ))); } } if name.is_empty() { - return Err(Error::eval_error( - format!("store path has empty name: {}", path), - None, - )); + return Err(Error::eval_error(format!( + "store path has empty name: {}", + path + ))); } if name.starts_with('.') { - return Err(Error::eval_error( - format!("store path name cannot start with '.': {}", name), - None, - )); + return Err(Error::eval_error(format!( + "store path name cannot start with '.': {}", + name + ))); } for ch in name.chars() { if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') { - return Err(Error::eval_error( - format!("invalid character '{}' in store path name: {}", ch, name), - None, - )); + return Err(Error::eval_error(format!( + "invalid character '{}' in store path name: {}", + ch, name + ))); } } diff --git a/fix/src/value.rs b/fix/src/value.rs index 49fa388..9425613 100644 --- a/fix/src/value.rs +++ b/fix/src/value.rs @@ -267,6 +267,15 @@ fn escape_quote_string(s: &str) -> String { ret } +/// Wrapper to format a float in Nix style (C printf `%g` with precision 6). +pub(crate) struct NixFloat(pub f64); + +impl Display for NixFloat { + fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult { + fmt_nix_float(f, self.0) + } +} + /// Format a float matching C's `printf("%g", x)` with default precision 6. fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult { if !x.is_finite() { diff --git a/flake.lock b/flake.lock index 00d02c2..bc74035 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1773471952, - "narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=", + "lastModified": 1774076307, + "narHash": "sha256-v8axK9HGgVERw9oG3SKdsuE+ri0GPUZDyRBN4GLqQ1c=", "owner": "nix-community", "repo": "fenix", - "rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5", + "rev": "556198cc6c69c0a13228a15e33b2360f333b0092", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1773282481, - "narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=", + "lastModified": 1773821835, + "narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", + "rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "type": "github" }, "original": { @@ -61,11 +61,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1773326183, - "narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=", + "lastModified": 1774036669, + "narHash": "sha256-EWhsBSh/h1VcyLKXuTEyH8lNVB2ktuKVkqx8dkQ6hxk=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "6254616e97f358e67b70dfc0463687f5f7911c1a", + "rev": "0cf3e8a07f0e29825f5db78840e646a4bb519742", "type": "github" }, "original": {