diff --git a/Cargo.lock b/Cargo.lock index 6d6c807..0eee692 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -41,12 +41,6 @@ version = "0.2.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" -[[package]] -name = "allocator-api2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c880a97d28a3681c0267bd29cff89621202715b065127cd445fa0f0fe0aa2880" - [[package]] name = "anes" version = "0.1.6" @@ -196,9 +190,7 @@ dependencies = [ [[package]] name = "boxing" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7a817f12ef805b34fe1565bea00630d84f8f08bf26200b05c41456c77cdada88" +version = "0.1.3" dependencies = [ "sptr", ] @@ -220,7 +212,7 @@ version = "3.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d20789868f4b01b2f2caec9f5c4e0213b41e3e5702a50157d699ae31ced2fcb" dependencies = [ - "allocator-api2 0.2.21", + "allocator-api2", ] [[package]] @@ -804,7 +796,6 @@ checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" name = "fix" version = "0.1.0" dependencies = [ - "allocator-api2 0.4.0", "anyhow", "base64", "boxing", @@ -835,10 +826,13 @@ dependencies = [ "rusqlite", "rust-embed", "rustyline", + "sealed", "serde", "serde_json", "sha1", "sha2", + "small-map", + "smallvec", "string-interner", "tap", "tar", @@ -980,20 +974,18 @@ dependencies = [ [[package]] name = "gc-arena" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cd70cf88a32937834aae9614ff2569b5d9467fa0c42c5d7762fd94a8de88266" +source = "git+https://github.com/kyren/gc-arena?rev=75671ae03f53718357b741ed4027560f14e90836#75671ae03f53718357b741ed4027560f14e90836" dependencies = [ - "allocator-api2 0.2.21", + "allocator-api2", "gc-arena-derive", - "hashbrown 0.14.5", - "sptr", + "hashbrown 0.16.1", + "smallvec", ] [[package]] name = "gc-arena-derive" version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c612a69f5557a11046b77a7408d2836fe77077f842171cd211c5ef504bd3cddd" +source = "git+https://github.com/kyren/gc-arena?rev=75671ae03f53718357b741ed4027560f14e90836#75671ae03f53718357b741ed4027560f14e90836" dependencies = [ "proc-macro2", "quote", @@ -1079,9 +1071,6 @@ name = "hashbrown" version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" -dependencies = [ - "allocator-api2 0.2.21", -] [[package]] name = "hashbrown" @@ -1098,7 +1087,7 @@ version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" dependencies = [ - "allocator-api2 0.2.21", + "allocator-api2", "equivalent", "foldhash 0.2.0", ] @@ -2024,6 +2013,15 @@ dependencies = [ "getrandom 0.3.4", ] +[[package]] +name = "rapidhash" +version = "4.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5e48930979c155e2f33aa36ab3119b5ee81332beb6482199a8ecd6029b80b59" +dependencies = [ + "rustversion", +] + [[package]] name = "rayon" version = "1.11.0" @@ -2385,6 +2383,17 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "sealed" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "22f968c5ea23d555e670b449c1c5e7b2fc399fdaec1d304a17cd48e288abc107" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "security-framework" version = "3.6.0" @@ -2524,6 +2533,17 @@ version = "0.4.12" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5" +[[package]] +name = "small-map" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64c0b79fa0e86d80062670059d27f93911776cdeba2555bf428b455d8738c32f" +dependencies = [ + "hashbrown 0.16.1", + "rapidhash", + "rustc-hash 2.1.1", +] + [[package]] name = "smallvec" version = "1.15.1" diff --git a/Cargo.toml b/Cargo.toml index eb64a44..deba82d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,7 +1,8 @@ [workspace] resolver = "3" members = [ - "fix" + "fix", + "boxing", ] [profile.profiling] diff --git a/boxing/Cargo.toml b/boxing/Cargo.toml new file mode 100644 index 0000000..fc0becf --- /dev/null +++ b/boxing/Cargo.toml @@ -0,0 +1,8 @@ +[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 new file mode 100644 index 0000000..0bb480f --- /dev/null +++ b/boxing/src/lib.rs @@ -0,0 +1,2 @@ +pub mod nan; +mod utils; diff --git a/boxing/src/nan.rs b/boxing/src/nan.rs new file mode 100644 index 0000000..d653ded --- /dev/null +++ b/boxing/src/nan.rs @@ -0,0 +1,7 @@ +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/nan/raw.rs b/boxing/src/nan/raw.rs new file mode 100644 index 0000000..1d70fcd --- /dev/null +++ b/boxing/src/nan/raw.rs @@ -0,0 +1,471 @@ +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 { + fn to_val(self, value: &mut Value); + fn from_val(value: &Value) -> Self; +} + +impl RawStore for [u8; 6] { + #[inline] + fn to_val(self, value: &mut Value) { + value.set_data(self); + } + + #[inline] + fn from_val(value: &Value) -> Self { + *value.data() + } +} + +impl RawStore for bool { + #[inline] + fn to_val(self, value: &mut Value) { + value.set_data([u8::from(self)].truncate_to()); + } + + #[inline] + fn from_val(value: &Value) -> Self { + value.data()[0] == 1 + } +} + +macro_rules! int_store { + ($ty:ty) => { + impl RawStore for $ty { + #[inline] + fn to_val(self, value: &mut Value) { + let bytes = self.to_ne_bytes(); + value.set_data(bytes.truncate_to()); + } + + #[inline] + fn from_val(value: &Value) -> Self { + <$ty>::from_ne_bytes(value.data().truncate_to()) + } + } + }; +} + +int_store!(u8); +int_store!(u16); +int_store!(u32); + +int_store!(i8); +int_store!(i16); +int_store!(i32); + +fn store_ptr(value: &mut Value, ptr: P) { + #[cfg(target_pointer_width = "64")] + { + assert!( + ptr.addr() <= 0x0000_FFFF_FFFF_FFFF, + "Pointer too large to store in NaN box" + ); + + let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::

(); + + let ptr = Strict::map_addr(ptr, |addr| { + addr | (usize::from(value.header().into_raw()) << 48) + }); + + unsafe { val.write(ptr) }; + } + + #[cfg(target_pointer_width = "32")] + { + let _ = (value, ptr); + unimplemented!("32-bit pointer storage not supported"); + } +} + +fn load_ptr(value: &Value) -> P { + #[cfg(target_pointer_width = "64")] + { + let val = (unsafe { value.whole() } as *const [u8; 8]).cast::

(); + let ptr = unsafe { val.read() }; + Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF) + } + + #[cfg(target_pointer_width = "32")] + { + let _ = value; + unimplemented!("32-bit pointer storage not supported"); + } +} + +impl RawStore for *const T { + fn to_val(self, value: &mut Value) { + store_ptr::<*const T>(value, self); + } + + fn from_val(value: &Value) -> Self { + load_ptr::<*const T>(value) + } +} + +impl RawStore for *mut T { + fn to_val(self, value: &mut Value) { + store_ptr::<*mut T>(value, self); + } + + fn from_val(value: &Value) -> Self { + load_ptr::<*mut T>(value) + } +} + +#[derive(Copy, Clone, PartialEq, Eq)] +enum TagVal { + _P1, + _P2, + _P3, + _P4, + _P5, + _P6, + _P7, + + _N1, + _N2, + _N3, + _N4, + _N5, + _N6, + _N7, +} + +#[derive(Copy, Clone, PartialEq, Eq)] +pub struct RawTag(TagVal); + +impl RawTag { + #[inline] + #[must_use] + pub 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 { + Some(RawTag(match (neg, val) { + (false, 1) => TagVal::_P1, + (false, 2) => TagVal::_P2, + (false, 3) => TagVal::_P3, + (false, 4) => TagVal::_P4, + (false, 5) => TagVal::_P5, + (false, 6) => TagVal::_P6, + (false, 7) => TagVal::_P7, + + (true, 1) => TagVal::_N1, + (true, 2) => TagVal::_N2, + (true, 3) => TagVal::_N3, + (true, 4) => TagVal::_N4, + (true, 5) => TagVal::_N5, + (true, 6) => TagVal::_N6, + (true, 7) => TagVal::_N7, + + _ => return None, + })) + } + + /// # Safety + /// + /// `val` must be in the range `1..8` + #[inline] + #[must_use] + pub unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag { + RawTag(match (neg, val) { + (false, 1) => TagVal::_P1, + (false, 2) => TagVal::_P2, + (false, 3) => TagVal::_P3, + (false, 4) => TagVal::_P4, + (false, 5) => TagVal::_P5, + (false, 6) => TagVal::_P6, + (false, 7) => TagVal::_P7, + + (true, 1) => TagVal::_N1, + (true, 2) => TagVal::_N2, + (true, 3) => TagVal::_N3, + (true, 4) => TagVal::_N4, + (true, 5) => TagVal::_N5, + (true, 6) => TagVal::_N6, + (true, 7) => TagVal::_N7, + + _ => unsafe { core::hint::unreachable_unchecked() }, + }) + } + + #[inline] + #[must_use] + pub fn is_neg(self) -> bool { + matches!(self.0, |TagVal::_N1| TagVal::_N2 + | TagVal::_N3 + | TagVal::_N4 + | TagVal::_N5 + | TagVal::_N6 + | TagVal::_N7) + } + + #[inline] + #[must_use] + pub fn val(self) -> NonZeroU8 { + match self.0 { + TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN, + TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1), + TagVal::_P3 | TagVal::_N3 => NonZeroU8::MIN.saturating_add(2), + TagVal::_P4 | TagVal::_N4 => NonZeroU8::MIN.saturating_add(3), + TagVal::_P5 | TagVal::_N5 => NonZeroU8::MIN.saturating_add(4), + TagVal::_P6 | TagVal::_N6 => NonZeroU8::MIN.saturating_add(5), + TagVal::_P7 | TagVal::_N7 => NonZeroU8::MIN.saturating_add(6), + } + } + + #[inline] + #[must_use] + pub fn neg_val(self) -> (bool, u8) { + match self.0 { + TagVal::_P1 => (false, 1), + TagVal::_P2 => (false, 2), + TagVal::_P3 => (false, 3), + TagVal::_P4 => (false, 4), + TagVal::_P5 => (false, 5), + TagVal::_P6 => (false, 6), + TagVal::_P7 => (false, 7), + TagVal::_N1 => (true, 1), + TagVal::_N2 => (true, 2), + TagVal::_N3 => (true, 3), + TagVal::_N4 => (true, 4), + TagVal::_N5 => (true, 5), + TagVal::_N6 => (true, 6), + TagVal::_N7 => (true, 7), + } + } +} + +impl fmt::Debug for RawTag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("RawTag") + .field("neg", &self.is_neg()) + .field("val", &self.val()) + .finish() + } +} + +#[derive(Copy, Clone, Debug, PartialEq)] +#[repr(transparent)] +struct Header(u16); + +impl Header { + #[inline] + fn new(tag: RawTag) -> Header { + let (neg, val) = tag.neg_val(); + Header(0x7FF8 | (u16::from(neg) << 15) | u16::from(val)) + } + + #[inline] + fn tag(self) -> RawTag { + unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) } + } + + #[inline] + fn get_sign(self) -> bool { + self.0 & 0x8000 != 0 + } + + #[inline] + fn get_tag(self) -> u8 { + (self.0 & 0x0007) as u8 + } + + #[inline] + fn into_raw(self) -> u16 { + self.0 + } +} + +#[derive(Clone, Debug, PartialEq)] +#[repr(C, align(8))] +pub struct Value { + #[cfg(target_endian = "big")] + header: Header, + data: [u8; 6], + #[cfg(target_endian = "little")] + header: Header, +} + +impl Value { + #[inline] + pub fn new(tag: RawTag, data: [u8; 6]) -> Value { + Value { + header: Header::new(tag), + data, + } + } + + #[inline] + pub fn empty(tag: RawTag) -> Value { + Value::new(tag, [0; 6]) + } + + pub 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 { + T::from_val(&self) + } + + #[inline] + #[must_use] + pub fn tag(&self) -> RawTag { + self.header.tag() + } + + #[inline] + fn header(&self) -> &Header { + &self.header + } + + #[inline] + pub fn set_data(&mut self, val: [u8; 6]) { + self.data = val; + } + + #[inline] + #[must_use] + pub fn data(&self) -> &[u8; 6] { + &self.data + } + + #[inline] + #[must_use] + pub fn data_mut(&mut self) -> &mut [u8; 6] { + &mut self.data + } + + #[inline] + #[must_use] + pub 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] { + let ptr = (self as *mut Value).cast::<[u8; 8]>(); + unsafe { &mut *ptr } + } +} + +#[repr(C)] +pub union RawBox { + float: f64, + value: ManuallyDrop, + bits: u64, + #[cfg(target_pointer_width = "64")] + ptr: *const (), + #[cfg(target_pointer_width = "32")] + ptr: (u32, *const ()), +} + +impl RawBox { + #[inline] + #[must_use] + pub fn from_float(val: f64) -> RawBox { + match (val.is_nan(), val.is_sign_positive()) { + (true, true) => RawBox { + float: f64::from_bits(QUIET_NAN), + }, + (true, false) => RawBox { + float: f64::from_bits(NEG_QUIET_NAN), + }, + (false, _) => RawBox { float: val }, + } + } + + #[inline] + #[must_use] + pub fn from_value(value: Value) -> RawBox { + RawBox { + value: ManuallyDrop::new(value), + } + } + + #[inline] + #[must_use] + pub fn tag(&self) -> Option { + if self.is_value() { + Some(unsafe { self.value.tag() }) + } else { + None + } + } + + #[inline] + #[must_use] + pub 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 { + (unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN }) + } + + #[inline] + #[must_use] + pub fn float(&self) -> Option<&f64> { + if self.is_float() { + Some(unsafe { &self.float }) + } else { + None + } + } + + #[inline] + #[must_use] + pub fn value(&self) -> Option<&Value> { + if self.is_value() { + Some(unsafe { &self.value }) + } else { + None + } + } + + #[inline] + pub 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() { + Some(val) => f.debug_tuple("RawBox::Float").field(val).finish(), + None => { + let val = self.value().expect("RawBox is neither float nor value"); + + f.debug_struct("RawBox::Data") + .field("tag", &val.tag()) + .field("data", val.data()) + .finish() + } + } + } +} diff --git a/boxing/src/utils.rs b/boxing/src/utils.rs new file mode 100644 index 0000000..2fe7ee3 --- /dev/null +++ b/boxing/src/utils.rs @@ -0,0 +1,16 @@ +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 4d91c5a..2234b4a 100644 --- a/fix/Cargo.toml +++ b/fix/Cargo.toml @@ -6,8 +6,16 @@ edition = "2024" [dependencies] mimalloc = "0.1" -tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"] } -nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"] } +tokio = { version = "1.41", features = [ + "rt-multi-thread", + "sync", + "net", + "io-util", +] } +nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = [ + "wire", + "async", +] } # REPL anyhow = "1.0" @@ -26,9 +34,13 @@ miette = { version = "7.4", features = ["fancy"] } hashbrown = "0.16" string-interner = "0.19" -bumpalo = { version = "3.20", features = ["allocator-api2", "boxed", "collections"] } +bumpalo = { version = "3.20", features = [ + "allocator-api2", + "boxed", + "collections", +] } -rust-embed="8.11" +rust-embed = "8.11" itertools = "0.14" @@ -42,7 +54,10 @@ hex = "0.4" base64 = "0.22" -reqwest = { version = "0.13", features = ["blocking", "rustls"], default-features = false } +reqwest = { version = "0.13", features = [ + "blocking", + "rustls", +], default-features = false } tar = "0.4" flate2 = "1.0" xz2 = "0.1" @@ -64,9 +79,15 @@ tap = "1.0.1" ghost-cell = "0.2" colored = "3.1" -boxing = "0.1" -gc-arena = { version = "0.5.3", features = ["allocator-api2"] } -allocator-api2 = "0.4.0" +boxing = { path = "../boxing" } +sealed = "0.6" +small-map = "0.1" +smallvec = "1.15" + +[dependencies.gc-arena] +git = "https://github.com/kyren/gc-arena" +rev = "75671ae03f53718357b741ed4027560f14e90836" +features = ["allocator-api2", "hashbrown", "smallvec"] [dev-dependencies] criterion = { version = "0.8", features = ["html_reports"] } diff --git a/fix/benches/utils.rs b/fix/benches/utils.rs index 21e9d01..3e9fd5b 100644 --- a/fix/benches/utils.rs +++ b/fix/benches/utils.rs @@ -1,18 +1,18 @@ #![allow(dead_code)] -use fix::context::Context; use fix::error::{Result, Source}; +use fix::runtime::Runtime; use fix::value::Value; pub fn eval(expr: &str) -> Value { - Context::new() + Runtime::new() .unwrap() .eval(Source::new_eval(expr.into()).unwrap()) .unwrap() } pub fn eval_result(expr: &str) -> Result { - Context::new() + Runtime::new() .unwrap() .eval(Source::new_eval(expr.into()).unwrap()) } diff --git a/fix/src/bytecode.rs b/fix/src/codegen.rs similarity index 83% rename from fix/src/bytecode.rs rename to fix/src/codegen.rs index 2cc4927..9ca1375 100644 --- a/fix/src/bytecode.rs +++ b/fix/src/codegen.rs @@ -1,36 +1,36 @@ use std::ops::Deref; -use std::path::Path; +use gc_arena::Collect; use hashbrown::HashMap; use num_enum::TryFromPrimitive; use rnix::TextRange; +use string_interner::Symbol as _; -use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, SymId, ThunkId, UnOpKind}; +use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, StringId, ThunkId, UnOpKind}; -#[derive(Clone, Hash, Eq, PartialEq)] -pub(crate) enum Constant { - Int(i64), - Float(u64), -} +pub(crate) struct InstructionPtr(pub usize); +#[derive(Collect)] +#[collect(require_static)] pub struct Bytecode { pub code: Box<[u8]>, pub current_dir: String, } pub(crate) trait BytecodeContext { - fn intern_string(&mut self, s: &str) -> u32; - fn intern_constant(&mut self, c: Constant) -> u32; - fn register_span(&self, range: TextRange) -> u32; - fn get_sym(&self, id: SymId) -> &str; - fn get_current_dir(&self) -> &Path; + fn intern_string(&mut self, s: &str) -> StringId; + fn register_span(&mut self, range: TextRange) -> u32; + fn get_code(&self) -> &[u8]; + fn get_code_mut(&mut self) -> &mut Vec; } #[repr(u8)] #[derive(Clone, Copy, TryFromPrimitive)] #[allow(clippy::enum_variant_names)] pub enum Op { - PushConst, + PushSmi, + PushBigInt, + PushFloat, PushString, PushNull, PushTrue, @@ -106,58 +106,78 @@ struct ScopeInfo { struct BytecodeEmitter<'a, Ctx: BytecodeContext> { ctx: &'a mut Ctx, - code: Vec, scope_stack: Vec, } -pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> Bytecode { - let current_dir = ctx.get_current_dir().to_string_lossy().to_string(); +pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr { + let ip = ctx.get_code().len(); let mut emitter = BytecodeEmitter::new(ctx); emitter.emit_toplevel(ir); - Bytecode { - code: emitter.code.into_boxed_slice(), - current_dir, - } + InstructionPtr(ip) } impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { fn new(ctx: &'a mut Ctx) -> Self { Self { ctx, - code: Vec::with_capacity(4096), scope_stack: Vec::with_capacity(32), } } #[inline] fn emit_op(&mut self, op: Op) { - self.code.push(op as u8); + self.ctx.get_code_mut().push(op as u8); } #[inline] fn emit_u8(&mut self, val: u8) { - self.code.push(val); + self.ctx.get_code_mut().push(val); } #[inline] fn emit_u16(&mut self, val: u16) { - self.code.extend_from_slice(&val.to_le_bytes()); + self.ctx + .get_code_mut() + .extend_from_slice(&val.to_le_bytes()); } #[inline] fn emit_u32(&mut self, val: u32) { - self.code.extend_from_slice(&val.to_le_bytes()); + self.ctx + .get_code_mut() + .extend_from_slice(&val.to_le_bytes()); + } + + #[inline] + fn emit_i32(&mut self, val: i32) { + self.ctx + .get_code_mut() + .extend_from_slice(&val.to_le_bytes()); + } + + #[inline] + fn emit_i64(&mut self, val: i64) { + self.ctx + .get_code_mut() + .extend_from_slice(&val.to_le_bytes()); + } + + #[inline] + fn emit_f64(&mut self, val: f64) { + self.ctx + .get_code_mut() + .extend_from_slice(&val.to_le_bytes()); } #[inline] fn emit_i32_placeholder(&mut self) -> usize { - let offset = self.code.len(); - self.code.extend_from_slice(&[0u8; 4]); + let offset = self.ctx.get_code_mut().len(); + self.ctx.get_code_mut().extend_from_slice(&[0u8; 4]); offset } #[inline] fn patch_i32(&mut self, offset: usize, val: i32) { - self.code[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); + self.ctx.get_code_mut()[offset..offset + 4].copy_from_slice(&val.to_le_bytes()); } #[inline] @@ -168,11 +188,18 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { #[inline] fn patch_jump_target(&mut self, placeholder_offset: usize) { - let current_pos = self.code.len(); + let current_pos = self.ctx.get_code_mut().len(); let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4; self.patch_i32(placeholder_offset, relative_offset); } + #[inline] + fn emit_str_id(&mut self, id: StringId) { + self.ctx + .get_code_mut() + .extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes()); + } + fn current_depth(&self) -> u16 { self.scope_stack.last().map_or(0, |s| s.depth) } @@ -390,13 +417,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { let label_idx = self.ctx.intern_string(&label); let skip_patch = self.emit_jump_placeholder(); - let entry_point = self.code.len() as u32; + let entry_point = self.ctx.get_code_mut().len() as u32; self.emit_expr(inner); self.emit_op(Op::Return); self.patch_jump_target(skip_patch); self.emit_op(Op::MakeThunk); self.emit_u32(entry_point); - self.emit_u32(label_idx); + self.emit_str_id(label_idx); let (_, local_idx) = self.resolve_thunk(id); self.emit_op(Op::StoreLocal); self.emit_u32(local_idx); @@ -406,14 +433,17 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { fn emit_expr(&mut self, ir: RawIrRef<'_>) { match ir.deref() { &Ir::Int(x) => { - let idx = self.ctx.intern_constant(Constant::Int(x)); - self.emit_op(Op::PushConst); - self.emit_u32(idx); + if x <= i32::MAX as i64 { + self.emit_op(Op::PushSmi); + self.emit_i32(x as i32); + } else { + self.emit_op(Op::PushBigInt); + self.emit_i64(x); + } } &Ir::Float(x) => { - let idx = self.ctx.intern_constant(Constant::Float(x.to_bits())); - self.emit_op(Op::PushConst); - self.emit_u32(idx); + self.emit_op(Op::PushFloat); + self.emit_f64(x); } &Ir::Bool(true) => self.emit_op(Op::PushTrue), &Ir::Bool(false) => self.emit_op(Op::PushFalse), @@ -421,7 +451,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { Ir::Str(s) => { let idx = self.ctx.intern_string(s.deref()); self.emit_op(Op::PushString); - self.emit_u32(idx); + self.emit_str_id(idx); } &Ir::Path(p) => { self.emit_expr(p); @@ -433,20 +463,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::JumpIfFalse); let else_placeholder = self.emit_i32_placeholder(); - let after_jif = self.code.len(); + let after_jif = self.ctx.get_code_mut().len(); self.emit_expr(consq); self.emit_op(Op::Jump); let end_placeholder = self.emit_i32_placeholder(); - let after_jump = self.code.len(); + let after_jump = self.ctx.get_code_mut().len(); let else_offset = (after_jump as i32) - (after_jif as i32); self.patch_i32(else_placeholder, else_offset); self.emit_expr(alter); - let end_offset = (self.code.len() as i32) - (after_jump as i32); + let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32); self.patch_i32(end_placeholder, end_offset); } &Ir::BinOp { lhs, rhs, kind } => { @@ -510,10 +540,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::LoadBuiltins); } &Ir::Builtin(name) => { - let sym = self.ctx.get_sym(name).to_string(); - let idx = self.ctx.intern_string(&sym); self.emit_op(Op::LoadBuiltin); - self.emit_u32(idx); + self.emit_u32(name.0.to_usize() as u32); } &Ir::ConcatStrings { ref parts, @@ -540,7 +568,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_expr(*assertion); self.emit_expr(*expr); self.emit_op(Op::Assert); - self.emit_u32(raw_idx); + self.emit_str_id(raw_idx); self.emit_u32(span_id); } &Ir::CurPos(span) => { @@ -549,16 +577,12 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_u32(span_id); } &Ir::ReplBinding(name) => { - let sym = self.ctx.get_sym(name).to_string(); - let idx = self.ctx.intern_string(&sym); self.emit_op(Op::LoadReplBinding); - self.emit_u32(idx); + self.emit_str_id(name); } &Ir::ScopedImportBinding(name) => { - let sym = self.ctx.get_sym(name).to_string(); - let idx = self.ctx.intern_string(&sym); self.emit_op(Op::LoadScopedBinding); - self.emit_u32(idx); + self.emit_str_id(name); } &Ir::With { namespace, @@ -568,10 +592,8 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_with(namespace, body, thunks); } &Ir::WithLookup(name) => { - let sym = self.ctx.get_sym(name).to_string(); - let idx = self.ctx.intern_string(&sym); self.emit_op(Op::WithLookup); - self.emit_u32(idx); + self.emit_str_id(name); } } } @@ -584,20 +606,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::ForceBool); self.emit_op(Op::JumpIfFalse); let skip_placeholder = self.emit_i32_placeholder(); - let after_jif = self.code.len(); + let after_jif = self.ctx.get_code_mut().len(); self.emit_expr(rhs); self.emit_op(Op::ForceBool); self.emit_op(Op::Jump); let end_placeholder = self.emit_i32_placeholder(); - let after_jump = self.code.len(); + let after_jump = self.ctx.get_code_mut().len(); let false_offset = (after_jump as i32) - (after_jif as i32); self.patch_i32(skip_placeholder, false_offset); self.emit_op(Op::PushFalse); - let end_offset = (self.code.len() as i32) - (after_jump as i32); + let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32); self.patch_i32(end_placeholder, end_offset); } Or => { @@ -605,20 +627,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::ForceBool); self.emit_op(Op::JumpIfTrue); let skip_placeholder = self.emit_i32_placeholder(); - let after_jit = self.code.len(); + let after_jit = self.ctx.get_code_mut().len(); self.emit_expr(rhs); self.emit_op(Op::ForceBool); self.emit_op(Op::Jump); let end_placeholder = self.emit_i32_placeholder(); - let after_jump = self.code.len(); + let after_jump = self.ctx.get_code_mut().len(); let true_offset = (after_jump as i32) - (after_jit as i32); self.patch_i32(skip_placeholder, true_offset); self.emit_op(Op::PushTrue); - let end_offset = (self.code.len() as i32) - (after_jump as i32); + let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32); self.patch_i32(end_placeholder, end_offset); } Impl => { @@ -626,20 +648,20 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_op(Op::ForceBool); self.emit_op(Op::JumpIfFalse); let skip_placeholder = self.emit_i32_placeholder(); - let after_jif = self.code.len(); + let after_jif = self.ctx.get_code_mut().len(); self.emit_expr(rhs); self.emit_op(Op::ForceBool); self.emit_op(Op::Jump); let end_placeholder = self.emit_i32_placeholder(); - let after_jump = self.code.len(); + let after_jump = self.ctx.get_code_mut().len(); let true_offset = (after_jump as i32) - (after_jif as i32); self.patch_i32(skip_placeholder, true_offset); self.emit_op(Op::PushTrue); - let end_offset = (self.code.len() as i32) - (after_jump as i32); + let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32); self.patch_i32(end_placeholder, end_offset); } PipeL => { @@ -688,7 +710,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { let thunk_ids: Vec = all_thunks.iter().map(|&(id, _)| id).collect(); let skip_patch = self.emit_jump_placeholder(); - let entry_point = self.code.len() as u32; + let entry_point = self.ctx.get_code().len() as u32; self.push_scope(true, Some(arg), &thunk_ids); self.emit_scope_thunks(thunks); self.emit_expr(body); @@ -710,20 +732,14 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { self.emit_u8(if *ellipsis { 1 } else { 0 }); for &(sym, _) in required.iter() { - let name = self.ctx.get_sym(sym).to_string(); - let idx = self.ctx.intern_string(&name); - self.emit_u32(idx); + self.emit_str_id(sym); } for &(sym, _) in optional.iter() { - let name = self.ctx.get_sym(sym).to_string(); - let idx = self.ctx.intern_string(&name); - self.emit_u32(idx); + self.emit_str_id(sym); } for &(sym, span) in required.iter().chain(optional.iter()) { - let name = self.ctx.get_sym(sym).to_string(); - let name_idx = self.ctx.intern_string(&name); let span_id = self.ctx.register_span(span); - self.emit_u32(name_idx); + self.emit_str_id(sym); self.emit_u32(span_id); } } else { @@ -735,7 +751,7 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { fn emit_attrset( &mut self, - stcs: &crate::ir::HashMap<'_, SymId, (RawIrRef<'_>, TextRange)>, + stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>, dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)], ) { if stcs.is_empty() && dyns.is_empty() { @@ -745,42 +761,35 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { if !dyns.is_empty() { for (&sym, &(val, _)) in stcs.iter() { - let key = self.ctx.get_sym(sym).to_string(); - let idx = self.ctx.intern_string(&key); self.emit_op(Op::PushString); - self.emit_u32(idx); + self.emit_str_id(sym); self.emit_expr(val); } for (_, &(_, span)) in stcs.iter() { let span_id = self.ctx.register_span(span); - let idx = self.ctx.intern_constant(Constant::Int(span_id as i64)); - self.emit_op(Op::PushConst); - self.emit_u32(idx); + self.emit_op(Op::PushSmi); + self.emit_u32(span_id); } for &(key, val, span) in dyns.iter() { self.emit_expr(key); self.emit_expr(val); let span_id = self.ctx.register_span(span); - let idx = self.ctx.intern_constant(Constant::Int(span_id as i64)); - self.emit_op(Op::PushConst); - self.emit_u32(idx); + self.emit_op(Op::PushSmi); + self.emit_u32(span_id); } self.emit_op(Op::MakeAttrsDyn); self.emit_u32(stcs.len() as u32); self.emit_u32(dyns.len() as u32); } else { for (&sym, &(val, _)) in stcs.iter() { - let key = self.ctx.get_sym(sym).to_string(); - let idx = self.ctx.intern_string(&key); self.emit_op(Op::PushString); - self.emit_u32(idx); + self.emit_str_id(sym); self.emit_expr(val); } for (_, &(_, span)) in stcs.iter() { let span_id = self.ctx.register_span(span); - let idx = self.ctx.intern_constant(Constant::Int(span_id as i64)); - self.emit_op(Op::PushConst); - self.emit_u32(idx); + self.emit_op(Op::PushSmi); + self.emit_u32(span_id); } self.emit_op(Op::MakeAttrs); self.emit_u32(stcs.len() as u32); @@ -796,15 +805,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { ) { self.emit_expr(expr); for attr in attrpath.iter() { - match attr { + match *attr { Attr::Str(sym, _) => { - let key = self.ctx.get_sym(*sym).to_string(); - let idx = self.ctx.intern_string(&key); self.emit_op(Op::PushString); - self.emit_u32(idx); + self.emit_str_id(sym); } Attr::Dynamic(expr, _) => { - self.emit_expr(*expr); + self.emit_expr(expr); } } } @@ -826,15 +833,13 @@ impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> { fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr>]) { self.emit_expr(lhs); for attr in rhs.iter() { - match attr { + match *attr { Attr::Str(sym, _) => { - let key = self.ctx.get_sym(*sym).to_string(); - let idx = self.ctx.intern_string(&key); self.emit_op(Op::PushString); - self.emit_u32(idx); + self.emit_str_id(sym); } Attr::Dynamic(expr, _) => { - self.emit_expr(*expr); + self.emit_expr(expr); } } } diff --git a/fix/src/context.rs b/fix/src/context.rs deleted file mode 100644 index fd7bc1e..0000000 --- a/fix/src/context.rs +++ /dev/null @@ -1,592 +0,0 @@ -use std::cell::UnsafeCell; -use std::hash::BuildHasher; -use std::path::Path; - -use bumpalo::Bump; -use ghost_cell::{GhostCell, GhostToken}; -use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable}; -use rnix::TextRange; -use string_interner::DefaultStringInterner; - -use crate::bytecode::{self, Bytecode, BytecodeContext, Constant}; -use crate::disassembler::{Disassembler, DisassemblerContext}; -use crate::downgrade::*; -use crate::error::{Error, Result, Source}; -use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, SymId, ThunkId, ir_content_eq}; -use crate::store::{DaemonStore, Store, StoreConfig}; -use crate::value::{Symbol, Value}; - -fn parse_error_span(error: &rnix::ParseError) -> Option { - use rnix::ParseError::*; - match error { - Unexpected(range) - | UnexpectedExtra(range) - | UnexpectedWanted(_, range, _) - | UnexpectedDoubleBind(range) - | DuplicatedArgs(range, _) => Some(*range), - _ => None, - } -} - -fn handle_parse_error<'a>( - errors: impl IntoIterator, - source: Source, -) -> Option> { - for err in errors { - if let Some(span) = parse_error_span(err) { - return Some( - Error::parse_error(err.to_string()) - .with_source(source) - .with_span(span), - ); - } - } - None -} - -impl Context { - pub fn eval(&mut self, _source: Source) -> Result { - todo!() - } - pub fn eval_shallow(&mut self, _source: Source) -> Result { - todo!() - } - pub fn eval_deep(&mut self, _source: Source) -> Result { - todo!() - } - - pub fn eval_repl<'a>(&'a mut self, _source: Source, _scope: &'a HashSet) -> Result { - todo!() - } - - pub fn disassemble(&self, bytecode: &Bytecode) -> String { - Disassembler::new(bytecode, self).disassemble() - } - - pub fn disassemble_colored(&self, bytecode: &Bytecode) -> String { - Disassembler::new(bytecode, self).disassemble_colored() - } - - pub fn add_binding<'a>( - &'a mut self, - name: &str, - expr: &str, - scope: &'a mut HashSet, - ) -> Result { - todo!() - } -} - -pub struct Context { - symbols: DefaultStringInterner, - global: HashMap>>, - sources: Vec, - store: DaemonStore, - spans: UnsafeCell>, - thunk_count: usize, - global_strings: Vec, - global_string_map: HashMap, - global_constants: Vec, - global_constant_map: HashMap, - synced_strings: usize, - synced_constants: usize, -} - -/// Owns the bump allocator and a read-only reference into it. -/// -/// # Safety -/// The `ir` field points into `_bump`'s storage. We use `'static` as a sentinel -/// lifetime because the struct owns the backing memory. The `as_ref` method -/// re-binds the lifetime to `&self`, preventing use-after-free. -struct OwnedIr { - _bump: Bump, - ir: RawIrRef<'static>, -} - -impl OwnedIr { - fn as_ref(&self) -> RawIrRef<'_> { - self.ir - } -} - -impl Context { - pub fn new() -> Result { - let mut symbols = DefaultStringInterner::new(); - let mut global = HashMap::new(); - let builtins_sym = symbols.get_or_intern("builtins"); - global.insert(builtins_sym, Ir::Builtins); - - let free_globals = [ - "abort", - "baseNameOf", - "break", - "dirOf", - "derivation", - "derivationStrict", - "fetchGit", - "fetchMercurial", - "fetchTarball", - "fetchTree", - "fromTOML", - "import", - "isNull", - "map", - "placeholder", - "removeAttrs", - "scopedImport", - "throw", - "toString", - ]; - let consts = [ - ("true", Ir::Bool(true)), - ("false", Ir::Bool(false)), - ("null", Ir::Null), - ]; - - for name in free_globals { - let name = symbols.get_or_intern(name); - let value = Ir::Builtin(name); - global.insert(name, value); - } - for (name, value) in consts { - let name = symbols.get_or_intern(name); - global.insert(name, value); - } - - let config = StoreConfig::from_env(); - let store = DaemonStore::connect(&config.daemon_socket)?; - - Ok(Self { - symbols, - global, - sources: Vec::new(), - store, - spans: UnsafeCell::new(Vec::new()), - thunk_count: 0, - global_strings: Vec::new(), - global_string_map: HashMap::new(), - global_constants: Vec::new(), - global_constant_map: HashMap::new(), - synced_strings: 0, - synced_constants: 0, - }) - } - - fn downgrade_ctx<'ctx, 'id, 'ir>( - &'ctx mut self, - bump: &'ir Bump, - token: GhostToken<'id>, - extra_scope: Option>, - ) -> DowngradeCtx<'ctx, 'id, 'ir> { - let source = self.get_current_source(); - DowngradeCtx::new( - bump, - token, - &mut self.symbols, - &self.global, - extra_scope, - &mut self.thunk_count, - source, - ) - } - - fn get_current_dir(&self) -> &Path { - self.sources - .last() - .as_ref() - .expect("current_source is not set") - .get_dir() - } - - fn get_current_source(&self) -> Source { - self.sources - .last() - .expect("current_source is not set") - .clone() - } - - fn downgrade<'ctx>( - &'ctx mut self, - source: Source, - extra_scope: Option>, - ) -> Result { - tracing::debug!("Parsing Nix expression"); - - self.sources.push(source.clone()); - - let root = rnix::Root::parse(&source.src); - handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?; - - tracing::debug!("Downgrading Nix expression"); - let expr = root - .tree() - .expr() - .ok_or_else(|| Error::parse_error("unexpected EOF".into()))?; - let bump = Bump::new(); - GhostToken::new(|token| { - let ir = self - .downgrade_ctx(&bump, token, extra_scope) - .downgrade_toplevel(expr)?; - let ir = unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) }; - Ok(OwnedIr { _bump: bump, ir }) - }) - } - - pub fn compile_bytecode(&mut self, source: Source) -> Result { - let root = self.downgrade(source, None)?; - tracing::debug!("Generating bytecode"); - let bytecode = bytecode::compile_bytecode(root.as_ref(), self); - tracing::debug!("Compiled bytecode: {:#04X?}", bytecode.code); - Ok(bytecode) - } - - pub fn get_store_dir(&self) -> &str { - self.store.get_store_dir() - } -} - -impl BytecodeContext for Context { - fn intern_string(&mut self, s: &str) -> u32 { - if let Some(&idx) = self.global_string_map.get(s) { - return idx; - } - let idx = self.global_strings.len() as u32; - self.global_strings.push(s.to_string()); - self.global_string_map.insert(s.to_string(), idx); - idx - } - - fn intern_constant(&mut self, c: Constant) -> u32 { - if let Some(&idx) = self.global_constant_map.get(&c) { - return idx; - } - let idx = self.global_constants.len() as u32; - self.global_constants.push(c.clone()); - self.global_constant_map.insert(c, idx); - idx - } - - fn register_span(&self, range: TextRange) -> u32 { - // FIXME: SAFETY - let spans = unsafe { &mut *self.spans.get() }; - let id = spans.len(); - let source_id = self - .sources - .len() - .checked_sub(1) - .expect("current_source not set"); - spans.push((source_id, range)); - id as u32 - } - - fn get_sym(&self, id: SymId) -> &str { - self.symbols.resolve(id).expect("SymId out of bounds") - } - - fn get_current_dir(&self) -> &Path { - Context::get_current_dir(self) - } -} - -impl DisassemblerContext for Context { - fn lookup_string(&self, id: u32) -> &str { - self.global_strings - .get(id as usize) - .expect("string not found") - } - fn lookup_constant(&self, id: u32) -> &Constant { - self.global_constants - .get(id as usize) - .expect("constant not found") - } -} - -enum Scope<'ctx> { - Global(&'ctx HashMap>>), - Repl(&'ctx HashSet), - ScopedImport(HashSet), - Let(HashMap), - Param(SymId, ArgId), -} - -struct ScopeGuard<'a, 'ctx, 'id, 'ir> { - ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>, -} - -impl Drop for ScopeGuard<'_, '_, '_, '_> { - fn drop(&mut self) { - self.ctx.scopes.pop(); - } -} - -impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> { - fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> { - self.ctx - } -} - -struct ThunkScope<'id, 'ir> { - bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>, - cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>, - hasher: DefaultHashBuilder, -} - -impl<'id, 'ir> ThunkScope<'id, 'ir> { - fn new_in(bump: &'ir Bump) -> Self { - Self { - bindings: bumpalo::collections::Vec::new_in(bump), - cache: HashTable::new(), - hasher: DefaultHashBuilder::default(), - } - } - - fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option { - let hash = self.hasher.hash_one(IrKey(key, token)); - self.cache - .find(hash, |&(ir, _)| ir_content_eq(key, ir, token)) - .map(|&(_, id)| id) - } - - fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) { - self.bindings.push((id, ir)); - let hash = self.hasher.hash_one(IrKey(ir, token)); - self.cache.insert_unique(hash, (ir, id), |&(ir, _)| { - self.hasher.hash_one(IrKey(ir, token)) - }); - } - - fn extend_bindings(&mut self, iter: impl IntoIterator)>) { - self.bindings.extend(iter); - } -} - -struct DowngradeCtx<'ctx, 'id, 'ir> { - bump: &'ir Bump, - token: GhostToken<'id>, - symbols: &'ctx mut DefaultStringInterner, - source: Source, - scopes: Vec>, - with_scope_count: usize, - arg_count: usize, - thunk_count: &'ctx mut usize, - thunk_scopes: Vec>, -} - -fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool { - !matches!( - ir.borrow(token), - Ir::Builtin(_) - | Ir::Builtins - | Ir::Int(_) - | Ir::Float(_) - | Ir::Bool(_) - | Ir::Null - | Ir::Str(_) - | Ir::Thunk(_) - ) -} - -impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> { - fn new( - bump: &'ir Bump, - token: GhostToken<'id>, - symbols: &'ctx mut DefaultStringInterner, - global: &'ctx HashMap>>, - extra_scope: Option>, - thunk_count: &'ctx mut usize, - source: Source, - ) -> Self { - Self { - bump, - token, - symbols, - source, - scopes: std::iter::once(Scope::Global(global)) - .chain(extra_scope) - .collect(), - thunk_count, - arg_count: 0, - with_scope_count: 0, - thunk_scopes: vec![ThunkScope::new_in(bump)], - } - } -} - -impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> { - fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> { - IrRef::new(self.bump.alloc(GhostCell::new(expr))) - } - - fn new_arg(&mut self) -> ArgId { - self.arg_count += 1; - ArgId(self.arg_count - 1) - } - - fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> { - if !should_thunk(ir, &self.token) { - return ir; - } - - let cached = self - .thunk_scopes - .last() - .expect("no active cache scope") - .lookup_cache(ir, &self.token); - - if let Some(id) = cached { - return IrRef::alloc(self.bump, Ir::Thunk(id)); - } - - let id = ThunkId(*self.thunk_count); - *self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow"); - self.thunk_scopes - .last_mut() - .expect("no active cache scope") - .add_binding(id, ir, &self.token); - IrRef::alloc(self.bump, Ir::Thunk(id)) - } - - fn new_sym(&mut self, sym: String) -> SymId { - self.symbols.get_or_intern(sym) - } - - fn get_sym(&self, id: SymId) -> Symbol<'_> { - self.symbols.resolve(id).expect("no symbol found").into() - } - - fn lookup(&self, sym: SymId, span: TextRange) -> Result> { - for scope in self.scopes.iter().rev() { - match scope { - &Scope::Global(global_scope) => { - if let Some(expr) = global_scope.get(&sym) { - let ir = match expr { - Ir::Builtins => Ir::Builtins, - Ir::Builtin(s) => Ir::Builtin(*s), - Ir::Bool(b) => Ir::Bool(*b), - Ir::Null => Ir::Null, - _ => unreachable!("globals should only contain leaf IR nodes"), - }; - return Ok(self.new_expr(ir)); - } - } - &Scope::Repl(repl_bindings) => { - if repl_bindings.contains(&sym) { - return Ok(self.new_expr(Ir::ReplBinding(sym))); - } - } - Scope::ScopedImport(scoped_bindings) => { - if scoped_bindings.contains(&sym) { - return Ok(self.new_expr(Ir::ScopedImportBinding(sym))); - } - } - Scope::Let(let_scope) => { - if let Some(&expr) = let_scope.get(&sym) { - return Ok(self.new_expr(Ir::Thunk(expr))); - } - } - &Scope::Param(param_sym, id) => { - if param_sym == sym { - return Ok(self.new_expr(Ir::Arg(id))); - } - } - } - } - - if self.with_scope_count > 0 { - Ok(self.new_expr(Ir::WithLookup(sym))) - } else { - Err(Error::downgrade_error( - format!("'{}' not found", self.get_sym(sym)), - self.get_current_source(), - span, - )) - } - } - - fn get_current_source(&self) -> Source { - self.source.clone() - } - - fn with_let_scope(&mut self, keys: &[SymId], f: F) -> Result - where - F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>, - { - let base = *self.thunk_count; - *self.thunk_count = self - .thunk_count - .checked_add(keys.len()) - .expect("thunk id overflow"); - let iter = keys.iter().enumerate().map(|(offset, &key)| { - ( - key, - ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }), - ) - }); - self.scopes.push(Scope::Let(iter.collect())); - let (vals, ret) = { - let mut guard = ScopeGuard { ctx: self }; - f(guard.as_ctx())? - }; - assert_eq!(keys.len(), vals.len()); - let scope = self.thunk_scopes.last_mut().expect("no active thunk scope"); - scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals)); - Ok(ret) - } - - fn with_param_scope(&mut self, param: SymId, arg: ArgId, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - self.scopes.push(Scope::Param(param, arg)); - let mut guard = ScopeGuard { ctx: self }; - f(guard.as_ctx()) - } - - fn with_with_scope(&mut self, f: F) -> R - where - F: FnOnce(&mut Self) -> R, - { - self.with_scope_count += 1; - let ret = f(self); - self.with_scope_count -= 1; - ret - } - - fn with_thunk_scope( - &mut self, - f: F, - ) -> ( - R, - bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>, - ) - where - F: FnOnce(&mut Self) -> R, - { - self.thunk_scopes.push(ThunkScope::new_in(self.bump)); - let ret = f(self); - ( - ret, - self.thunk_scopes - .pop() - .expect("no thunk scope left???") - .bindings, - ) - } - - fn bump(&self) -> &'ir bumpalo::Bump { - self.bump - } -} - -impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> { - fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result> { - let body = root.downgrade(&mut self)?; - let thunks = self - .thunk_scopes - .pop() - .expect("no thunk scope left???") - .bindings; - let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks }); - Ok(ir.freeze(self.token)) - } -} diff --git a/fix/src/disassembler.rs b/fix/src/disassembler.rs index dc769e0..afd9770 100644 --- a/fix/src/disassembler.rs +++ b/fix/src/disassembler.rs @@ -3,11 +3,10 @@ use std::fmt::Write; use colored::Colorize; use num_enum::TryFromPrimitive; -use crate::bytecode::{Bytecode, Constant, Op}; +use crate::codegen::{Bytecode, Op}; pub(crate) trait DisassemblerContext { fn lookup_string(&self, id: u32) -> &str; - fn lookup_constant(&self, id: u32) -> &Constant; } pub(crate) struct Disassembler<'a, Ctx> { @@ -55,6 +54,22 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { i32::from_le_bytes(bytes) } + fn read_i64(&mut self) -> i64 { + let bytes = self.code[self.pos..self.pos + 8] + .try_into() + .expect("no enough bytes"); + self.pos += 8; + i64::from_le_bytes(bytes) + } + + fn read_f64(&mut self) -> f64 { + let bytes = self.code[self.pos..self.pos + 8] + .try_into() + .expect("no enough bytes"); + self.pos += 8; + f64::from_le_bytes(bytes) + } + pub fn disassemble(&mut self) -> String { self.disassemble_impl(false) } @@ -144,14 +159,17 @@ impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> { let op = Op::try_from_primitive(op_byte).expect("invalid op code"); match op { - Op::PushConst => { - let idx = self.read_u32(); - let val = self.ctx.lookup_constant(idx); - let val_str = match val { - Constant::Int(i) => format!("Int({})", i), - Constant::Float(f) => format!("Float(bits: {})", f), - }; - ("PushConst", format!("@{} ({})", idx, val_str)) + Op::PushSmi => { + let val = self.read_i32(); + ("PushSmi", format!("{}", val)) + } + Op::PushBigInt => { + let val = self.read_i64(); + ("PushBigInt", format!("{}", val)) + } + Op::PushFloat => { + let val = self.read_f64(); + ("PushFloat", format!("{}", val)) } Op::PushString => { let idx = self.read_u32(); diff --git a/fix/src/downgrade.rs b/fix/src/downgrade.rs index c69feec..1d5c1dc 100644 --- a/fix/src/downgrade.rs +++ b/fix/src/downgrade.rs @@ -50,16 +50,16 @@ pub trait DowngradeContext<'id: 'ir, 'ir> { fn new_arg(&mut self) -> ArgId; fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir>; - fn new_sym(&mut self, sym: String) -> SymId; - fn get_sym(&self, id: SymId) -> Symbol<'_>; - fn lookup(&self, sym: SymId, span: TextRange) -> Result>; + fn new_sym(&mut self, sym: String) -> StringId; + fn get_sym(&self, id: StringId) -> Symbol<'_>; + fn lookup(&self, sym: StringId, span: TextRange) -> Result>; fn get_current_source(&self) -> Source; - fn with_param_scope(&mut self, param: SymId, arg: ArgId, f: F) -> R + fn with_param_scope(&mut self, param: StringId, arg: ArgId, f: F) -> R where F: FnOnce(&mut Self) -> R; - fn with_let_scope(&mut self, bindings: &[SymId], f: F) -> Result + fn with_let_scope(&mut self, bindings: &[StringId], f: F) -> Result where F: FnOnce(&mut Self) -> Result<(Vec<'ir, IrRef<'id, 'ir>>, R)>; fn with_with_scope(&mut self, f: F) -> R @@ -456,8 +456,8 @@ impl<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>> Downgrade<'id, 'ir, Ctx> fo enum PendingValue<'ir> { Expr(ast::Expr), - InheritFrom(ast::Expr, SymId, TextRange), - InheritScope(SymId, TextRange), + InheritFrom(ast::Expr, StringId, TextRange), + InheritScope(StringId, TextRange), Set(PendingAttrSet<'ir>), ExtendedRecAttrSet { base: ast::AttrSet, @@ -466,7 +466,7 @@ enum PendingValue<'ir> { } struct PendingAttrSet<'ir> { - stcs: HashMap<'ir, SymId, (PendingValue<'ir>, TextRange)>, + stcs: HashMap<'ir, StringId, (PendingValue<'ir>, TextRange)>, dyns: Vec<'ir, (ast::Attr, PendingValue<'ir>, TextRange)>, } @@ -510,7 +510,7 @@ impl<'id: 'ir, 'ir> PendingAttrSet<'ir> { fn insert_static( &mut self, - sym: SymId, + sym: StringId, span: TextRange, path: &[ast::Attr], value: ast::Expr, @@ -846,7 +846,7 @@ fn make_attrpath_value_entry<'ir>(path: Vec<'ir, ast::Attr>, value: ast::Expr) - } struct FinalizedAttrSet<'id, 'ir> { - stcs: HashMap<'ir, SymId, (IrRef<'id, 'ir>, TextRange)>, + stcs: HashMap<'ir, StringId, (IrRef<'id, 'ir>, TextRange)>, dyns: Vec<'ir, (IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)>, } @@ -930,23 +930,23 @@ fn downgrade_attrpath<'id, 'ir>( struct PatternBindings<'id, 'ir> { body: IrRef<'id, 'ir>, - required: Vec<'ir, (SymId, TextRange)>, - optional: Vec<'ir, (SymId, TextRange)>, + required: Vec<'ir, (StringId, TextRange)>, + optional: Vec<'ir, (StringId, TextRange)>, } fn downgrade_pattern_bindings<'id, 'ir, Ctx>( pat_entries: impl Iterator, - alias: Option, + alias: Option, arg: ArgId, ctx: &mut Ctx, - body_fn: impl FnOnce(&mut Ctx, &[SymId]) -> Result>, + body_fn: impl FnOnce(&mut Ctx, &[StringId]) -> Result>, ) -> Result> where Ctx: DowngradeContext<'id, 'ir>, { let arg = ctx.new_expr(Ir::Arg(arg)); struct Param { - sym: SymId, + sym: StringId, sym_span: TextRange, default: Option, span: TextRange, @@ -1045,7 +1045,7 @@ fn downgrade_let_bindings<'id, 'ir, Ctx, F>( ) -> Result> where Ctx: DowngradeContext<'id, 'ir>, - F: FnOnce(&mut Ctx, &[SymId]) -> Result>, + F: FnOnce(&mut Ctx, &[StringId]) -> Result>, { downgrade_rec_attrs_impl::<_, _, false>(entries, ctx, |ctx, binding_keys, _dyns| { body_fn(ctx, binding_keys) @@ -1081,7 +1081,7 @@ where Ctx: DowngradeContext<'id, 'ir>, F: FnOnce( &mut Ctx, - &[SymId], + &[StringId], &[(IrRef<'id, 'ir>, IrRef<'id, 'ir>, TextRange)], ) -> Result>, { @@ -1106,7 +1106,7 @@ where fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>( entries: &[ast::Entry], ctx: &mut Ctx, -) -> Result, TextRange)>> { +) -> Result, TextRange)>> { let mut inherit_lookups = HashMap::new_in(ctx.bump()); for entry in entries { if let ast::Entry::Inherit(inherit) = entry @@ -1128,7 +1128,7 @@ fn collect_inherit_lookups<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>>( fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( pending: &PendingAttrSet, ctx: &mut Ctx, -) -> Result> { +) -> Result> { let mut binding_syms = HashSet::new(); for (sym, (_, span)) in &pending.stcs { @@ -1146,7 +1146,7 @@ fn collect_binding_syms<'id: 'ir, 'ir, Ctx: DowngradeContext<'id, 'ir>, const AL fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( pending: PendingAttrSet, - inherit_lookups: &HashMap, TextRange)>, + inherit_lookups: &HashMap, TextRange)>, ctx: &mut Ctx, ) -> Result> { let mut stcs = HashMap::new_in(ctx.bump()); @@ -1176,7 +1176,7 @@ fn finalize_pending_set<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_D fn finalize_pending_value<'id, 'ir, Ctx: DowngradeContext<'id, 'ir>, const ALLOW_DYN: bool>( value: PendingValue, - inherit_lookups: &HashMap, TextRange)>, + inherit_lookups: &HashMap, TextRange)>, ctx: &mut Ctx, ) -> Result> { match value { diff --git a/fix/src/ir.rs b/fix/src/ir.rs index 406c0e4..117ede5 100644 --- a/fix/src/ir.rs +++ b/fix/src/ir.rs @@ -4,6 +4,7 @@ use std::{ }; use bumpalo::{Bump, boxed::Box, collections::Vec}; +use gc_arena::Collect; use ghost_cell::{GhostCell, GhostToken}; use rnix::{TextRange, ast}; use string_interner::symbol::SymbolU32; @@ -23,6 +24,10 @@ impl<'id, 'ir> IrRef<'id, 'ir> { Self(bump.alloc(GhostCell::new(ir))) } + pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a Ir<'ir, Self> { + self.0.borrow(token) + } + /// Freeze a mutable IR reference into a read-only one, consuming the /// `GhostToken` to prevent any further mutation. /// @@ -42,13 +47,6 @@ impl<'id, 'ir> IrRef<'id, 'ir> { } } -impl<'id, 'ir> Deref for IrRef<'id, 'ir> { - type Target = GhostCell<'id, Ir<'ir, IrRef<'id, 'ir>>>; - fn deref(&self) -> &Self::Target { - self.0 - } -} - #[repr(transparent)] #[derive(Clone, Copy)] pub struct RawIrRef<'ir>(&'ir Ir<'ir, Self>); @@ -68,7 +66,7 @@ pub enum Ir<'ir, Ref> { Null, Str(Box<'ir, String>), AttrSet { - stcs: HashMap<'ir, SymId, (Ref, TextRange)>, + stcs: HashMap<'ir, StringId, (Ref, TextRange)>, dyns: Vec<'ir, (Ref, Ref, TextRange)>, }, List { @@ -119,7 +117,7 @@ pub enum Ir<'ir, Ref> { body: Ref, thunks: Vec<'ir, (ThunkId, Ref)>, }, - WithLookup(SymId), + WithLookup(StringId), // Function related Func { @@ -137,7 +135,7 @@ pub enum Ir<'ir, Ref> { // Builtins Builtins, - Builtin(SymId), + Builtin(StringId), // Misc TopLevel { @@ -146,19 +144,26 @@ pub enum Ir<'ir, Ref> { }, Thunk(ThunkId), CurPos(TextRange), - ReplBinding(SymId), - ScopedImportBinding(SymId), + ReplBinding(StringId), + ScopedImportBinding(StringId), } #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] pub struct ThunkId(pub usize); -pub type SymId = SymbolU32; +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Collect)] +#[collect(require_static)] +pub struct StringId(pub SymbolU32); #[repr(transparent)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] -pub struct ArgId(pub usize); +pub struct ArgId(pub u32); + +#[repr(transparent)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)] +pub struct SpanId(pub u32); /// Represents a key in an attribute path. #[allow(unused)] @@ -169,7 +174,7 @@ pub enum Attr { Dynamic(Ref, TextRange), /// A static attribute key. /// Example: `attrs.key` - Str(SymId, TextRange), + Str(StringId, TextRange), } /// The kinds of binary operations supported in Nix. @@ -248,8 +253,8 @@ impl From for UnOpKind { /// Describes the parameters of a function. #[derive(Debug)] pub struct Param<'ir> { - pub required: Vec<'ir, (SymId, TextRange)>, - pub optional: Vec<'ir, (SymId, TextRange)>, + pub required: Vec<'ir, (StringId, TextRange)>, + pub optional: Vec<'ir, (StringId, TextRange)>, pub ellipsis: bool, } diff --git a/fix/src/lib.rs b/fix/src/lib.rs index 2db92f4..0599478 100644 --- a/fix/src/lib.rs +++ b/fix/src/lib.rs @@ -1,11 +1,12 @@ #![warn(clippy::unwrap_used)] +#![allow(dead_code)] -pub mod context; pub mod error; pub mod logging; +pub mod runtime; pub mod value; -mod bytecode; +mod codegen; mod derivation; mod disassembler; mod downgrade; @@ -13,7 +14,6 @@ mod downgrade; mod ir; mod nar; mod nix_utils; -mod runtime; mod store; mod string_context; diff --git a/fix/src/main.rs b/fix/src/main.rs index fe21d4c..ade11fd 100644 --- a/fix/src/main.rs +++ b/fix/src/main.rs @@ -3,9 +3,9 @@ use std::process::exit; use anyhow::Result; use clap::{Args, Parser, Subcommand}; -use hashbrown::HashSet; -use fix::context::Context; use fix::error::Source; +use fix::runtime::Runtime; +use hashbrown::HashSet; use rustyline::DefaultEditor; use rustyline::error::ReadlineError; @@ -40,7 +40,7 @@ struct ExprSource { file: Option, } -fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<()> { +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 { @@ -48,10 +48,11 @@ fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<( } else { unreachable!() }; - match context.compile_bytecode(src) { + todo!() + /* match runtime.compile_bytecode(src) { Ok(compiled) => { if !silent { - println!("{}", context.disassemble_colored(&compiled)); + println!("{}", runtime.disassemble_colored(&compiled)); } } Err(err) => { @@ -59,10 +60,10 @@ fn run_compile(context: &mut Context, src: ExprSource, silent: bool) -> Result<( exit(1); } }; - Ok(()) + Ok(()) */ } -fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> { +fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> { let src = if let Some(expr) = src.expr { Source::new_eval(expr)? } else if let Some(file) = src.file { @@ -70,7 +71,7 @@ fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> { } else { unreachable!() }; - match context.eval_deep(src) { + match runtime.eval_deep(src) { Ok(value) => { println!("{}", value.display_compat()); } @@ -82,7 +83,7 @@ fn run_eval(context: &mut Context, src: ExprSource) -> Result<()> { Ok(()) } -fn run_repl(context: &mut Context) -> Result<()> { +fn run_repl(runtime: &mut Runtime) -> Result<()> { let mut rl = DefaultEditor::new()?; let mut scope = HashSet::new(); const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$"); @@ -101,20 +102,20 @@ fn run_repl(context: &mut Context) -> Result<()> { eprintln!("Error: missing expression after '='"); continue; } - match context.add_binding(ident, expr, &mut scope) { + match runtime.add_binding(ident, expr, &mut scope) { Ok(value) => println!("{} = {}", ident, value), Err(err) => eprintln!("{:?}", miette::Report::new(*err)), } } else { let src = Source::new_repl(line)?; - match context.eval_repl(src, &scope) { + match runtime.eval_repl(src, &scope) { Ok(value) => println!("{value}"), Err(err) => eprintln!("{:?}", miette::Report::new(*err)), } } } else { let src = Source::new_repl(line)?; - match context.eval_shallow(src) { + match runtime.eval_repl(src, &scope) { Ok(value) => println!("{value}"), Err(err) => eprintln!("{:?}", miette::Report::new(*err)), } @@ -141,11 +142,11 @@ fn main() -> Result<()> { let cli = Cli::parse(); - let mut context = Context::new()?; + let mut runtime = Runtime::new()?; match cli.command { - Command::Compile { source, silent } => run_compile(&mut context, source, silent), - Command::Eval { source } => run_eval(&mut context, source), - Command::Repl => run_repl(&mut context), + Command::Compile { source, silent } => run_compile(&mut runtime, source, silent), + Command::Eval { source } => run_eval(&mut runtime, source), + Command::Repl => run_repl(&mut runtime), } } diff --git a/fix/src/runtime.rs b/fix/src/runtime.rs index 0a15cad..390f169 100644 --- a/fix/src/runtime.rs +++ b/fix/src/runtime.rs @@ -1 +1,531 @@ +use std::hash::BuildHasher; + +use bumpalo::Bump; +use gc_arena::{Arena, Rootable, arena::CollectionPhase}; +use ghost_cell::{GhostCell, GhostToken}; +use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable}; +use rnix::TextRange; +use string_interner::DefaultStringInterner; + +use crate::codegen::{BytecodeContext, InstructionPtr}; +use crate::downgrade::{Downgrade as _, DowngradeContext}; +use crate::error::{Error, Result, Source}; +use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq}; +use crate::runtime::builtins::new_builtins_env; +use crate::store::{DaemonStore, StoreConfig}; +use crate::value::Symbol; + +mod builtins; +mod stack; mod value; +mod vm; +use vm::{Action, VM}; + +pub struct Runtime { + bytecode: Vec, + global_env: HashMap>>, + sources: Vec, + store: DaemonStore, + spans: Vec<(usize, TextRange)>, + thunk_count: usize, + strings: DefaultStringInterner, + arena: Arena]>, +} + +impl Runtime { + const COLLECTOR_GRANULARITY: f64 = 1024.0; + + pub fn new() -> Result { + let mut strings = DefaultStringInterner::new(); + let global_env = new_builtins_env(&mut strings); + + let config = StoreConfig::from_env(); + let store = DaemonStore::connect(&config.daemon_socket)?; + + Ok(Self { + global_env, + store, + strings, + thunk_count: 0, + bytecode: Vec::new(), + sources: Vec::new(), + spans: Vec::new(), + arena: Arena::new(|mc| VM::new(mc)), + }) + } + + pub fn eval(&mut self, source: Source) -> Result { + let root = self.downgrade(source, None)?; + let ip = crate::codegen::compile_bytecode(root.as_ref(), self); + self.run(ip) + } + + pub fn eval_shallow(&mut self, _source: Source) -> Result { + todo!() + } + + pub fn eval_deep(&mut self, source: Source) -> Result { + // FIXME: deep + let root = self.downgrade(source, None)?; + let ip = crate::codegen::compile_bytecode(root.as_ref(), self); + self.run(ip) + } + + pub fn eval_repl( + &mut self, + source: Source, + scope: &HashSet, + ) -> Result { + // FIXME: shallow + let root = self.downgrade(source, Some(Scope::Repl(scope)))?; + let ip = crate::codegen::compile_bytecode(root.as_ref(), self); + self.run(ip) + } + + pub fn add_binding( + &mut self, + _ident: &str, + _expr: &str, + _scope: &mut HashSet, + ) -> Result { + todo!() + } + + fn downgrade_ctx<'a, 'bump, 'id>( + &'a mut self, + bump: &'bump Bump, + token: GhostToken<'id>, + extra_scope: Option>, + ) -> DowngradeCtx<'a, 'id, 'bump> { + let Runtime { + global_env, + sources, + thunk_count, + strings, + .. + } = self; + DowngradeCtx { + bump, + token, + strings, + source: sources.last().unwrap().clone(), + scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope.into_iter()).collect(), + with_scope_count: 0, + arg_count: 0, + thunk_count, + thunk_scopes: vec![ThunkScope::new_in(bump)], + } + } + + fn downgrade<'a>(&'a mut self, source: Source, extra_scope: Option>) -> Result { + tracing::debug!("Parsing Nix expression"); + + self.sources.push(source.clone()); + + let root = rnix::Root::parse(&source.src); + handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?; + + tracing::debug!("Downgrading Nix expression"); + let expr = root + .tree() + .expr() + .ok_or_else(|| Error::parse_error("unexpected EOF".into()))?; + let bump = Bump::new(); + GhostToken::new(|token| { + let ir = self + .downgrade_ctx(&bump, token, extra_scope) + .downgrade_toplevel(expr)?; + let ir = unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) }; + Ok(OwnedIr { _bump: bump, ir }) + }) + } + + fn run(&mut self, ip: InstructionPtr) -> Result { + let mut pc = ip.0; + loop { + let Runtime { + bytecode, + strings, + arena, + .. + } = self; + let action = + arena.mutate_root(|mc, root| root.run_batch(bytecode, &mut pc, mc, strings)); + match action { + Action::NeedGc => { + if self.arena.collection_phase() == CollectionPhase::Sweeping { + self.arena.collect_debt(); + } else if let Some(marked) = self.arena.mark_debt() { + marked.start_sweeping(); + } + } + Action::Done(done) => { + break done; + } + Action::Continue => (), + Action::IoRequest(_) => todo!(), + } + } + } +} + +fn parse_error_span(error: &rnix::ParseError) -> Option { + use rnix::ParseError::*; + match error { + Unexpected(range) + | UnexpectedExtra(range) + | UnexpectedWanted(_, range, _) + | UnexpectedDoubleBind(range) + | DuplicatedArgs(range, _) => Some(*range), + _ => None, + } +} + +fn handle_parse_error<'a>( + errors: impl IntoIterator, + source: Source, +) -> Option> { + for err in errors { + if let Some(span) = parse_error_span(err) { + return Some( + Error::parse_error(err.to_string()) + .with_source(source) + .with_span(span), + ); + } + } + None +} + +struct DowngradeCtx<'ctx, 'id, 'ir> { + bump: &'ir Bump, + token: GhostToken<'id>, + strings: &'ctx mut DefaultStringInterner, + source: Source, + scopes: Vec>, + with_scope_count: u32, + arg_count: u32, + thunk_count: &'ctx mut usize, + thunk_scopes: Vec>, +} + +fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool { + !matches!( + ir.borrow(token), + Ir::Builtin(_) + | Ir::Builtins + | Ir::Int(_) + | Ir::Float(_) + | Ir::Bool(_) + | Ir::Null + | Ir::Str(_) + | Ir::Thunk(_) + ) +} + +impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> { + fn new( + bump: &'ir Bump, + token: GhostToken<'id>, + symbols: &'ctx mut DefaultStringInterner, + global: &'ctx HashMap>>, + extra_scope: Option>, + thunk_count: &'ctx mut usize, + source: Source, + ) -> Self { + Self { + bump, + token, + strings: symbols, + source, + scopes: std::iter::once(Scope::Global(global)) + .chain(extra_scope) + .collect(), + thunk_count, + arg_count: 0, + with_scope_count: 0, + thunk_scopes: vec![ThunkScope::new_in(bump)], + } + } +} + +impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> { + fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> { + IrRef::new(self.bump.alloc(GhostCell::new(expr))) + } + + fn new_arg(&mut self) -> ArgId { + self.arg_count += 1; + ArgId(self.arg_count - 1) + } + + fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> { + if !should_thunk(ir, &self.token) { + return ir; + } + + let cached = self + .thunk_scopes + .last() + .expect("no active cache scope") + .lookup_cache(ir, &self.token); + + if let Some(id) = cached { + return IrRef::alloc(self.bump, Ir::Thunk(id)); + } + + let id = ThunkId(*self.thunk_count); + *self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow"); + self.thunk_scopes + .last_mut() + .expect("no active cache scope") + .add_binding(id, ir, &self.token); + IrRef::alloc(self.bump, Ir::Thunk(id)) + } + + fn new_sym(&mut self, sym: String) -> StringId { + StringId(self.strings.get_or_intern(sym)) + } + + fn get_sym(&self, id: StringId) -> Symbol<'_> { + self.strings.resolve(id.0).expect("no symbol found").into() + } + + fn lookup(&self, sym: StringId, span: TextRange) -> Result> { + for scope in self.scopes.iter().rev() { + match scope { + &Scope::Global(global_scope) => { + if let Some(expr) = global_scope.get(&sym) { + let ir = match expr { + Ir::Builtins => Ir::Builtins, + Ir::Builtin(s) => Ir::Builtin(*s), + Ir::Bool(b) => Ir::Bool(*b), + Ir::Null => Ir::Null, + _ => unreachable!("globals should only contain leaf IR nodes"), + }; + return Ok(self.new_expr(ir)); + } + } + &Scope::Repl(repl_bindings) => { + if repl_bindings.contains(&sym) { + return Ok(self.new_expr(Ir::ReplBinding(sym))); + } + } + Scope::ScopedImport(scoped_bindings) => { + if scoped_bindings.contains(&sym) { + return Ok(self.new_expr(Ir::ScopedImportBinding(sym))); + } + } + Scope::Let(let_scope) => { + if let Some(&expr) = let_scope.get(&sym) { + return Ok(self.new_expr(Ir::Thunk(expr))); + } + } + &Scope::Param(param_sym, id) => { + if param_sym == sym { + return Ok(self.new_expr(Ir::Arg(id))); + } + } + } + } + + if self.with_scope_count > 0 { + Ok(self.new_expr(Ir::WithLookup(sym))) + } else { + Err(Error::downgrade_error( + format!("'{}' not found", self.get_sym(sym)), + self.get_current_source(), + span, + )) + } + } + + fn get_current_source(&self) -> Source { + self.source.clone() + } + + fn with_let_scope(&mut self, keys: &[StringId], f: F) -> Result + where + F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>, + { + let base = *self.thunk_count; + *self.thunk_count = self + .thunk_count + .checked_add(keys.len()) + .expect("thunk id overflow"); + let iter = keys.iter().enumerate().map(|(offset, &key)| { + ( + key, + ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }), + ) + }); + self.scopes.push(Scope::Let(iter.collect())); + let (vals, ret) = { + let mut guard = ScopeGuard { ctx: self }; + f(guard.as_ctx())? + }; + assert_eq!(keys.len(), vals.len()); + let scope = self.thunk_scopes.last_mut().expect("no active thunk scope"); + scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals)); + Ok(ret) + } + + fn with_param_scope(&mut self, param: StringId, arg: ArgId, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.scopes.push(Scope::Param(param, arg)); + let mut guard = ScopeGuard { ctx: self }; + f(guard.as_ctx()) + } + + fn with_with_scope(&mut self, f: F) -> R + where + F: FnOnce(&mut Self) -> R, + { + self.with_scope_count += 1; + let ret = f(self); + self.with_scope_count -= 1; + ret + } + + fn with_thunk_scope( + &mut self, + f: F, + ) -> ( + R, + bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>, + ) + where + F: FnOnce(&mut Self) -> R, + { + self.thunk_scopes.push(ThunkScope::new_in(self.bump)); + let ret = f(self); + ( + ret, + self.thunk_scopes + .pop() + .expect("no thunk scope left???") + .bindings, + ) + } + + fn bump(&self) -> &'ir bumpalo::Bump { + self.bump + } +} + +impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> { + fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result> { + let body = root.downgrade(&mut self)?; + let thunks = self + .thunk_scopes + .pop() + .expect("no thunk scope left???") + .bindings; + let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks }); + Ok(ir.freeze(self.token)) + } +} + +struct ThunkScope<'id, 'ir> { + bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>, + cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>, + hasher: DefaultHashBuilder, +} + +impl<'id, 'ir> ThunkScope<'id, 'ir> { + fn new_in(bump: &'ir Bump) -> Self { + Self { + bindings: bumpalo::collections::Vec::new_in(bump), + cache: HashTable::new(), + hasher: DefaultHashBuilder::default(), + } + } + + fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option { + let hash = self.hasher.hash_one(IrKey(key, token)); + self.cache + .find(hash, |&(ir, _)| ir_content_eq(key, ir, token)) + .map(|&(_, id)| id) + } + + fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) { + self.bindings.push((id, ir)); + let hash = self.hasher.hash_one(IrKey(ir, token)); + self.cache.insert_unique(hash, (ir, id), |&(ir, _)| { + self.hasher.hash_one(IrKey(ir, token)) + }); + } + + fn extend_bindings(&mut self, iter: impl IntoIterator)>) { + self.bindings.extend(iter); + } +} + +enum Scope<'ctx> { + Global(&'ctx HashMap>>), + Repl(&'ctx HashSet), + ScopedImport(HashSet), + Let(HashMap), + Param(StringId, ArgId), +} + +struct ScopeGuard<'a, 'ctx, 'id, 'ir> { + ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>, +} + +impl Drop for ScopeGuard<'_, '_, '_, '_> { + fn drop(&mut self) { + self.ctx.scopes.pop(); + } +} + +impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> { + fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> { + self.ctx + } +} + +struct OwnedIr { + _bump: Bump, + ir: RawIrRef<'static>, +} + +impl OwnedIr { + unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self { + Self { + _bump: bump, + ir: unsafe { std::mem::transmute::, RawIrRef<'static>>(ir) } + } + } + + fn as_ref(&self) -> RawIrRef<'_> { + self.ir + } +} + +impl BytecodeContext for Runtime { + fn intern_string(&mut self, s: &str) -> StringId { + StringId(self.strings.get_or_intern(s)) + } + + fn register_span(&mut self, range: TextRange) -> u32 { + let id = self.spans.len(); + let source_id = self + .sources + .len() + .checked_sub(1) + .expect("current_source not set"); + self.spans.push((source_id, range)); + id as u32 + } + + fn get_code(&self) -> &[u8] { + &self.bytecode + } + + fn get_code_mut(&mut self) -> &mut Vec { + &mut self.bytecode + } +} diff --git a/fix/src/runtime/builtins.rs b/fix/src/runtime/builtins.rs new file mode 100644 index 0000000..548c391 --- /dev/null +++ b/fix/src/runtime/builtins.rs @@ -0,0 +1,51 @@ +use hashbrown::HashMap; +use string_interner::DefaultStringInterner; + +use crate::ir::{Ir, RawIrRef, StringId}; + +pub(super) fn new_builtins_env( + interner: &mut DefaultStringInterner, +) -> HashMap>> { + let mut builtins = HashMap::new(); + let builtins_sym = StringId(interner.get_or_intern("builtins")); + builtins.insert(builtins_sym, Ir::Builtins); + + let free_globals = [ + "abort", + "baseNameOf", + "break", + "dirOf", + "derivation", + "derivationStrict", + "fetchGit", + "fetchMercurial", + "fetchTarball", + "fetchTree", + "fromTOML", + "import", + "isNull", + "map", + "placeholder", + "removeAttrs", + "scopedImport", + "throw", + "toString", + ]; + let consts = [ + ("true", Ir::Bool(true)), + ("false", Ir::Bool(false)), + ("null", Ir::Null), + ]; + + for name in free_globals { + let name = StringId(interner.get_or_intern(name)); + let value = Ir::Builtin(name); + builtins.insert(name, value); + } + for (name, value) in consts { + let name = StringId(interner.get_or_intern(name)); + builtins.insert(name, value); + } + + builtins +} diff --git a/fix/src/runtime/stack.rs b/fix/src/runtime/stack.rs new file mode 100644 index 0000000..fc43452 --- /dev/null +++ b/fix/src/runtime/stack.rs @@ -0,0 +1,89 @@ +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; + fn trace>(&self, cc: &mut U) { + for item in self.inner[..self.len].iter() { + unsafe { + item.assume_init_ref().trace(cc); + } + } + } +} + +impl Default for Stack { + fn default() -> Self { + Self::new() + } +} + +impl Stack { + pub(super) fn new() -> Self { + Self { + inner: Box::new([const { MaybeUninit::uninit() }; N]), + len: 0, + } + } + + pub(super) fn push(&mut self, val: T) -> Result<(), T> { + if self.len == N { + return Err(val); + } + unsafe { + self.inner.get_unchecked_mut(self.len).write(val); + } + self.len += 1; + Ok(()) + } + + pub(super) fn pop(&mut self) -> Option { + if self.len == 0 { + return None; + } + let ret = unsafe { + self.inner + .get_unchecked_mut(self.len - 1) + .assume_init_read() + }; + self.len -= 1; + Some(ret) + } + + pub(super) fn tos(&self) -> Option<&T> { + if self.len == 0 { + return None; + } + Some(unsafe { self.inner.get_unchecked(self.len - 1).assume_init_ref() }) + } + + pub(super) fn tos_mut(&mut self) -> Option<&mut T> { + if self.len == 0 { + return None; + } + Some(unsafe { self.inner.get_unchecked_mut(self.len - 1).assume_init_mut() }) + } + + pub(super) fn len(&self) -> usize { + self.len + } + + pub(super) fn pop_n(&mut self, n: usize) -> SmallVec<[T; M]> { + assert!(n <= self.len, "pop_n: not enough items on stack"); + let mut result = SmallVec::new(); + let start = self.len - n; + for i in start..self.len { + result.push(unsafe { self.inner.get_unchecked(i).assume_init_read() }); + } + self.len = start; + result + } +} diff --git a/fix/src/runtime/value.rs b/fix/src/runtime/value.rs index 3aa4f30..cfa7e12 100644 --- a/fix/src/runtime/value.rs +++ b/fix/src/runtime/value.rs @@ -1,68 +1,121 @@ use std::fmt; 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}; -use hashbrown::HashTable; +use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace}; +use sealed::sealed; +use smallvec::SmallVec; +use string_interner::{Symbol, symbol::SymbolU32}; -trait Storable { +use crate::ir::StringId; + +#[sealed] +pub(crate) trait Storable { const TAG: (bool, u8); } -trait InlineStorable: Storable + RawStore {} -trait GcStorable: Storable {} +pub(crate) trait InlineStorable: Storable + RawStore {} +pub(crate) trait GcStorable: Storable {} -macro_rules! inline_types { - ($($type:ty => $id:expr),*$(,)?) => { +macro_rules! define_value_types { + ( + inline { $($itype:ty => $itag:expr, $iname:literal;)* } + gc { $($gtype:ty => $gtag:expr, $gname:literal;)* } + ) => { $( - impl Storable for $type { - const TAG: (bool, u8) = (false, $id); + #[sealed] + impl Storable for $itype { + const TAG: (bool, u8) = $itag; } - impl InlineStorable for $type {} + impl InlineStorable for $itype {} )* - }; -} -macro_rules! gc_types { - ($($type:ty => $id:expr),*$(,)?) => { $( - impl Storable for $type { - const TAG: (bool, u8) = (true, $id); + #[sealed] + impl Storable for $gtype { + const TAG: (bool, u8) = $gtag; } - impl GcStorable for $type {} + impl GcStorable for $gtype {} )* + + const _: () = assert!(size_of::>() == 8); + $(const _: () = assert!(size_of::<$itype>() <= 6);)* + $(const _: () = { let (_, val) = $itag; assert!(val >= 1 && val <= 7); };)* + $(const _: () = { let (_, val) = $gtag; assert!(val >= 1 && val <= 7); };)* + + const _: () = { + let tags: &[(bool, u8)] = &[$($itag),*, $($gtag),*]; + let mut mask_false: u8 = 0; + let mut mask_true: u8 = 0; + let mut i = 0; + while i < tags.len() { + let (neg, val) = tags[i]; + let bit = 1 << val; + if neg { + assert!(mask_true & bit == 0, "duplicate true tag id"); + mask_true |= bit; + } else { + assert!(mask_false & bit == 0, "duplicate false tag id"); + mask_false |= bit; + } + i += 1; + } + }; + + unsafe impl<'gc> Collect<'gc> for Value<'gc> { + const NEEDS_TRACE: bool = true; + fn trace>(&self, cc: &mut T) { + let Some(tag) = self.raw.tag() else { return }; + match tag.neg_val() { + $(<$gtype as Storable>::TAG => unsafe { + self.load_gc::<$gtype>().trace(cc) + },)* + $(<$itype as Storable>::TAG => (),)* + (neg, val) => unreachable!("invalid tag: neg={neg}, val={val}"), + } + } + } + + impl fmt::Debug for Value<'_> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.tag() { + None => write!(f, "Float({:?})", unsafe { + self.raw.float().unwrap_unchecked() + }), + $(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe { + self.as_inline::<$itype>().unwrap_unchecked() + }),)* + $(Some(<$gtype as Storable>::TAG) => + write!(f, "{}({:?})", $gname, unsafe { self.as_gc::<$gtype>().unwrap_unchecked() }),)* + Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"), + } + } + } }; } -inline_types! { - i32 => 1, - bool => 2, - Null => 3, - SmallStringId => 4, -} -gc_types! { - i64 => 1, - NixString => 2, - SmallAttrSet<'_> => 3, - AttrSet<'_> => 4, - Box<[Value<'_>]> => 5, +define_value_types! { + inline { + i32 => (false, 1), "SmallInt"; + bool => (false, 2), "Bool"; + Null => (false, 3), "Null"; + StringId => (false, 4), "SmallString"; + PrimOp => (false, 5), "PrimOp"; + } + gc { + i64 => (false, 6), "BigInt"; + NixString => (false, 7), "String"; + AttrSet<'_> => (true, 1), "AttrSet"; + List<'_> => (true, 2), "List"; + Thunk<'_> => (true, 3), "Thunk"; + Closure<'_> => (true, 4), "Closure"; + PrimOpApp<'_> => (true, 5), "PrimOpApp"; + } } /// # Nix runtime value representation /// -/// NaN-boxed value fitting in 8 bytes. Morally equivalent to: -/// ```ignore -/// enum Value<'gc> { -/// Float(SingleNaNF64), -/// SmallInt(i32), -/// BigInt(Gc<'gc, i64>), -/// Bool(bool), -/// Null, -/// SmallString(SmallStringId), -/// String(Gc<'gc, NixString>), -/// SmallAttrSet(Gc<'gc, SmallAttrSet<'gc>>), -/// AttrSet(Gc<'gc, AttrSet<'gc>>), -/// List(Gc<'gc, Box<[Value<'gc>]>>), -/// } -/// ``` +/// NaN-boxed value fitting in 8 bytes. #[repr(transparent)] pub(crate) struct Value<'gc> { raw: RawBox, @@ -79,90 +132,16 @@ impl Clone for Value<'_> { } } -macro_rules! trace_impl { - ($self:expr, $cc:expr, $tag:expr; $($type:ty),*$(,)?) => { - match $tag.neg_val() { - // Positive tags are inline data - skip it. - (false, _) => (), - - // Negative tags are Gc pointers - reconstruct and trace it. - $( - <$type as Storable>::TAG => unsafe { $self.load_gc::<$type>().trace($cc) }, - )* - - (true, val) => unreachable!("invalid negative tag value: {val}"), - } - }; -} - -unsafe impl<'gc> Collect for Value<'gc> { - fn trace(&self, cc: &gc_arena::Collection) { - // No tag - raw float - let Some(tag) = self.raw.tag() else { return }; - trace_impl! { - self, cc, tag; - i64, - NixString, - SmallAttrSet<'gc>, - AttrSet<'gc>, - Box<[Value<'gc>]>, - } - } - - fn needs_trace() -> bool - where - Self: Sized, - { - true - } -} - -macro_rules! debug_impl { - ($self:expr; $($type:ty),*$(,)?) => { - match $self.tag() { - None => write!(f, "Float({:?})", unsafe { - self.raw.float().unwrap_unchecked() - }), - - $( - Some() - )* - } - }; -} - -impl fmt::Debug for Value<'_> { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.tag() { - None => write!(f, "Float({:?})", unsafe { - self.raw.float().unwrap_unchecked() - }), - Some(i32::TAG) => write!(f, "SmallInt({:?})", unsafe { - self.as_inline::().unwrap_unchecked() - }), - Some(bool::TAG) => write!(f, "Bool({:?})", unsafe { - self.as_inline::().unwrap_unchecked() - }), - Some(Null::TAG) => write!(f, "Null"), - Some(SmallStringId::TAG) => { - write!(f, "SmallString({:?})", unsafe { - self.as_inline::().unwrap_unchecked().0 - }) - } - Some(i64::TAG) => write!(f, "BigInt(Gc<..>)"), - Some(NixString::TAG) => write!(f, "String(Gc<..>)"), - Some(SmallAttrSet::TAG) => write!(f, "SmallAttrSet(Gc<..>)"), - Some(AttrSet::TAG) => write!(f, "AttrSet(Gc<..>)"), - Some(]> as Storable>::TAG) => write!(f, "List(Gc<..>)"), - Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"), - } +impl Default for Value<'_> { + #[inline(always)] + fn default() -> Self { + Self::new_inline(Null) } } impl<'gc> Value<'gc> { #[inline(always)] fn mk_tag(neg: bool, val: u8) -> RawTag { - debug_assert!((1..=7).contains(&val)); // Safety: val is asserted to be in 1..=7. unsafe { RawTag::new_unchecked(neg, val) } } @@ -175,20 +154,13 @@ impl<'gc> Value<'gc> { } } - /// Store a GC pointer with the given (negative) tag value. - #[inline(always)] - fn store_gc(tag_val: u8, gc: Gc<'gc, T>) -> Self { - let ptr = Gc::as_ptr(gc); - Self::from_raw_value(RawValue::store(Self::mk_tag(true, tag_val), ptr)) - } - /// Load a GC pointer from a value with a negative tag. /// /// # Safety /// /// The value must actually store a `Gc<'gc, T>` with the matching type. #[inline(always)] - unsafe fn load_gc(&self) -> Gc<'gc, T> { + unsafe fn load_gc(&self) -> Gc<'gc, T> { unsafe { let rv = self.raw.value().unwrap_unchecked(); let ptr: *const T = <*const T as RawStore>::from_val(rv); @@ -214,13 +186,13 @@ impl<'gc> Value<'gc> { #[inline] pub(crate) fn new_inline(val: T) -> Self { - Self::from_raw_value(RawValue::store(Self::mk_tag(false, T::TAG.1), val)) + Self::from_raw_value(RawValue::store(Self::mk_tag(T::TAG.0, T::TAG.1), val)) } #[inline] pub(crate) fn new_gc(gc: Gc<'gc, T>) -> Self { let ptr = Gc::as_ptr(gc); - Self::from_raw_value(RawValue::store(Self::mk_tag(true, T::TAG.1), ptr)) + Self::from_raw_value(RawValue::store(Self::mk_tag(T::TAG.0, T::TAG.1), ptr)) } } @@ -268,6 +240,7 @@ impl<'gc> Value<'gc> { } } +#[derive(Clone, Copy, Debug)] pub(crate) struct Null; impl RawStore for Null { fn to_val(self, value: &mut RawValue) { @@ -278,17 +251,15 @@ impl RawStore for Null { } } -// TODO: size? -#[repr(transparent)] -#[derive(Clone, Copy, PartialEq, Eq, Debug, Collect)] -#[collect(require_static)] -pub(crate) struct SmallStringId(u32); -impl RawStore for SmallStringId { +impl RawStore for StringId { fn to_val(self, value: &mut RawValue) { - self.0.to_val(value); + (self.0.to_usize() as u32).to_val(value); } fn from_val(value: &RawValue) -> Self { - Self(u32::from_val(value)) + Self( + SymbolU32::try_from_usize(u32::from_val(value) as usize) + .expect("failed to read StringId from Value"), + ) } } @@ -320,45 +291,199 @@ impl fmt::Debug for NixString { } } -/// Fixed-size attribute set (up to 8 entries). -#[derive(Collect)] +#[derive(Collect, Debug)] #[collect(no_drop)] -pub(crate) struct SmallAttrSet<'gc> { - // TODO: proper key storage, length tracking, and lookup - inner: [Value<'gc>; 8], -} - -/// Hash-table-backed attribute set. pub(crate) struct AttrSet<'gc> { - inner: HashTable>, + pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>, } -unsafe impl<'gc> Collect for AttrSet<'gc> { - fn trace(&self, cc: &gc_arena::Collection) { - for entry in self.inner.iter() { - Collect::trace(&entry.key, cc); - Collect::trace(&entry.value, cc); +impl<'gc> AttrSet<'gc> { + pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self { + debug_assert!(entries.is_sorted_by_key(|(key, _)| *key)); + Self { entries } + } + + pub(crate) fn lookup(&self, key: StringId) -> Option> { + self.entries + .binary_search_by_key(&key, |(k, _)| *k) + .ok() + .map(|i| self.entries[i].1.clone()) + } + + pub(crate) fn has(&self, key: StringId) -> bool { + self.entries + .binary_search_by_key(&key, |(k, _)| *k) + .is_ok() + } + + pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> { + use std::cmp::Ordering::*; + + debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key)); + debug_assert!(other.entries.is_sorted_by_key(|(key, _)| *key)); + + let mut entries = SmallVec::new(); + let mut i = 0; + let mut j = 0; + 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()); + i += 1; + } + Greater => { + entries.push(other.entries[j].clone()); + j += 1; + } + Equal => { + entries.push(other.entries[j].clone()); + i += 1; + j += 1; + } + } + } + entries.extend(other.entries[j..].iter().cloned()); + entries.extend(self.entries[i..].iter().cloned()); + + debug_assert!(entries.is_sorted_by_key(|(key, _)| *key)); + + Gc::new(mc, AttrSet { entries }) + } +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub(crate) struct List<'gc> { + pub(crate) inner: SmallVec<[Value<'gc>; 4]>, +} + +pub(crate) type Thunk<'gc> = RefLock>; + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub(crate) enum ThunkState<'gc> { + Pending { + ip: u32, + env: Gc<'gc, RefLock>>, + }, + Blackhole, + Evaluated(Value<'gc>), +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub(crate) struct Env<'gc> { + pub(crate) locals: SmallVec<[Value<'gc>; 4]>, + pub(crate) prev: Option>>>, +} + +impl<'gc> Env<'gc> { + pub(crate) fn empty() -> Self { + Env { + locals: SmallVec::new(), + prev: None, } } - fn needs_trace() -> bool - where - Self: Sized, - { - true + pub(crate) fn with_arg( + arg: Value<'gc>, + n_locals: u32, + prev: Gc<'gc, RefLock>>, + ) -> Self { + let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize]; + locals[0] = arg; + Env { + locals, + prev: Some(prev), + } } } -#[derive(Collect)] +#[derive(Collect, Debug)] #[collect(no_drop)] -struct AttrSetEntry<'gc> { - key: AttrKey<'gc>, - value: Value<'gc>, +pub(crate) struct Closure<'gc> { + pub(crate) ip: u32, + pub(crate) n_locals: u32, + pub(crate) env: Gc<'gc, RefLock>>, + pub(crate) pattern: Option>, } -#[derive(Collect)] -#[collect(no_drop)] -pub(crate) enum AttrKey<'gc> { - Small(SmallStringId), - Large(Gc<'gc, str>), +#[derive(Collect, Debug)] +#[collect(require_static)] +pub(crate) struct PatternInfo { + pub(crate) required: SmallVec<[StringId; 4]>, + pub(crate) optional: SmallVec<[StringId; 4]>, + pub(crate) ellipsis: bool, + pub(crate) param_spans: Box<[(StringId, u32)]>, +} + +#[derive(Clone, Copy, Debug, Collect)] +#[collect(require_static)] +pub(crate) struct PrimOp { + pub(crate) id: u8, + 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]); + } + fn from_val(value: &RawValue) -> Self { + let [.., id, arity] = *value.data(); + Self { id, arity } + } +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub(crate) struct PrimOpApp<'gc> { + pub(crate) primop: PrimOp, + pub(crate) args: SmallVec<[Value<'gc>; 2]>, +} + +#[repr(transparent)] +pub(crate) struct StrictValue<'gc>(Value<'gc>); + +impl<'gc> StrictValue<'gc> { + #[inline] + pub(crate) fn try_from_forced(val: Value<'gc>) -> Option { + if !val.is::>() { + Some(Self(val)) + } else { + None + } + } + + #[inline] + pub(crate) fn into_relaxed(self) -> Value<'gc> { + self.0 + } +} + +impl<'gc> Deref for StrictValue<'gc> { + type Target = Value<'gc>; + #[inline] + fn deref(&self) -> &Value<'gc> { + &self.0 + } +} + +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) + } +} + +unsafe impl<'gc> Collect<'gc> for StrictValue<'gc> { + const NEEDS_TRACE: bool = true; + fn trace>(&self, cc: &mut T) { + self.0.trace(cc); + } } diff --git a/fix/src/runtime/vm.rs b/fix/src/runtime/vm.rs new file mode 100644 index 0000000..a884aa4 --- /dev/null +++ b/fix/src/runtime/vm.rs @@ -0,0 +1,1849 @@ +use std::path::PathBuf; + +use gc_arena::{Collect, Gc, Mutation, RefLock}; +use hashbrown::HashMap; +use num_enum::TryFromPrimitive; +use smallvec::SmallVec; +use string_interner::{DefaultStringInterner, Symbol as _}; + +use super::stack::Stack; +use super::value::*; +use crate::error::{Error, Result}; +use crate::ir::StringId; + +type VmResult = std::result::Result; + +enum VmError { + Catchable(String), + Uncatchable(Box), +} + +impl From> for VmError { + fn from(e: Box) -> Self { + VmError::Uncatchable(e) + } +} + +#[derive(Collect)] +#[collect(no_drop)] +pub(super) struct VM<'gc> { + stack: Stack<65536, Value<'gc>>, + frames: Stack<8192, CallFrame<'gc>>, + with_scope: Option>>, + error_contexts: Stack<8192, ErrorFrame>, + globals: Gc<'gc, GlobalState<'gc>>, + import_cache: HashMap>, + pc: usize, + current_env: Option>>>, + started: bool, +} + +#[derive(Collect)] +#[collect(no_drop)] +struct GlobalState<'gc> { + builtins: Value<'gc>, +} + +#[derive(Collect)] +#[collect(no_drop)] +struct ErrorFrame { + span_id: u32, + message: Option, +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +struct WithScope<'gc> { + env: Value<'gc>, + prev: Option>>, +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +struct CallFrame<'gc> { + pc: usize, + env: Gc<'gc, RefLock>>, + continuation: Continuation<'gc>, + span: Option, +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +enum Continuation<'gc> { + Return, + ForceThunk { + thunk: Gc<'gc, Thunk<'gc>>, + after: AfterForce<'gc>, + }, +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +enum AfterForce<'gc> { + Identity, + ForceBool, + BinOpLhs { + rhs: Value<'gc>, + op: BinOpTag, + }, + BinOpRhs { + lhs_forced: StrictValue<'gc>, + op: BinOpTag, + }, + UnNeg, + UnNot, + Call { + arg: Value<'gc>, + span: Option, + }, + PatternCallArgForce { + func: StrictValue<'gc>, + span: Option, + }, + Select { + keys: SmallVec<[Value<'gc>; 4]>, + remaining: u16, + span: u32, + default: Option>, + }, + HasAttr { + keys: SmallVec<[Value<'gc>; 4]>, + remaining: u16, + }, + Assert { + expr: Value<'gc>, + raw_idx: u32, + span_id: u32, + }, + ConcatStrings { + forced: SmallVec<[StrictValue<'gc>; 8]>, + remaining: SmallVec<[Value<'gc>; 8]>, + force_string: bool, + }, + PushWith, + WithLookup { + name: StringId, + next: Option>>, + }, + TopLevelForce, +} + +#[derive(Clone, Copy, Debug, Collect)] +#[collect(require_static)] +enum BinOpTag { + Add, + Sub, + Mul, + Div, + Eq, + Neq, + Lt, + Gt, + Leq, + Geq, + Concat, + Update, +} + +enum ForceResult<'gc> { + Ready(StrictValue<'gc>), + NeedEval { + ip: u32, + env: Gc<'gc, RefLock>>, + thunk: Gc<'gc, Thunk<'gc>>, + }, +} + +pub(crate) enum Action { + Continue, + Done(Result), + NeedGc, + IoRequest(()), +} + +enum NixNum { + Int(i64), + Float(f64), +} + +macro_rules! try_vm { + ($expr:expr) => { + match $expr { + Ok(v) => v, + Err(e) => return VM::vm_err_to_action(e), + } + }; +} + +impl<'gc> VM<'gc> { + pub(super) fn new(mc: &Mutation<'gc>) -> Self { + Self { + stack: Stack::new(), + frames: Stack::new(), + with_scope: None, + error_contexts: Stack::new(), + globals: Gc::new( + mc, + GlobalState { + builtins: Value::new_inline(Null), + }, + ), + import_cache: HashMap::new(), + pc: 0, + current_env: None, + started: false, + } + } + + #[inline(always)] + fn read_u8(&mut self, bc: &[u8]) -> u8 { + let v = bc[self.pc]; + self.pc += 1; + v + } + + #[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 + } + + #[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 + } + + #[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 + } + + #[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 + } + + #[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 + } + + #[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()) + } + + #[inline(always)] + fn env(&self) -> Gc<'gc, RefLock>> { + self.current_env.expect("no current env") + } + + 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 thunk_ref = thunk_gc.borrow(); + match &*thunk_ref { + ThunkState::Evaluated(v) => { + current = v.clone(); + drop(thunk_ref); + } + ThunkState::Pending { ip, env } => { + return Ok(ForceResult::NeedEval { + ip: *ip, + env: *env, + thunk: thunk_gc, + }); + } + ThunkState::Blackhole => { + return Err(VmError::Uncatchable(Error::eval_error( + "infinite recursion encountered".into(), + None, + ))); + } + } + } + } + + fn push_force_frame( + &mut self, + thunk: Gc<'gc, Thunk<'gc>>, + after: AfterForce<'gc>, + ip: u32, + env: Gc<'gc, RefLock>>, + mc: &Mutation<'gc>, + ) { + self.frames + .push(CallFrame { + pc: self.pc, + env: self.env(), + continuation: Continuation::ForceThunk { thunk, after }, + span: None, + }) + .expect("frame stack overflow"); + *thunk.borrow_mut(mc) = ThunkState::Blackhole; + self.pc = ip as usize; + self.current_env = Some(env); + } + + fn make_int(val: i64, mc: &Mutation<'gc>) -> Value<'gc> { + if val >= i32::MIN as i64 && val <= i32::MAX as i64 { + Value::new_inline(val as i32) + } else { + Value::new_gc(Gc::new(mc, val)) + } + } + + fn as_num(val: StrictValue<'gc>) -> Option { + if let Some(i) = val.as_inline::() { + Some(NixNum::Int(i as i64)) + } else if let Some(gc_i) = val.as_gc::() { + Some(NixNum::Int(*gc_i)) + } else { + val.as_float().map(NixNum::Float) + } + } + + fn get_string_id(val: &Value<'gc>) -> Option { + val.as_inline::() + } + + fn get_string(val: StrictValue<'gc>, strings: &DefaultStringInterner) -> Option { + if let Some(sid) = val.as_inline::() { + Some(strings.resolve(sid.0)?.to_owned()) + } else { + val.as_gc::().map(|ns| ns.as_str().to_owned()) + } + } + + fn err(msg: impl Into) -> VmError { + VmError::Uncatchable(Error::eval_error(msg.into(), None)) + } + + 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( + &mut self, + ret_val: Value<'gc>, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> Action { + let Some(frame) = self.frames.pop() else { + return match self.force_inline(ret_val) { + Ok(ForceResult::Ready(v)) => Action::Done(Ok(self.convert_value(&v, strings))), + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame(thunk, AfterForce::TopLevelForce, ip, env, mc); + Action::Continue + } + Err(e) => Self::vm_err_to_action(e), + }; + }; + + self.pc = frame.pc; + self.current_env = Some(frame.env); + + match frame.continuation { + Continuation::Return => { + self.stack.push(ret_val).expect("stack overflow"); + Action::Continue + } + Continuation::ForceThunk { thunk, after } => { + *thunk.borrow_mut(mc) = ThunkState::Evaluated(ret_val.clone()); + 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); + Action::Continue + } + Err(e) => Self::vm_err_to_action(e), + } + } + } + } + + fn resume_after_force( + &mut self, + val: StrictValue<'gc>, + after: AfterForce<'gc>, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> Action { + match after { + AfterForce::Identity => { + self.stack.push(val.into_relaxed()).expect("stack overflow"); + Action::Continue + } + AfterForce::ForceBool => { + if val.as_inline::().is_none() { + return Self::vm_err_to_action(Self::err("value is not a boolean")); + } + self.stack.push(val.into_relaxed()).expect("stack overflow"); + 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"); + Action::Continue + } + Err(e) => Self::vm_err_to_action(e), + } + } + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame( + thunk, + AfterForce::BinOpRhs { + lhs_forced: val, + op, + }, + ip, + env, + mc, + ); + Action::Continue + } + Err(e) => Self::vm_err_to_action(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"); + Action::Continue + } + Err(e) => Self::vm_err_to_action(e), + } + } + AfterForce::UnNeg => match Self::as_num(val) { + Some(NixNum::Int(i)) => { + self.stack + .push(Self::make_int(-i, mc)) + .expect("stack overflow"); + Action::Continue + } + Some(NixNum::Float(f)) => { + self.stack + .push(Value::new_float(-f)) + .expect("stack overflow"); + Action::Continue + } + None => Self::vm_err_to_action(Self::err("cannot negate non-number")), + }, + AfterForce::UnNot => match val.as_inline::() { + Some(b) => { + self.stack + .push(Value::new_inline(!b)) + .expect("stack overflow"); + Action::Continue + } + None => Self::vm_err_to_action(Self::err("value is not a boolean")), + }, + AfterForce::Call { arg, span } => match self.do_call(val, arg, span, mc) { + Ok(()) => Action::Continue, + Err(e) => Self::vm_err_to_action(e), + }, + AfterForce::PatternCallArgForce { func, span } => { + if let Some(closure_gc) = func.as_gc::>() { + let pattern = closure_gc.pattern.as_ref().unwrap(); + match self.setup_pattern_call( + closure_gc.ip, + closure_gc.n_locals, + closure_gc.env, + pattern, + val, + span, + mc, + ) { + Ok(()) => Action::Continue, + Err(e) => Self::vm_err_to_action(e), + } + } else { + Self::vm_err_to_action(Self::err("internal: pattern call on non-closure")) + } + } + AfterForce::Select { + keys, + remaining, + span, + default, + } => match self.do_select_step(val, keys, remaining, span, default, mc, strings) { + Ok(()) => Action::Continue, + Err(e) => Self::vm_err_to_action(e), + }, + AfterForce::HasAttr { keys, remaining } => { + match self.do_has_attr_step(val, keys, remaining, mc) { + Ok(()) => Action::Continue, + Err(e) => Self::vm_err_to_action(e), + } + } + AfterForce::Assert { + expr, + raw_idx, + span_id: _, + } => match val.as_inline::() { + Some(true) => { + self.stack.push(expr).expect("stack overflow"); + 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, + ))) + } + None => Self::vm_err_to_action(Self::err("assertion condition must be a boolean")), + }, + AfterForce::ConcatStrings { + mut forced, + remaining, + force_string, + } => { + forced.push(val); + match self.concat_strings_continue(forced, remaining, force_string, mc, strings) { + Ok(()) => Action::Continue, + Err(e) => Self::vm_err_to_action(e), + } + } + AfterForce::PushWith => { + let scope = Gc::new( + mc, + WithScope { + env: val.into_relaxed(), + prev: self.with_scope, + }, + ); + self.with_scope = Some(scope); + Action::Continue + } + AfterForce::WithLookup { name, next } => { + match self.do_with_lookup_step(val, name, next, mc, strings) { + Ok(()) => Action::Continue, + Err(e) => Self::vm_err_to_action(e), + } + } + AfterForce::TopLevelForce => Action::Done(Ok(self.convert_value(&val, strings))), + } + } + + fn do_call( + &mut self, + func: StrictValue<'gc>, + arg: Value<'gc>, + span: Option, + mc: &Mutation<'gc>, + ) -> VmResult<()> { + if let Some(closure_gc) = func.as_gc::>() { + let ip = closure_gc.ip; + let n_locals = closure_gc.n_locals; + let closure_env = closure_gc.env; + + if let Some(ref pattern) = closure_gc.pattern { + match self.force_inline(arg.clone())? { + ForceResult::Ready(forced_arg) => { + self.setup_pattern_call( + ip, + n_locals, + closure_env, + pattern, + forced_arg, + span, + 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, + mc, + ); + } + } + } else { + let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, closure_env))); + self.frames + .push(CallFrame { + pc: self.pc, + env: self.env(), + continuation: Continuation::Return, + span, + }) + .expect("frame stack overflow"); + self.pc = ip as usize; + self.current_env = Some(new_env); + } + Ok(()) + } else if let Some(po) = func.as_inline::() { + if po.arity == 1 { + Err(Self::err("builtins not yet implemented")) + } else { + let app = Gc::new( + mc, + PrimOpApp { + primop: po, + args: SmallVec::from_elem(arg, 1), + }, + ); + self.stack.push(Value::new_gc(app)).expect("stack overflow"); + 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")) + } else { + let app = Gc::new( + mc, + PrimOpApp { + primop: poa.primop, + args, + }, + ); + self.stack.push(Value::new_gc(app)).expect("stack overflow"); + Ok(()) + } + } else { + Err(Self::err( + "attempt to call something which is not a function", + )) + } + } + + fn setup_pattern_call( + &mut self, + ip: u32, + n_locals: u32, + closure_env: Gc<'gc, RefLock>>, + pattern: &PatternInfo, + arg: StrictValue<'gc>, + span: Option, + mc: &Mutation<'gc>, + ) -> VmResult<()> { + let Some(attrs) = arg.as_gc::>() else { + return Err(Self::err( + "function that expected a set received a non-set argument", + )); + }; + + for &req in &pattern.required { + if !attrs.has(req) { + return Err(Self::err("function argument missing required attribute")); + } + } + + if !pattern.ellipsis { + for (key, _) in attrs.entries.iter() { + let is_known = pattern.required.contains(key) || pattern.optional.contains(key); + if !is_known { + return Err(Self::err("function received unexpected attribute")); + } + } + } + + let new_env = Gc::new( + mc, + RefLock::new(Env::with_arg( + arg.clone().into_relaxed(), + n_locals, + closure_env, + )), + ); + + self.frames + .push(CallFrame { + pc: self.pc, + env: self.env(), + continuation: Continuation::Return, + span, + }) + .expect("frame stack overflow"); + self.pc = ip as usize; + self.current_env = Some(new_env); + Ok(()) + } + + fn compute_binop( + &self, + op: BinOpTag, + lhs: StrictValue<'gc>, + rhs: StrictValue<'gc>, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> VmResult> { + match op { + BinOpTag::Add => { + if let (Some(ls), Some(rs)) = ( + Self::get_string(lhs.clone(), strings), + Self::get_string(rhs.clone(), strings), + ) { + let ns = Gc::new(mc, NixString::new(format!("{ls}{rs}"))); + return Ok(Value::new_gc(ns)); + } + self.numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b) + } + BinOpTag::Sub => self.numeric_binop(lhs, rhs, mc, i64::wrapping_sub, |a, b| a - b), + BinOpTag::Mul => self.numeric_binop(lhs, rhs, mc, i64::wrapping_mul, |a, b| a * b), + BinOpTag::Div => match (Self::as_num(lhs), Self::as_num(rhs)) { + (_, Some(NixNum::Int(0))) => Err(Self::err("division by zero")), + (_, Some(NixNum::Float(f))) if f == 0.0 => Err(Self::err("division by zero")), + (Some(NixNum::Int(a)), Some(NixNum::Int(b))) => { + Ok(Self::make_int(a.wrapping_div(b), mc)) + } + (Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new_float(a / b)), + (Some(NixNum::Int(a)), Some(NixNum::Float(b))) => { + Ok(Value::new_float(a as f64 / b)) + } + (Some(NixNum::Float(a)), Some(NixNum::Int(b))) => { + Ok(Value::new_float(a / b as f64)) + } + _ => Err(Self::err("cannot divide non-numbers")), + }, + BinOpTag::Eq => Ok(Value::new_inline(self.values_equal(lhs, rhs, strings))), + BinOpTag::Neq => Ok(Value::new_inline(!self.values_equal(lhs, rhs, strings))), + BinOpTag::Lt => self.compare_values(lhs, rhs, strings, |o| o.is_lt()), + BinOpTag::Gt => self.compare_values(lhs, rhs, strings, |o| o.is_gt()), + BinOpTag::Leq => self.compare_values(lhs, rhs, strings, |o| o.is_le()), + BinOpTag::Geq => self.compare_values(lhs, rhs, strings, |o| o.is_ge()), + BinOpTag::Concat => { + let Some(l) = lhs.as_gc::>() else { + return Err(Self::err("cannot concatenate: left operand is not a list")); + }; + let Some(r) = rhs.as_gc::>() else { + return Err(Self::err("cannot concatenate: right operand is not a list")); + }; + let mut items = SmallVec::new(); + items.extend(l.inner.iter().cloned()); + items.extend(r.inner.iter().cloned()); + Ok(Value::new_gc(Gc::new(mc, List { inner: items }))) + } + BinOpTag::Update => { + let Some(l) = lhs.as_gc::>() else { + return Err(Self::err("cannot update: left operand is not a set")); + }; + let Some(r) = rhs.as_gc::>() else { + return Err(Self::err("cannot update: right operand is not a set")); + }; + Ok(Value::new_gc(l.merge(&r, mc))) + } + } + } + + fn numeric_binop( + &self, + lhs: StrictValue<'gc>, + rhs: StrictValue<'gc>, + mc: &Mutation<'gc>, + int_op: fn(i64, i64) -> i64, + float_op: fn(f64, f64) -> f64, + ) -> VmResult> { + match (Self::as_num(lhs), Self::as_num(rhs)) { + (Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Self::make_int(int_op(a, b), mc)), + (Some(NixNum::Float(a)), Some(NixNum::Float(b))) => { + Ok(Value::new_float(float_op(a, b))) + } + (Some(NixNum::Int(a)), Some(NixNum::Float(b))) => { + Ok(Value::new_float(float_op(a as f64, b))) + } + (Some(NixNum::Float(a)), Some(NixNum::Int(b))) => { + Ok(Value::new_float(float_op(a, b as f64))) + } + _ => Err(Self::err("cannot perform arithmetic on non-numbers")), + } + } + + 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())) { + return match (a, b) { + (NixNum::Int(a), NixNum::Int(b)) => a == b, + (NixNum::Float(a), NixNum::Float(b)) => a == b, + (NixNum::Int(a), NixNum::Float(b)) => a as f64 == b, + (NixNum::Float(a), NixNum::Int(b)) => a == b as f64, + }; + } + if let (Some(a), Some(b)) = (lhs.as_inline::(), rhs.as_inline::()) { + return a == b; + } + if lhs.is::() && rhs.is::() { + return true; + } + if let (Some(a), Some(b)) = ( + Self::get_string(lhs.clone(), strings), + Self::get_string(rhs.clone(), strings), + ) { + return a == b; + } + if let (Some(a), Some(b)) = (lhs.as_gc::>(), rhs.as_gc::>()) { + if a.inner.len() != b.inner.len() { + return false; + } + return a + .inner + .iter() + .zip(b.inner.iter()) + .all(|(x, y)| { + let (Ok(ForceResult::Ready(x)), Ok(ForceResult::Ready(y))) = + (self.force_inline(x.clone()), self.force_inline(y.clone())) + 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() { + return false; + } + return a + .entries + .iter() + .zip(b.entries.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())) + else { + return false; + }; + self.values_equal(v1, v2, strings) + }); + } + false + } + + 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())) { + let ord = match (a, b) { + (NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b), + (NixNum::Float(a), NixNum::Float(b)) => { + a.partial_cmp(&b).unwrap_or(std::cmp::Ordering::Less) + } + (NixNum::Int(a), NixNum::Float(b)) => (a as f64) + .partial_cmp(&b) + .unwrap_or(std::cmp::Ordering::Less), + (NixNum::Float(a), NixNum::Int(b)) => a + .partial_cmp(&(b as f64)) + .unwrap_or(std::cmp::Ordering::Less), + }; + return Ok(Value::new_inline(pred(ord))); + } + if let (Some(a), Some(b)) = ( + Self::get_string(lhs, strings), + Self::get_string(rhs, strings), + ) { + return Ok(Value::new_inline(pred(a.cmp(&b)))); + } + Err(Self::err("cannot compare these types")) + } + + fn do_select_step( + &mut self, + set_val: StrictValue<'gc>, + keys: SmallVec<[Value<'gc>; 4]>, + remaining: u16, + span: u32, + default: Option>, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> VmResult<()> { + let Some(attrs) = set_val.as_gc::>() else { + if let Some(def) = default { + self.stack.push(def).expect("stack overflow"); + return Ok(()); + } + return Err(Self::err("cannot select from non-set")); + }; + + let key_idx = keys.len() - remaining as usize; + let key = &keys[key_idx]; + let Some(key_sid) = Self::get_string_id(key) else { + return Err(Self::err("attribute name must be a string")); + }; + + let found = attrs.lookup(key_sid); + + if remaining <= 1 { + match found { + Some(v) => { + self.stack.push(v).expect("stack overflow"); + Ok(()) + } + None if default.is_some() => { + self.stack.push(default.unwrap()).expect("stack overflow"); + Ok(()) + } + 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)? { + 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, + AfterForce::Select { + keys, + remaining: remaining - 1, + span, + default, + }, + ip, + env, + mc, + ); + Ok(()) + } + }, + None if default.is_some() => { + self.stack.push(default.unwrap()).expect("stack overflow"); + Ok(()) + } + None => { + let name = strings.resolve(key_sid.0).unwrap_or(""); + Err(Self::err(format!("attribute '{name}' missing"))) + } + } + } + } + + fn do_has_attr_step( + &mut self, + set_val: StrictValue<'gc>, + keys: SmallVec<[Value<'gc>; 4]>, + remaining: u16, + mc: &Mutation<'gc>, + ) -> VmResult<()> { + let Some(attrs) = set_val.as_gc::>() else { + self.stack + .push(Value::new_inline(false)) + .expect("stack overflow"); + 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"); + return Ok(()); + }; + + if remaining <= 1 { + self.stack + .push(Value::new_inline(attrs.has(key_sid))) + .expect("stack overflow"); + 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) + } + ForceResult::NeedEval { ip, env, thunk } => { + self.push_force_frame( + thunk, + AfterForce::HasAttr { + keys, + remaining: remaining - 1, + }, + ip, + env, + mc, + ); + Ok(()) + } + }, + None => { + self.stack + .push(Value::new_inline(false)) + .expect("stack overflow"); + Ok(()) + } + } + } + } + + fn concat_strings_continue( + &mut self, + mut forced: SmallVec<[StrictValue<'gc>; 8]>, + mut remaining: SmallVec<[Value<'gc>; 8]>, + force_string: bool, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> VmResult<()> { + while let Some(part) = remaining.pop() { + match self.force_inline(part)? { + ForceResult::Ready(v) => forced.push(v), + ForceResult::NeedEval { ip, env, thunk } => { + self.push_force_frame( + thunk, + AfterForce::ConcatStrings { + forced, + remaining, + force_string, + }, + ip, + env, + mc, + ); + return Ok(()); + } + } + } + + 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()) { + match n { + NixNum::Int(i) => result.push_str(&i.to_string()), + NixNum::Float(f) => result.push_str(&format!("{f}")), + } + } else if part.is::() { + } else if let Some(b) = part.as_inline::() { + if force_string { + result.push_str(if b { "1" } else { "" }); + } else { + return Err(Self::err("cannot coerce a boolean to a string")); + } + } else { + return Err(Self::err("cannot coerce value to string")); + } + } + + let ns = Gc::new(mc, NixString::new(result)); + self.stack.push(Value::new_gc(ns)).expect("stack overflow"); + Ok(()) + } + + fn do_with_lookup_step( + &mut self, + scope_val: StrictValue<'gc>, + name: StringId, + next: Option>>, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> VmResult<()> { + if let Some(attrs) = scope_val.as_gc::>() { + if let Some(v) = attrs.lookup(name) { + self.stack.push(v).expect("stack overflow"); + return Ok(()); + } + } + + match next { + Some(scope) => { + let env_val = scope.env.clone(); + 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, + AfterForce::WithLookup { + name, + next: next_prev, + }, + ip, + env, + mc, + ); + Ok(()) + } + } + } + None => { + let name_str = strings.resolve(name.0).unwrap_or(""); + Err(Self::err(format!("undefined variable '{name_str}'"))) + } + } + } + + fn convert_value( + &self, + val: &Value<'gc>, + strings: &DefaultStringInterner, + ) -> crate::value::Value { + if let Some(i) = val.as_inline::() { + crate::value::Value::Int(i as i64) + } else if let Some(gc_i) = val.as_gc::() { + crate::value::Value::Int(*gc_i) + } else if let Some(f) = val.as_float() { + crate::value::Value::Float(f) + } else if let Some(b) = val.as_inline::() { + crate::value::Value::Bool(b) + } else if val.is::() { + crate::value::Value::Null + } else if let Some(sid) = val.as_inline::() { + let s = strings.resolve(sid.0).unwrap_or("").to_owned(); + crate::value::Value::String(s) + } else if let Some(ns) = val.as_gc::() { + crate::value::Value::String(ns.as_str().to_owned()) + } else if let Some(attrs) = val.as_gc::>() { + let mut map = std::collections::BTreeMap::new(); + for (key, val) in attrs.entries.iter() { + let key_str = strings.resolve(key.0).unwrap_or("").to_owned(); + let converted = self.convert_value(&val, strings); + map.insert(crate::value::Symbol::from(key_str), converted); + } + crate::value::Value::AttrSet(crate::value::AttrSet::new(map)) + } else if let Some(list) = val.as_gc::>() { + let items: Vec<_> = list + .inner + .iter() + .map(|v| self.convert_value(&v, strings)) + .collect(); + crate::value::Value::List(crate::value::List::new(items)) + } else if val.is::>() { + crate::value::Value::Func + } else if val.is::>() { + crate::value::Value::Thunk + } else if val.as_inline::().is_some() { + crate::value::Value::PrimOp("primop".into()) + } else if val.is::>() { + crate::value::Value::PrimOpApp("primop-app".into()) + } else { + crate::value::Value::Null + } + } + + fn execute_one( + &mut self, + bc: &[u8], + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> Action { + use crate::codegen::Op::{self, *}; + + #[cfg(debug_assertions)] + let opcode_byte = bc[self.pc]; + #[cfg(not(debug_assertions))] + let opcode_byte = unsafe { + *bc.get_unchecked(self.pc) + }; + self.pc += 1; + + #[cfg(debug_assertions)] + let Ok(op) = Op::try_from_primitive(opcode_byte) else { + return Action::Done(Err(Error::eval_error( + format!("unknown opcode: {opcode_byte:#04x}"), + None, + ))); + }; + #[cfg(not(debug_assertions))] + let op = unsafe { + Op::try_from_primitive(opcode_byte).unwrap_unchecked() + }; + + match op { + PushSmi => { + let val = self.read_i32(bc); + self.stack + .push(Value::new_inline(val)) + .expect("stack overflow"); + } + PushBigInt => { + let val = self.read_i64(bc); + self.stack + .push(Value::new_gc(Gc::new(mc, val))) + .expect("stack overflow"); + } + PushFloat => { + let val = self.read_f64(bc); + self.stack + .push(Value::new_float(val)) + .expect("stack overflow"); + } + PushString => { + let sid = self.read_string_id(bc); + self.stack + .push(Value::new_inline(sid)) + .expect("stack overflow"); + } + 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"), + + LoadLocal => { + let idx = self.read_u32(bc) as usize; + let val = self.env().borrow().locals[idx].clone(); + self.stack.push(val).expect("stack overflow"); + } + LoadOuter => { + let layer = self.read_u8(bc); + let idx = self.read_u32(bc) as usize; + let mut env = self.env(); + for _ in 0..layer { + let prev = env.borrow().prev.expect("LoadOuter: env chain too short"); + env = prev; + } + let val = env.borrow().locals[idx].clone(); + self.stack.push(val).expect("stack overflow"); + } + StoreLocal => { + let idx = self.read_u32(bc) as usize; + let val = self.stack.pop().expect("stack underflow"); + self.env().borrow_mut(mc).locals[idx] = val; + } + AllocLocals => { + let count = self.read_u32(bc) as usize; + self.env() + .borrow_mut(mc) + .locals + .resize(count, Value::default()); + } + + MakeThunk => { + let entry_point = self.read_u32(bc); + let _label = self.read_string_id(bc); + let thunk = Gc::new( + mc, + RefLock::new(ThunkState::Pending { + ip: entry_point, + env: self.env(), + }), + ); + self.stack + .push(Value::new_gc(thunk)) + .expect("stack overflow"); + } + MakeClosure => { + let entry_point = self.read_u32(bc); + let n_locals = self.read_u32(bc); + let closure = Gc::new( + mc, + Closure { + ip: entry_point, + n_locals, + env: self.env(), + pattern: None, + }, + ); + self.stack + .push(Value::new_gc(closure)) + .expect("stack overflow"); + } + MakePatternClosure => { + let entry_point = self.read_u32(bc); + let n_locals = self.read_u32(bc); + let req_count = self.read_u16(bc) as usize; + let opt_count = self.read_u16(bc) as usize; + let has_ellipsis = self.read_u8(bc) != 0; + + let mut required = SmallVec::new(); + for _ in 0..req_count { + required.push(self.read_string_id(bc)); + } + let mut optional = SmallVec::new(); + for _ in 0..opt_count { + optional.push(self.read_string_id(bc)); + } + let total = req_count + opt_count; + let mut param_spans = Vec::with_capacity(total); + for _ in 0..total { + let name = self.read_string_id(bc); + let span_id = self.read_u32(bc); + param_spans.push((name, span_id)); + } + + let pattern = Gc::new( + mc, + PatternInfo { + required, + optional, + ellipsis: has_ellipsis, + param_spans: param_spans.into_boxed_slice(), + }, + ); + let closure = Gc::new( + mc, + Closure { + ip: entry_point, + n_locals, + env: self.env(), + pattern: Some(pattern), + }, + ); + self.stack + .push(Value::new_gc(closure)) + .expect("stack overflow"); + } + + Call => { + let span_id = self.read_u32(bc); + let arg = self.stack.pop().expect("stack underflow"); + let func = self.stack.pop().expect("stack underflow"); + match self.force_inline(func) { + Ok(ForceResult::Ready(f)) => try_vm!(self.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, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(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, + ); + } + Err(e) => return Self::vm_err_to_action(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 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 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"); + } + 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 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())); + } + + 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(); + 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, + ))); + }; + entries.push((key_sid, val)); + } + + entries.sort_by_key(|(k, _)| *k); + 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"); + } + MakeEmptyAttrs => { + let attrs = Gc::new(mc, AttrSet::from_sorted(SmallVec::new())); + self.stack + .push(Value::new_gc(attrs)) + .expect("stack overflow"); + } + + 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 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, 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, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(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 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 { + keys, + remaining, + span: span_id, + default: Some(default), + }, + ip, + env, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(e), + } + } + HasAttr => { + let n = self.read_u16(bc) as usize; + let mut keys: SmallVec<[Value<'gc>; 4]> = 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)); + } + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame( + thunk, + AfterForce::HasAttr { keys, remaining }, + ip, + env, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(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 list = Gc::new(mc, List { inner: items }); + self.stack + .push(Value::new_gc(list)) + .expect("stack overflow"); + } + + OpAdd | OpSub | OpMul | OpDiv | OpEq | OpNeq | OpLt | OpGt | OpLeq | OpGeq + | OpConcat | OpUpdate => { + let tag = match op { + OpAdd => BinOpTag::Add, + OpSub => BinOpTag::Sub, + OpMul => BinOpTag::Mul, + OpDiv => BinOpTag::Div, + OpEq => BinOpTag::Eq, + OpNeq => BinOpTag::Neq, + OpLt => BinOpTag::Lt, + OpGt => BinOpTag::Gt, + OpLeq => BinOpTag::Leq, + OpGeq => BinOpTag::Geq, + OpConcat => BinOpTag::Concat, + OpUpdate => BinOpTag::Update, + _ => unreachable!(), + }; + let rhs = self.stack.pop().expect("stack underflow"); + let lhs = self.stack.pop().expect("stack underflow"); + + match self.force_inline(lhs) { + Ok(ForceResult::Ready(lhs_f)) => match self.force_inline(rhs) { + Ok(ForceResult::Ready(rhs_f)) => { + match self.compute_binop(tag, lhs_f, rhs_f, mc, strings) { + Ok(r) => self.stack.push(r).expect("stack overflow"), + Err(e) => return Self::vm_err_to_action(e), + } + } + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame( + thunk, + AfterForce::BinOpRhs { + lhs_forced: lhs_f, + op: tag, + }, + ip, + env, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(e), + }, + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame( + thunk, + AfterForce::BinOpLhs { rhs, op: tag }, + ip, + env, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(e), + } + } + + OpNeg => { + let val = self.stack.pop().expect("stack underflow"); + match self.force_inline(val) { + Ok(ForceResult::Ready(f)) => match Self::as_num(f) { + Some(NixNum::Int(i)) => self + .stack + .push(Self::make_int(-i, mc)) + .expect("stack overflow"), + Some(NixNum::Float(fl)) => self + .stack + .push(Value::new_float(-fl)) + .expect("stack overflow"), + None => { + return Self::vm_err_to_action(Self::err("cannot negate non-number")); + } + }, + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame(thunk, AfterForce::UnNeg, ip, env, mc); + } + Err(e) => return Self::vm_err_to_action(e), + } + } + OpNot => { + let val = self.stack.pop().expect("stack underflow"); + match self.force_inline(val) { + Ok(ForceResult::Ready(f)) => match f.as_inline::() { + Some(b) => self + .stack + .push(Value::new_inline(!b)) + .expect("stack overflow"), + None => return Self::vm_err_to_action(Self::err("value is not a boolean")), + }, + Ok(ForceResult::NeedEval { ip, env, thunk }) => { + self.push_force_frame(thunk, AfterForce::UnNot, ip, env, mc); + } + Err(e) => return Self::vm_err_to_action(e), + } + } + + ForceBool => { + let val = self.stack.pop().expect("stack underflow"); + match self.force_inline(val) { + Ok(ForceResult::Ready(f)) => { + if f.as_inline::().is_none() { + return Self::vm_err_to_action(Self::err("value is not a boolean")); + } + self.stack.push(f.into_relaxed()).expect("stack overflow"); + } + 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), + } + } + JumpIfFalse => { + let offset = self.read_i32(bc); + let val = self.stack.pop().expect("stack underflow"); + if let Some(false) = val.as_inline::() { + self.pc = ((self.pc as isize) + (offset as isize)) as usize; + } + } + JumpIfTrue => { + let offset = self.read_i32(bc); + let val = self.stack.pop().expect("stack underflow"); + if let Some(true) = val.as_inline::() { + self.pc = ((self.pc as isize) + (offset as isize)) as usize; + } + } + Jump => { + let offset = self.read_i32(bc); + self.pc = ((self.pc as isize) + (offset as isize)) as usize; + } + + ConcatStrings => { + let parts_count = self.read_u16(bc) as usize; + let force_string = self.read_u8(bc) != 0; + let parts: SmallVec<[Value<'gc>; 8]> = self.stack.pop_n::<8>(parts_count); + let mut remaining = parts; + remaining.reverse(); + let forced = SmallVec::new(); + try_vm!( + 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"); + } + + Assert => { + let raw_idx = self.read_u32(bc); + let span_id = self.read_u32(bc); + let expr = self.stack.pop().expect("stack underflow"); + let assertion = self.stack.pop().expect("stack underflow"); + + match self.force_inline(assertion) { + Ok(ForceResult::Ready(f)) => match f.as_inline::() { + Some(true) => self.stack.push(expr).expect("stack overflow"), + 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), + )); + } + None => { + return Self::vm_err_to_action(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, + mc, + ); + } + Err(e) => return Self::vm_err_to_action(e), + } + } + + PushWith => { + let ns = self.stack.pop().expect("stack underflow"); + let scope = Gc::new( + mc, + WithScope { + env: ns, + prev: self.with_scope, + }, + ); + self.with_scope = Some(scope); + } + PopWith => { + if let Some(scope) = self.with_scope { + self.with_scope = scope.prev; + } + } + WithLookup => { + let name = self.read_string_id(bc); + match self.with_scope { + Some(scope_gc) => { + let env_val = scope_gc.env.clone(); + 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, + ); + } + Err(e) => return Self::vm_err_to_action(e), + } + } + None => { + let name_str = strings.resolve(name.0).unwrap_or(""); + return Self::vm_err_to_action(Self::err(format!( + "undefined variable '{name_str}'" + ))); + } + } + } + + LoadBuiltins => { + self.stack + .push(self.globals.builtins.clone()) + .expect("stack overflow"); + } + LoadBuiltin => { + let _name = self.read_u32(bc); + self.stack + .push(Value::new_inline(Null)) + .expect("stack overflow"); + } + + MkPos => { + let _span_id = self.read_u32(bc); + self.stack + .push(Value::new_inline(Null)) + .expect("stack overflow"); + } + + LoadReplBinding => { + let _name = self.read_string_id(bc); + self.stack + .push(Value::new_inline(Null)) + .expect("stack overflow"); + } + LoadScopedBinding => { + let _name = self.read_string_id(bc); + self.stack + .push(Value::new_inline(Null)) + .expect("stack overflow"); + } + + Return => { + let ret_val = self.stack.pop().expect("stack underflow"); + return self.handle_return(ret_val, mc, strings); + } + } + + Action::Continue + } + + pub(super) fn run_batch( + &mut self, + bytecode: &[u8], + pc: &mut usize, + mc: &Mutation<'gc>, + strings: &DefaultStringInterner, + ) -> Action { + if !self.started { + self.pc = *pc; + self.current_env = Some(Gc::new(mc, RefLock::new(Env::empty()))); + self.started = true; + } + + for _ in 0..1024 { + let action = self.execute_one(bytecode, mc, strings); + match action { + Action::Continue => {} + other => { + *pc = self.pc; + if matches!(other, Action::Done(_)) { + self.started = false; + } + return other; + } + } + } + + *pc = self.pc; + Action::NeedGc + } +} diff --git a/fix/tests/tests/io_operations.rs b/fix/tests/tests/io_operations.rs index f83b04f..db48494 100644 --- a/fix/tests/tests/io_operations.rs +++ b/fix/tests/tests/io_operations.rs @@ -1,5 +1,5 @@ -use fix::context::Context; use fix::error::Source; +use fix::runtime::Runtime; use fix::value::Value; use crate::utils::{eval, eval_result}; @@ -97,7 +97,7 @@ fn import_with_complex_dependency_graph() { #[test_log::test] fn path_with_file() { - let mut ctx = Context::new().unwrap(); + let mut ctx = Runtime::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("test.txt"); std::fs::write(&test_file, "Hello, World!").unwrap(); @@ -107,7 +107,7 @@ fn path_with_file() { // Should return a store path string if let Value::String(store_path) = result { - assert!(store_path.starts_with(ctx.get_store_dir())); + assert!(store_path.starts_with("/nix/store")); assert!(store_path.contains("test.txt")); } else { panic!("Expected string, got {:?}", result); @@ -136,7 +136,7 @@ fn path_with_custom_name() { #[test_log::test] fn path_with_directory_recursive() { - let mut ctx = Context::new().unwrap(); + let mut ctx = Runtime::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let test_dir = temp_dir.path().join("mydir"); std::fs::create_dir_all(&test_dir).unwrap(); @@ -150,7 +150,7 @@ fn path_with_directory_recursive() { let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap(); if let Value::String(store_path) = result { - assert!(store_path.starts_with(ctx.get_store_dir())); + assert!(store_path.starts_with("/nix/store")); assert!(store_path.contains("mydir")); } else { panic!("Expected string, got {:?}", result); @@ -159,7 +159,7 @@ fn path_with_directory_recursive() { #[test_log::test] fn path_flat_with_file() { - let mut ctx = Context::new().unwrap(); + let mut ctx = Runtime::new().unwrap(); let temp_dir = tempfile::tempdir().unwrap(); let test_file = temp_dir.path().join("flat.txt"); std::fs::write(&test_file, "Flat content").unwrap(); @@ -171,7 +171,7 @@ fn path_flat_with_file() { let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap(); if let Value::String(store_path) = result { - assert!(store_path.starts_with(ctx.get_store_dir())); + assert!(store_path.starts_with("/nix/store")); } else { panic!("Expected string, got {:?}", result); } diff --git a/fix/tests/tests/lang.rs b/fix/tests/tests/lang.rs index 2362f95..2c768f6 100644 --- a/fix/tests/tests/lang.rs +++ b/fix/tests/tests/lang.rs @@ -2,8 +2,8 @@ use std::path::PathBuf; -use fix::context::Context; use fix::error::Source; +use fix::runtime::Runtime; use fix::value::Value; fn get_lang_dir() -> PathBuf { @@ -16,7 +16,7 @@ fn eval_file(name: &str) -> Result<(Value, Source), String> { let expr = format!(r#"import "{}""#, nix_path.display()); - let mut ctx = Context::new().map_err(|e| e.to_string())?; + let mut ctx = Runtime::new().map_err(|e| e.to_string())?; let source = Source { ty: fix::error::SourceType::File(nix_path.into()), src: expr.into(), @@ -247,7 +247,7 @@ eval_fail_test!(fail_abort); eval_fail_test!(fail_addDrvOutputDependencies_empty_context); eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context); eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind); -eval_fail_test!(fail_addErrorContext_example); +eval_fail_test!(fail_addErrorRuntime_example); eval_fail_test!(fail_assert); eval_fail_test!(fail_assert_equal_attrs_names); eval_fail_test!(fail_assert_equal_attrs_names_2); diff --git a/fix/tests/tests/string_context.rs b/fix/tests/tests/string_context.rs index 079698a..5d16581 100644 --- a/fix/tests/tests/string_context.rs +++ b/fix/tests/tests/string_context.rs @@ -1,4 +1,4 @@ -use fix::context::Context; +use fix::runtime::Runtime; use fix::value::Value; use crate::utils::eval_result; @@ -153,8 +153,8 @@ fn string_add_merges_context() { #[test_log::test] fn context_in_derivation_args() { - let mut ctx = Context::new().unwrap(); - let result = ctx + let mut rt = Runtime::new().unwrap(); + let result = rt .eval( r#" let @@ -173,7 +173,7 @@ fn context_in_derivation_args() { .unwrap(); match result { Value::String(s) => { - assert!(s.starts_with(ctx.get_store_dir()), "Should be a store path"); + assert!(s.starts_with("/nix/store"), "Should be a store path"); assert!(s.ends_with(".drv"), "Should be a .drv file"); } _ => panic!("Expected String, got {:?}", result), @@ -182,8 +182,8 @@ fn context_in_derivation_args() { #[test_log::test] fn context_in_derivation_env() { - let mut ctx = Context::new().unwrap(); - let result = ctx + let mut rt = Runtime::new().unwrap(); + let result = rt .eval( r#" let @@ -202,7 +202,7 @@ fn context_in_derivation_env() { .unwrap(); match result { Value::String(s) => { - assert!(s.starts_with(ctx.get_store_dir()), "Should be a store path"); + assert!(s.starts_with("/nix/store"), "Should be a store path"); assert!(s.ends_with(".drv"), "Should be a .drv file"); } _ => panic!("Expected String, got {:?}", result), @@ -224,8 +224,8 @@ fn tostring_preserves_context() { #[test_log::test] fn interpolation_derivation_returns_outpath() { - let mut ctx = Context::new().unwrap(); - let result = ctx + let mut rt = Runtime::new().unwrap(); + let result = rt .eval( r#" let @@ -238,7 +238,7 @@ fn interpolation_derivation_returns_outpath() { .unwrap(); match result { Value::String(s) => { - assert!(s.starts_with(ctx.get_store_dir()), "Should be a store path"); + assert!(s.starts_with("/nix/store"), "Should be a store path"); assert!(s.ends_with("-test"), "Should end with derivation name"); } _ => panic!("Expected String, got {:?}", result), diff --git a/fix/tests/tests/utils.rs b/fix/tests/tests/utils.rs index ecc10cc..7fb28b6 100644 --- a/fix/tests/tests/utils.rs +++ b/fix/tests/tests/utils.rs @@ -1,31 +1,31 @@ #![allow(dead_code)] -use fix::context::Context; use fix::error::{Result, Source}; +use fix::runtime::Runtime; use fix::value::Value; pub fn eval(expr: &str) -> Value { - Context::new() + Runtime::new() .unwrap() .eval(Source::new_eval(expr.into()).unwrap()) .unwrap() } pub fn eval_deep(expr: &str) -> Value { - Context::new() + Runtime::new() .unwrap() .eval_deep(Source::new_eval(expr.into()).unwrap()) .unwrap() } pub fn eval_deep_result(expr: &str) -> Result { - Context::new() + Runtime::new() .unwrap() .eval_deep(Source::new_eval(expr.into()).unwrap()) } pub fn eval_result(expr: &str) -> Result { - Context::new() + Runtime::new() .unwrap() .eval(Source::new_eval(expr.into()).unwrap()) } diff --git a/flake.lock b/flake.lock index cc7d286..00d02c2 100644 --- a/flake.lock +++ b/flake.lock @@ -8,11 +8,11 @@ "rust-analyzer-src": "rust-analyzer-src" }, "locked": { - "lastModified": 1770966612, - "narHash": "sha256-S6k14z/JsDwX6zZyLucDBTOe/9RsvxH9GTUxHn2o4vc=", + "lastModified": 1773471952, + "narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=", "owner": "nix-community", "repo": "fenix", - "rev": "e90d48dcfaebac7ea7a5687888a2d0733be26343", + "rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5", "type": "github" }, "original": { @@ -37,11 +37,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1770841267, - "narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=", + "lastModified": 1773282481, + "narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae", + "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127", "type": "github" }, "original": { @@ -61,11 +61,11 @@ "rust-analyzer-src": { "flake": false, "locked": { - "lastModified": 1770934477, - "narHash": "sha256-GX0cINHhhzUbQHyDYN2Mc+ovb6Sx/4yrF95VVou9aW4=", + "lastModified": 1773326183, + "narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=", "owner": "rust-lang", "repo": "rust-analyzer", - "rev": "931cd553be123b11db1435ac7ea5657e62e5e601", + "rev": "6254616e97f358e67b70dfc0463687f5f7911c1a", "type": "github" }, "original": {