From 29fab93cd125af4e487785539d67119d6510fbef Mon Sep 17 00:00:00 2001 From: imxyy_soope_ Date: Wed, 13 May 2026 18:28:18 +0800 Subject: [PATCH] refactor: abstract VM --- Cargo.lock | 38 +- Cargo.toml | 2 + fix-abstract-vm/Cargo.toml | 18 + {fix-vm => fix-abstract-vm}/src/boxing.rs | 38 +- .../src/bytecode_reader.rs | 35 +- {fix-vm => fix-abstract-vm}/src/forced.rs | 134 +++--- fix-abstract-vm/src/host.rs | 154 ++++++ fix-abstract-vm/src/lib.rs | 18 + fix-abstract-vm/src/machine.rs | 174 +++++++ fix-abstract-vm/src/path_util.rs | 18 + fix-abstract-vm/src/resolve.rs | 33 ++ fix-abstract-vm/src/state.rs | 99 ++++ {fix-vm => fix-abstract-vm}/src/value.rs | 118 ++--- fix-builtins/src/lib.rs | 13 +- fix-primops/Cargo.toml | 17 + fix-primops/src/control.rs | 362 ++++++++++++++ fix-primops/src/conv.rs | 51 ++ fix-primops/src/io.rs | 190 ++++++++ fix-primops/src/lib.rs | 69 +++ fix-primops/src/list.rs | 166 +++++++ fix-primops/src/path.rs | 43 ++ fix-vm/Cargo.toml | 2 + fix-vm/src/helpers.rs | 7 - fix-vm/src/instructions/arithmetic.rs | 9 +- fix-vm/src/instructions/calls.rs | 14 +- fix-vm/src/instructions/collections.rs | 8 +- fix-vm/src/instructions/control.rs | 2 +- fix-vm/src/instructions/misc.rs | 21 +- fix-vm/src/instructions/with_scope.rs | 4 +- fix-vm/src/lib.rs | 450 +++++++----------- fix-vm/src/primops/attrs.rs | 1 - fix-vm/src/primops/control.rs | 364 -------------- fix-vm/src/primops/conv.rs | 53 --- fix-vm/src/primops/io.rs | 193 -------- fix-vm/src/primops/list.rs | 169 ------- fix-vm/src/primops/mod.rs | 90 ---- fix-vm/src/primops/path.rs | 46 -- fix-vm/src/primops/regex.rs | 1 - fix-vm/src/primops/string.rs | 0 fix-vm/src/primops/version.rs | 1 - fix/Cargo.toml | 1 + fix/src/lib.rs | 7 +- 42 files changed, 1823 insertions(+), 1410 deletions(-) create mode 100644 fix-abstract-vm/Cargo.toml rename {fix-vm => fix-abstract-vm}/src/boxing.rs (92%) rename {fix-vm => fix-abstract-vm}/src/bytecode_reader.rs (76%) rename {fix-vm => fix-abstract-vm}/src/forced.rs (61%) create mode 100644 fix-abstract-vm/src/host.rs create mode 100644 fix-abstract-vm/src/lib.rs create mode 100644 fix-abstract-vm/src/machine.rs create mode 100644 fix-abstract-vm/src/path_util.rs create mode 100644 fix-abstract-vm/src/resolve.rs create mode 100644 fix-abstract-vm/src/state.rs rename {fix-vm => fix-abstract-vm}/src/value.rs (86%) create mode 100644 fix-primops/Cargo.toml create mode 100644 fix-primops/src/control.rs create mode 100644 fix-primops/src/conv.rs create mode 100644 fix-primops/src/io.rs create mode 100644 fix-primops/src/lib.rs create mode 100644 fix-primops/src/list.rs create mode 100644 fix-primops/src/path.rs delete mode 100644 fix-vm/src/helpers.rs delete mode 100644 fix-vm/src/primops/attrs.rs delete mode 100644 fix-vm/src/primops/control.rs delete mode 100644 fix-vm/src/primops/conv.rs delete mode 100644 fix-vm/src/primops/io.rs delete mode 100644 fix-vm/src/primops/list.rs delete mode 100644 fix-vm/src/primops/mod.rs delete mode 100644 fix-vm/src/primops/path.rs delete mode 100644 fix-vm/src/primops/regex.rs delete mode 100644 fix-vm/src/primops/string.rs delete mode 100644 fix-vm/src/primops/version.rs diff --git a/Cargo.lock b/Cargo.lock index 270f605..7f1cefc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -449,6 +449,7 @@ dependencies = [ "clap", "criterion", "ere", + "fix-abstract-vm", "fix-builtins", "fix-codegen", "fix-common", @@ -469,6 +470,23 @@ dependencies = [ "tracing-subscriber", ] +[[package]] +name = "fix-abstract-vm" +version = "0.1.0" +dependencies = [ + "fix-builtins", + "fix-codegen", + "fix-common", + "fix-error", + "gc-arena", + "hashbrown 0.16.1", + "likely_stable", + "num_enum", + "smallvec", + "sptr", + "string-interner", +] + [[package]] name = "fix-builtins" version = "0.1.0" @@ -526,15 +544,33 @@ dependencies = [ ] [[package]] -name = "fix-vm" +name = "fix-primops" version = "0.1.0" dependencies = [ + "fix-abstract-vm", "fix-builtins", "fix-codegen", "fix-common", "fix-error", "gc-arena", "hashbrown 0.16.1", + "num_enum", + "smallvec", + "string-interner", +] + +[[package]] +name = "fix-vm" +version = "0.1.0" +dependencies = [ + "fix-abstract-vm", + "fix-builtins", + "fix-codegen", + "fix-common", + "fix-error", + "fix-primops", + "gc-arena", + "hashbrown 0.16.1", "likely_stable", "num_enum", "smallvec", diff --git a/Cargo.toml b/Cargo.toml index d4ac7cd..54a11d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,11 +2,13 @@ resolver = "3" members = [ "fix", + "fix-abstract-vm", "fix-builtins", "fix-codegen", "fix-common", "fix-error", "fix-ir", + "fix-primops", "fix-vm", ] diff --git a/fix-abstract-vm/Cargo.toml b/fix-abstract-vm/Cargo.toml new file mode 100644 index 0000000..c75023a --- /dev/null +++ b/fix-abstract-vm/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "fix-abstract-vm" +version = "0.1.0" +edition = "2024" + +[dependencies] +gc-arena = { workspace = true } +hashbrown = { workspace = true } +num_enum = { workspace = true } +smallvec = { workspace = true } +string-interner = { workspace = true } +likely_stable = { workspace = true } +sptr = "0.3" + +fix-builtins = { path = "../fix-builtins" } +fix-codegen = { path = "../fix-codegen" } +fix-common = { path = "../fix-common" } +fix-error = { path = "../fix-error" } diff --git a/fix-vm/src/boxing.rs b/fix-abstract-vm/src/boxing.rs similarity index 92% rename from fix-vm/src/boxing.rs rename to fix-abstract-vm/src/boxing.rs index 7319152..b0313b6 100644 --- a/fix-vm/src/boxing.rs +++ b/fix-abstract-vm/src/boxing.rs @@ -24,7 +24,7 @@ impl ArrayExt for [T; N] { } } -pub(crate) trait RawStore: Sized { +pub trait RawStore: Sized { fn to_val(self, value: &mut Value); fn from_val(value: &Value) -> Self; } @@ -157,24 +157,24 @@ enum TagVal { } #[derive(Copy, Clone, PartialEq, Eq)] -pub(crate) struct RawTag(TagVal); +pub struct RawTag(TagVal); impl RawTag { - pub(crate) const P1: RawTag = RawTag(TagVal::_P1); - pub(crate) const P2: RawTag = RawTag(TagVal::_P2); - pub(crate) const P3: RawTag = RawTag(TagVal::_P3); - pub(crate) const P4: RawTag = RawTag(TagVal::_P4); - pub(crate) const P5: RawTag = RawTag(TagVal::_P5); - pub(crate) const P6: RawTag = RawTag(TagVal::_P6); - pub(crate) const P7: RawTag = RawTag(TagVal::_P7); + pub const P1: RawTag = RawTag(TagVal::_P1); + pub const P2: RawTag = RawTag(TagVal::_P2); + pub const P3: RawTag = RawTag(TagVal::_P3); + pub const P4: RawTag = RawTag(TagVal::_P4); + pub const P5: RawTag = RawTag(TagVal::_P5); + pub const P6: RawTag = RawTag(TagVal::_P6); + pub const P7: RawTag = RawTag(TagVal::_P7); - pub(crate) const N1: RawTag = RawTag(TagVal::_N1); - pub(crate) const N2: RawTag = RawTag(TagVal::_N2); - pub(crate) const N3: RawTag = RawTag(TagVal::_N3); - pub(crate) const N4: RawTag = RawTag(TagVal::_N4); - pub(crate) const N5: RawTag = RawTag(TagVal::_N5); - pub(crate) const N6: RawTag = RawTag(TagVal::_N6); - pub(crate) const N7: RawTag = RawTag(TagVal::_N7); + pub const N1: RawTag = RawTag(TagVal::_N1); + pub const N2: RawTag = RawTag(TagVal::_N2); + pub const N3: RawTag = RawTag(TagVal::_N3); + pub const N4: RawTag = RawTag(TagVal::_N4); + pub const N5: RawTag = RawTag(TagVal::_N5); + pub const N6: RawTag = RawTag(TagVal::_N6); + pub const N7: RawTag = RawTag(TagVal::_N7); #[inline] #[must_use] @@ -260,7 +260,7 @@ impl RawTag { #[inline] #[must_use] - pub(crate) const fn neg_val(self) -> (bool, u8) { + pub const fn neg_val(self) -> (bool, u8) { match self.0 { TagVal::_P1 => (false, 1), TagVal::_P2 => (false, 2), @@ -323,7 +323,7 @@ impl Header { #[derive(Copy, Clone, Debug, PartialEq)] #[repr(C, align(8))] -pub(crate) struct Value { +pub struct Value { #[cfg(target_endian = "big")] header: Header, data: [u8; 6], @@ -373,7 +373,7 @@ impl Value { #[inline] #[must_use] - pub(crate) fn data(&self) -> &[u8; 6] { + pub fn data(&self) -> &[u8; 6] { &self.data } diff --git a/fix-vm/src/bytecode_reader.rs b/fix-abstract-vm/src/bytecode_reader.rs similarity index 76% rename from fix-vm/src/bytecode_reader.rs rename to fix-abstract-vm/src/bytecode_reader.rs index 84c274c..bab00c6 100644 --- a/fix-vm/src/bytecode_reader.rs +++ b/fix-abstract-vm/src/bytecode_reader.rs @@ -1,3 +1,5 @@ +#![allow(dead_code)] + use fix_codegen::OperandType; use fix_common::StringId; use num_enum::TryFromPrimitive; @@ -5,15 +7,14 @@ use string_interner::Symbol as _; use crate::{OperandData, VmRuntimeCtx}; -pub(crate) struct BytecodeReader<'a> { +pub struct BytecodeReader<'a> { bytecode: &'a [u8], pc: usize, inst_start_pc: usize, } impl<'a> BytecodeReader<'a> { - #[cfg_attr(feature = "tailcall", allow(dead_code))] - pub(crate) fn new(bytecode: &'a [u8], pc: usize) -> Self { + pub fn new(bytecode: &'a [u8], pc: usize) -> Self { Self { bytecode, pc, @@ -22,8 +23,7 @@ impl<'a> BytecodeReader<'a> { } #[inline(always)] - #[cfg_attr(not(feature = "tailcall"), allow(dead_code))] - pub(crate) fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self { + pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self { Self { bytecode, pc: inst_start_pc + 1, @@ -42,8 +42,7 @@ impl<'a> BytecodeReader<'a> { } #[inline(always)] - #[cfg_attr(feature = "tailcall", allow(dead_code))] - pub(crate) fn read_op(&mut self) -> fix_codegen::Op { + pub fn read_op(&mut self) -> fix_codegen::Op { use fix_codegen::Op; self.inst_start_pc = self.pc; let byte = self.bytecode[self.pc]; @@ -55,46 +54,46 @@ impl<'a> BytecodeReader<'a> { } #[inline(always)] - pub(crate) fn read_u8(&mut self) -> u8 { + pub fn read_u8(&mut self) -> u8 { let val = self.bytecode[self.pc]; self.pc += 1; val } #[inline(always)] - pub(crate) fn read_u16(&mut self) -> u16 { + pub fn read_u16(&mut self) -> u16 { u16::from_le_bytes(self.read_array()) } #[inline(always)] - pub(crate) fn read_u32(&mut self) -> u32 { + pub fn read_u32(&mut self) -> u32 { u32::from_le_bytes(self.read_array()) } #[inline(always)] - pub(crate) fn read_i32(&mut self) -> i32 { + pub fn read_i32(&mut self) -> i32 { i32::from_le_bytes(self.read_array()) } #[inline(always)] - pub(crate) fn read_i64(&mut self) -> i64 { + pub fn read_i64(&mut self) -> i64 { i64::from_le_bytes(self.read_array()) } #[inline(always)] - pub(crate) fn read_f64(&mut self) -> f64 { + pub fn read_f64(&mut self) -> f64 { f64::from_le_bytes(self.read_array()) } #[inline(always)] - pub(crate) fn read_string_id(&mut self) -> StringId { + pub fn read_string_id(&mut self) -> StringId { let raw = self.read_u32(); #[allow(clippy::unwrap_used)] StringId(string_interner::symbol::SymbolU32::try_from_usize(raw as usize).unwrap()) } #[inline(always)] - pub(crate) fn read_operand_data(&mut self, ctx: &C) -> OperandData { + pub fn read_operand_data(&mut self, ctx: &C) -> OperandData { let tag = self.read_u8(); let Ok(ty) = OperandType::try_from_primitive(tag) .map_err(|err| panic!("unknown operand tag: {:#04x}", err.number)); @@ -129,15 +128,15 @@ impl<'a> BytecodeReader<'a> { } } - pub(crate) fn pc(&self) -> usize { + pub fn pc(&self) -> usize { self.pc } - pub(crate) fn set_pc(&mut self, pc: usize) { + pub fn set_pc(&mut self, pc: usize) { self.pc = pc; } - pub(crate) fn inst_start_pc(&self) -> usize { + pub fn inst_start_pc(&self) -> usize { self.inst_start_pc } } diff --git a/fix-vm/src/forced.rs b/fix-abstract-vm/src/forced.rs similarity index 61% rename from fix-vm/src/forced.rs rename to fix-abstract-vm/src/forced.rs index f0f716f..3c1124f 100644 --- a/fix-vm/src/forced.rs +++ b/fix-abstract-vm/src/forced.rs @@ -1,18 +1,20 @@ use fix_common::StringId; use gc_arena::{Gc, Mutation}; -use crate::value::*; -use crate::{Break, BytecodeReader, NixNum, Step, Vm}; +use crate::{ + AttrSet, Break, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null, + PrimOp, PrimOpApp, Step, StrictValue, +}; -pub(crate) trait Forced<'gc>: Sized { +pub trait Forced<'gc>: Sized { const WIDTH: usize; /// Force and type-check the `WIDTH` slots starting at `base_depth` from /// TOS, deepest-first. If a slot holds a thunk, enter it and return /// `Break::Force`. If a slot holds a value of the wrong type, call /// `finish_type_err` and return `Break::Done`. - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base_depth: usize, @@ -22,26 +24,26 @@ pub(crate) trait Forced<'gc>: Sized { /// After `force_and_check` returned `Continue`, pop `WIDTH` slots /// (TOS first) and convert. Type assertions are infallible because /// `force_and_check` already validated every slot. - fn pop_converted(vm: &mut Vm<'gc>) -> Self; + fn pop_converted>(m: &mut M) -> Self; } impl<'gc> Forced<'gc> for StrictValue<'gc> { const WIDTH: usize = 1; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base_depth: usize, resume_pc: usize, ) -> Step { - vm.force_slot_to_pc(base_depth, reader, mc, resume_pc) + m.force_slot_to_pc(base_depth, reader, mc, resume_pc) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - vm.pop_forced() + fn pop_converted>(m: &mut M) -> Self { + m.pop_forced() } } @@ -52,25 +54,25 @@ macro_rules! impl_forced_inline { const WIDTH: usize = 1; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base_depth: usize, resume_pc: usize, ) -> Step { - vm.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; - let v = vm.peek_forced(base_depth); + m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; + let v = m.peek_forced(base_depth); if v.as_inline::<$ty>().is_none() { - let _: Step = vm.finish_type_err($nix_ty, v.ty()); + let _: Step = m.finish_type_err($nix_ty, v.ty()); return Step::Break(Break::Done); } Step::Continue(()) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - vm.pop_forced() + fn pop_converted>(m: &mut M) -> Self { + m.pop_forced() .as_inline::<$ty>() .expect("type checked in force_and_check") } @@ -86,25 +88,25 @@ macro_rules! impl_forced_gc { const WIDTH: usize = 1; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base_depth: usize, resume_pc: usize, ) -> Step { - vm.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; - let v = vm.peek_forced(base_depth); + m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; + let v = m.peek_forced(base_depth); if v.as_gc::<$ty>().is_none() { - let _: Step = vm.finish_type_err($nix_ty, v.ty()); + let _: Step = m.finish_type_err($nix_ty, v.ty()); return Step::Break(Break::Done); } Step::Continue(()) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - vm.pop_forced() + fn pop_converted>(m: &mut M) -> Self { + m.pop_forced() .as_gc::<$ty>() .expect("type checked in force_and_check") } @@ -134,25 +136,25 @@ impl<'gc> Forced<'gc> for NixNum { const WIDTH: usize = 1; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base_depth: usize, resume_pc: usize, ) -> Step { - vm.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; - let v = vm.peek_forced(base_depth); + m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; + let v = m.peek_forced(base_depth); if v.as_num().is_none() { - let _: Step = vm.finish_type_err(NixType::Int, v.ty()); + let _: Step = m.finish_type_err(NixType::Int, v.ty()); return Step::Break(Break::Done); } Step::Continue(()) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - vm.pop_forced() + fn pop_converted>(m: &mut M) -> Self { + m.pop_forced() .as_num() .expect("type checked in force_and_check") } @@ -162,25 +164,25 @@ impl<'gc> Forced<'gc> for f64 { const WIDTH: usize = 1; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base_depth: usize, resume_pc: usize, ) -> Step { - vm.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; - let v = vm.peek_forced(base_depth); + m.force_slot_to_pc(base_depth, reader, mc, resume_pc)?; + let v = m.peek_forced(base_depth); if v.as_float().is_none() { - let _: Step = vm.finish_type_err(NixType::Float, v.ty()); + let _: Step = m.finish_type_err(NixType::Float, v.ty()); return Step::Break(Break::Done); } Step::Continue(()) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - vm.pop_forced() + fn pop_converted>(m: &mut M) -> Self { + m.pop_forced() .as_float() .expect("type checked in force_and_check") } @@ -190,21 +192,21 @@ impl<'gc, A: Forced<'gc>, B: Forced<'gc>> Forced<'gc> for (A, B) { const WIDTH: usize = A::WIDTH + B::WIDTH; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base: usize, resume_pc: usize, ) -> Step { - A::force_and_check(vm, reader, mc, base + B::WIDTH, resume_pc)?; - B::force_and_check(vm, reader, mc, base, resume_pc) + A::force_and_check(m, reader, mc, base + B::WIDTH, resume_pc)?; + B::force_and_check(m, reader, mc, base, resume_pc) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - let b = B::pop_converted(vm); - let a = A::pop_converted(vm); + fn pop_converted>(m: &mut M) -> Self { + let b = B::pop_converted(m); + let a = A::pop_converted(m); (a, b) } } @@ -213,23 +215,23 @@ impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>> Forced<'gc> for (A, B, const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base: usize, resume_pc: usize, ) -> Step { - A::force_and_check(vm, reader, mc, base + B::WIDTH + C::WIDTH, resume_pc)?; - B::force_and_check(vm, reader, mc, base + C::WIDTH, resume_pc)?; - C::force_and_check(vm, reader, mc, base, resume_pc) + A::force_and_check(m, reader, mc, base + B::WIDTH + C::WIDTH, resume_pc)?; + B::force_and_check(m, reader, mc, base + C::WIDTH, resume_pc)?; + C::force_and_check(m, reader, mc, base, resume_pc) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - let c = C::pop_converted(vm); - let b = B::pop_converted(vm); - let a = A::pop_converted(vm); + fn pop_converted>(m: &mut M) -> Self { + let c = C::pop_converted(m); + let b = B::pop_converted(m); + let a = A::pop_converted(m); (a, b, c) } } @@ -240,31 +242,31 @@ impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>, D: Forced<'gc>> Forced const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH + D::WIDTH; #[inline(always)] - fn force_and_check( - vm: &mut Vm<'gc>, + fn force_and_check>( + m: &mut M, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, base: usize, resume_pc: usize, ) -> Step { A::force_and_check( - vm, + m, reader, mc, base + B::WIDTH + C::WIDTH + D::WIDTH, resume_pc, )?; - B::force_and_check(vm, reader, mc, base + C::WIDTH + D::WIDTH, resume_pc)?; - C::force_and_check(vm, reader, mc, base + D::WIDTH, resume_pc)?; - D::force_and_check(vm, reader, mc, base, resume_pc) + B::force_and_check(m, reader, mc, base + C::WIDTH + D::WIDTH, resume_pc)?; + C::force_and_check(m, reader, mc, base + D::WIDTH, resume_pc)?; + D::force_and_check(m, reader, mc, base, resume_pc) } #[inline(always)] - fn pop_converted(vm: &mut Vm<'gc>) -> Self { - let d = D::pop_converted(vm); - let c = C::pop_converted(vm); - let b = B::pop_converted(vm); - let a = A::pop_converted(vm); + fn pop_converted>(m: &mut M) -> Self { + let d = D::pop_converted(m); + let c = C::pop_converted(m); + let b = B::pop_converted(m); + let a = A::pop_converted(m); (a, b, c, d) } } diff --git a/fix-abstract-vm/src/host.rs b/fix-abstract-vm/src/host.rs new file mode 100644 index 0000000..764169f --- /dev/null +++ b/fix-abstract-vm/src/host.rs @@ -0,0 +1,154 @@ +use fix_codegen::InstructionPtr; +use fix_common::StringId; +use fix_error::Source; +use hashbrown::HashSet; + +use crate::{ + AttrSet, Closure, ExtraScope, List, NixString, NixType, Null, Path, PrimOp, PrimOpApp, + StaticValue, StrictValue, Thunk, ThunkState, Value, +}; + +pub trait VmContext { + fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx); +} + +pub trait VmRuntimeCtx { + fn intern_string(&mut self, s: impl AsRef) -> StringId; + fn resolve_string(&self, id: StringId) -> &str; + fn get_const(&self, id: u32) -> StaticValue; + fn add_const(&mut self, val: StaticValue) -> u32; +} + +pub trait VmCode { + fn bytecode(&self) -> &[u8]; + fn compile_with_scope( + &mut self, + source: Source, + extra_scope: Option, + ctx: &mut impl VmRuntimeCtx, + ) -> fix_error::Result; +} + +pub trait VmRuntimeCtxExt: VmRuntimeCtx { + fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>; + fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>; + fn get_string_id<'a, 'gc: 'a>( + &'a mut self, + val: StrictValue<'gc>, + ) -> std::result::Result; + fn convert_value(&self, val: Value) -> fix_common::Value; +} + +impl VmRuntimeCtxExt for T { + fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> { + if let Some(sid) = val.as_inline::() { + Some(self.resolve_string(sid)) + } else { + val.as_gc::().map(|ns| ns.as_ref().as_str()) + } + } + + /// Like `get_string`, but also accepts `Path` values (returning their + /// underlying canonical-path string). Use this in places where Nix + /// would coerce a path to a string (string interpolation, file IO + /// builtins, etc.). + fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> { + if let Some(p) = val.as_inline::() { + Some(self.resolve_string(p.0)) + } else { + self.get_string(val) + } + } + + fn get_string_id<'a, 'gc: 'a>( + &'a mut self, + val: StrictValue<'gc>, + ) -> std::result::Result { + if let Some(sid) = val.as_inline::() { + Ok(sid) + } else if let Some(s) = val.as_gc::().map(|ns| ns.as_ref().as_str()) { + Ok(self.intern_string(s)) + } else { + Err(val.ty()) + } + } + + fn convert_value(&self, val: Value) -> fix_common::Value { + self.convert_value_with_seen(val, &mut HashSet::new()) + } +} + +pub(crate) trait ConvertValueWithSeen: VmRuntimeCtx { + fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet) -> fix_common::Value; +} + +impl ConvertValueWithSeen for T { + fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet) -> fix_common::Value { + use fix_common::Value; + if let Some(i) = val.as_inline::() { + Value::Int(i as i64) + } else if let Some(gc_i) = val.as_gc::() { + Value::Int(*gc_i) + } else if let Some(f) = val.as_float() { + Value::Float(f) + } else if let Some(b) = val.as_inline::() { + Value::Bool(b) + } else if val.is::() { + Value::Null + } else if let Some(sid) = val.as_inline::() { + let s = self.resolve_string(sid).to_owned(); + Value::String(s) + } else if let Some(ns) = val.as_gc::() { + Value::String(ns.as_str().to_owned()) + } else if let Some(p) = val.as_inline::() { + Value::Path(self.resolve_string(p.0).to_owned()) + } else if let Some(attrs) = val.as_gc::() { + let bits = val.to_bits(); + if attrs.entries.is_empty() { + return Value::AttrSet(Default::default()); + } + if !seen.insert(bits) { + return Value::Repeated; + } + let mut map = std::collections::BTreeMap::new(); + for &(key, val) in attrs.entries.iter() { + let key = self.resolve_string(key).to_owned(); + let converted = self.convert_value_with_seen(val, seen); + map.insert(fix_common::Symbol::from(key), converted); + } + Value::AttrSet(fix_common::AttrSet::new(map)) + } else if let Some(list) = val.as_gc::() { + let bits = val.to_bits(); + if list.inner.borrow().is_empty() { + return Value::List(Default::default()); + } + if !seen.insert(bits) { + return Value::Repeated; + } + let items: Vec<_> = list + .inner + .borrow() + .iter() + .copied() + .map(|v| self.convert_value_with_seen(v, seen)) + .collect(); + Value::List(fix_common::List::new(items)) + } else if val.is::() { + Value::Func + } else if let Some(thunk) = val.as_gc::() { + if let ThunkState::Evaluated(v) = *thunk.borrow() { + self.convert_value_with_seen(v.relax(), seen) + } else { + Value::Thunk + } + } else if let Some(primop) = val.as_inline::() { + let name = fix_builtins::BUILTINS[primop.id as usize].0; + Value::PrimOp(name.strip_prefix("__").unwrap_or(name)) + } else if let Some(app) = val.as_gc::() { + let name = fix_builtins::BUILTINS[app.primop.id as usize].0; + Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name)) + } else { + Value::Null + } + } +} diff --git a/fix-abstract-vm/src/lib.rs b/fix-abstract-vm/src/lib.rs new file mode 100644 index 0000000..465b01a --- /dev/null +++ b/fix-abstract-vm/src/lib.rs @@ -0,0 +1,18 @@ +mod boxing; +mod bytecode_reader; +mod forced; +mod host; +mod machine; +mod path_util; +mod resolve; +mod state; +mod value; + +pub use bytecode_reader::*; +pub use forced::*; +pub use host::*; +pub use machine::*; +pub use path_util::*; +pub use resolve::*; +pub use state::*; +pub use value::*; diff --git a/fix-abstract-vm/src/machine.rs b/fix-abstract-vm/src/machine.rs new file mode 100644 index 0000000..d6a9c7b --- /dev/null +++ b/fix-abstract-vm/src/machine.rs @@ -0,0 +1,174 @@ +use std::ops::ControlFlow; +use std::path::{Path, PathBuf}; + +use fix_common::StringId; +use fix_error::Error; +use gc_arena::Mutation; + +use crate::{ + Break, BytecodeReader, CallFrame, ForceMode, Forced, GcEnv, NixType, PendingLoad, Step, + StrictValue, Value, VmError, +}; + +/// Abstract VM-side operations consumed by instruction handlers and primops. +/// +/// Implementors maintain a value stack, a call stack, an environment chain, +/// pending result/error state, and a set of GC-allocated globals. Methods +/// fall into a few groups: +/// +/// - Stack ops (`push` / `pop` / `peek` / `replace` / `pop_forced` / ...) +/// - Forcing primitives (`force_slot` / `force_slot_to_pc`) +/// - Calling (`call` / `return_from_primop`) +/// - Call-frame management (`push_call_frame` / `pop_call_frame` / call-depth) +/// - Environment access (`env` / `set_env` / `local`) +/// - Result finalization (`finish_ok` / `finish_err` / ...) +/// - Global lookup (`builtins` / `empty_list` / `empty_attrs` / ...) +/// - Imports and scope slots (`import_cache_*` / `scope_slot*` / `set_pending_load`) +pub trait Machine<'gc> { + fn push(&mut self, val: Value<'gc>); + fn pop(&mut self) -> Value<'gc>; + fn peek(&self, depth: usize) -> Value<'gc>; + fn peek_forced(&self, depth: usize) -> StrictValue<'gc>; + fn pop_forced(&mut self) -> StrictValue<'gc>; + fn replace(&mut self, depth: usize, val: Value<'gc>); + fn stack_len(&self) -> usize; + + fn force_slot_to_pc( + &mut self, + depth: usize, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + resume_pc: usize, + ) -> Step; + + #[inline(always)] + fn force_slot( + &mut self, + depth: usize, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> Step { + let pc = reader.inst_start_pc(); + self.force_slot_to_pc(depth, reader, mc, pc) + } + + fn call( + &mut self, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + arg: Value<'gc>, + resume_pc: usize, + ) -> Step; + + #[inline(always)] + fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step { + self.push(val); + let Some(CallFrame { + pc: ret_pc, + thunk: _, + env, + }) = self.pop_call_frame() + else { + unreachable!() + }; + reader.set_pc(ret_pc); + self.dec_call_depth(); + self.set_env(env); + Step::Continue(()) + } + + fn push_call_frame(&mut self, frame: CallFrame<'gc>); + fn pop_call_frame(&mut self) -> Option>; + fn call_depth(&self) -> usize; + fn inc_call_depth(&mut self); + fn dec_call_depth(&mut self); + + fn env(&self) -> GcEnv<'gc>; + fn set_env(&mut self, env: GcEnv<'gc>); + + #[inline(always)] + fn local(&self, layer: u8, idx: u32) -> Value<'gc> { + let mut cur = self.env(); + for _ in 0..layer { + let prev = cur.borrow().prev.expect("env chain too short"); + cur = prev; + } + cur.borrow().locals[idx as usize] + } + + fn finish_ok(&mut self, val: fix_common::Value) -> Step; + fn finish_err(&mut self, err: Box) -> Step; + fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step; + + #[inline(always)] + fn finish_vm_err(&mut self, err: VmError) -> Step { + self.finish_err(err.into_error()) + } + + fn builtins(&self) -> Value<'gc>; + fn functor_sym(&self) -> StringId; + fn empty_list(&self) -> Value<'gc>; + fn empty_attrs(&self) -> Value<'gc>; + fn force_mode(&self) -> ForceMode; + + fn import_cache_get(&self, path: &Path) -> Option>; + fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>); + fn scope_slot(&self, idx: u32) -> Value<'gc>; + fn scope_slots_push(&mut self, val: Value<'gc>) -> u32; + fn set_pending_load(&mut self, load: PendingLoad); +} + +/// Extension trait with convenience helpers built on top of [`Machine`]. +/// +/// Auto-implemented for every `Machine<'gc>` so callers just need to bring +/// `MachineExt` (or `Machine`) into scope. +pub trait MachineExt<'gc>: Machine<'gc> { + /// Force the top `T::WIDTH` stack slots and return them as `T`. + /// + /// If any slot holds a pending thunk, this method pushes a call frame + /// whose resume PC is the **start of the current instruction** + /// (`reader.inst_start_pc()`), enters the thunk, and returns + /// `Break::Force`. When the thunk eventually returns, the VM will + /// **re-execute the entire opcode handler from the beginning**. + /// + /// # Invariants + /// + /// * **Do not call this method more than once in a single handler.** + /// If you need to force multiple values, use a tuple type such as + /// `(StrictValue, StrictValue)` so they are forced and popped in one + /// atomic operation. + /// * The stack layout at the call site must be **identical** every time + /// the handler is re-entered. + /// * Propagate the return value with `?` so `Break::Force` correctly + /// unwinds to the dispatch loop. + #[inline(always)] + fn force_and_retry>( + &mut self, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> ControlFlow + where + Self: Sized, + { + let pc = reader.inst_start_pc(); + self.force_and_retry_pc(reader, mc, pc) + } + + /// Same as [`force_and_retry`](Self::force_and_retry) but allows + /// specifying a custom resume PC. + #[inline(always)] + fn force_and_retry_pc>( + &mut self, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + resume_pc: usize, + ) -> ControlFlow + where + Self: Sized, + { + T::force_and_check(self, reader, mc, 0, resume_pc)?; + ControlFlow::Continue(T::pop_converted(self)) + } +} + +impl<'gc, M: Machine<'gc>> MachineExt<'gc> for M {} diff --git a/fix-abstract-vm/src/path_util.rs b/fix-abstract-vm/src/path_util.rs new file mode 100644 index 0000000..98dc3ac --- /dev/null +++ b/fix-abstract-vm/src/path_util.rs @@ -0,0 +1,18 @@ +use std::path::{Component, PathBuf}; + +pub fn canon_path_str(path: impl AsRef) -> String { + let p = path.as_ref(); + let mut normalized = PathBuf::new(); + for component in p.components() { + match component { + Component::Prefix(p) => normalized.push(p.as_os_str()), + Component::RootDir => normalized.push("/"), + Component::CurDir => {} + Component::ParentDir => { + normalized.pop(); + } + Component::Normal(c) => normalized.push(c), + } + } + normalized.to_string_lossy().into_owned() +} diff --git a/fix-abstract-vm/src/resolve.rs b/fix-abstract-vm/src/resolve.rs new file mode 100644 index 0000000..04dcb6a --- /dev/null +++ b/fix-abstract-vm/src/resolve.rs @@ -0,0 +1,33 @@ +use gc_arena::{Gc, Mutation}; + +use crate::{AttrSet, Machine, OperandData, Value}; + +/// Resolve a decoded operand into a runtime [`Value`]. +/// +/// The operand decoder ([`crate::BytecodeReader::read_operand_data`]) +/// produces a static enum; this function materializes it against the +/// running [`Machine`] (env chain, builtins, scope slots, ...). +#[inline] +pub fn resolve_operand<'gc, M: Machine<'gc>>( + op: &OperandData, + mc: &Mutation<'gc>, + m: &M, +) -> Value<'gc> { + use OperandData::*; + match *op { + Const(sv) => sv.into(), + BigInt(val) => Value::new_gc(Gc::new(mc, val)), + Local { layer, idx } => m.local(layer, idx), + #[allow(clippy::unwrap_used)] + BuiltinConst(id) => m.builtins().as_gc::().unwrap().lookup(id).unwrap(), + Builtins => m.builtins(), + ReplBinding(_id) => todo!(), + ScopedImportBinding { slot_id, name } => { + let scope = m.scope_slot(slot_id); + #[allow(clippy::unwrap_used)] + let attrs = scope.as_gc::().expect("scope must be attrset"); + #[allow(clippy::unwrap_used)] + attrs.lookup(name).expect("scoped binding not found") + } + } +} diff --git a/fix-abstract-vm/src/state.rs b/fix-abstract-vm/src/state.rs new file mode 100644 index 0000000..89cf780 --- /dev/null +++ b/fix-abstract-vm/src/state.rs @@ -0,0 +1,99 @@ +use std::ops::ControlFlow; +use std::path::PathBuf; + +use fix_common::StringId; +use fix_error::Error; +use gc_arena::{Collect, Gc}; +use hashbrown::HashSet; + +use crate::{GcEnv, StaticValue, Thunk}; + +#[allow(dead_code)] +pub enum VmError { + Catchable(String), + Uncatchable(Box), +} + +impl From> for VmError { + fn from(e: Box) -> Self { + VmError::Uncatchable(e) + } +} + +impl VmError { + pub fn into_error(self) -> Box { + match self { + VmError::Catchable(_) => todo!("Check for tryEval catch frames"), + VmError::Uncatchable(e) => e, + } + } +} + +pub fn vm_err(msg: impl Into) -> VmError { + VmError::Uncatchable(Error::eval_error(msg.into())) +} + +#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)] +#[collect(require_static)] +pub enum ForceMode { + #[default] + AsIs, + Shallow, + Deep, +} + +#[repr(u8)] +pub enum Break { + Force, + Done, + LoadFile, +} + +pub type Step = ControlFlow; + +#[allow(dead_code)] +pub struct ErrorFrame { + pub span_id: u32, + pub message: Option, +} + +#[derive(Collect, Debug)] +#[collect(no_drop)] +pub struct CallFrame<'gc> { + pub pc: usize, + pub thunk: Option>>, + pub env: GcEnv<'gc>, +} + +#[derive(Debug)] +pub struct PendingLoad { + pub path: PathBuf, + pub scope: Option, +} + +#[derive(Debug)] +pub struct PendingScope { + pub keys: HashSet, + pub slot_id: u32, +} + +/// Extra scope passed to a re-entrant compile from inside a running VM. +/// +/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin), +/// but the variant is kept open so REPL bindings could later land here too. +pub enum ExtraScope { + ScopedImport { + keys: HashSet, + slot_id: u32, + }, +} + +pub enum OperandData { + Const(StaticValue), + BigInt(i64), + Local { layer: u8, idx: u32 }, + BuiltinConst(StringId), + Builtins, + ReplBinding(StringId), + ScopedImportBinding { slot_id: u32, name: StringId }, +} diff --git a/fix-vm/src/value.rs b/fix-abstract-vm/src/value.rs similarity index 86% rename from fix-vm/src/value.rs rename to fix-abstract-vm/src/value.rs index e5da1eb..621c11f 100644 --- a/fix-vm/src/value.rs +++ b/fix-abstract-vm/src/value.rs @@ -16,7 +16,6 @@ use smallvec::SmallVec; use string_interner::Symbol; use string_interner::symbol::SymbolU32; -use crate::NixNum; use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue}; mod private { @@ -250,7 +249,7 @@ impl<'gc> Value<'gc> { } #[inline] - pub(crate) fn to_bits(self) -> u64 { + pub fn to_bits(self) -> u64 { self.raw.to_bits() } @@ -308,27 +307,27 @@ impl<'gc> Value<'gc> { } #[inline] - pub(crate) fn expect_inline(self) -> Result { + pub fn expect_inline(self) -> Result { self.as_inline::().ok_or_else(|| self.ty()) } #[inline] - pub(crate) fn expect_gc(self) -> Result, NixType> { + pub fn expect_gc(self) -> Result, NixType> { self.as_gc::().ok_or_else(|| self.ty()) } #[inline] - pub(crate) fn expect_num(self) -> Result { + pub fn expect_num(self) -> Result { self.as_num().ok_or_else(|| self.ty()) } #[inline] - pub(crate) fn expect_bool(self) -> Result { + pub fn expect_bool(self) -> Result { self.as_inline::().ok_or_else(|| self.ty()) } #[inline] - pub(crate) fn expect_float(self) -> Result { + pub fn expect_float(self) -> Result { self.as_float().ok_or_else(|| self.ty()) } } @@ -385,7 +384,7 @@ impl StaticValue { } #[derive(Clone, Copy, Debug)] -pub(crate) struct Null; +pub struct Null; impl RawStore for Null { fn to_val(self, value: &mut RawValue) { value.set_data([0; 6]); @@ -410,7 +409,7 @@ impl RawStore for StringId { /// A canonicalized absolute path. Inline value carrying an interned /// `StringId` whose contents are the path's absolute, dot-resolved form. #[derive(Clone, Copy, Debug, PartialEq, Eq)] -pub(crate) struct Path(pub StringId); +pub struct Path(pub StringId); impl RawStore for Path { fn to_val(self, value: &mut RawValue) { @@ -428,17 +427,17 @@ impl RawStore for Path { /// the outer allocation. #[derive(Collect)] #[collect(require_static)] -pub(crate) struct NixString { +pub struct NixString { data: Box, // TODO: string context for derivation dependency tracking } impl NixString { - pub(crate) fn new(s: impl Into>) -> Self { + pub fn new(s: impl Into>) -> Self { Self { data: s.into() } } - pub(crate) fn as_str(&self) -> &str { + pub fn as_str(&self) -> &str { &self.data } } @@ -451,28 +450,28 @@ impl fmt::Debug for NixString { #[derive(Collect, Debug, Default)] #[collect(no_drop)] -pub(crate) struct AttrSet<'gc> { - pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>, +pub struct AttrSet<'gc> { + pub entries: SmallVec<[(StringId, Value<'gc>); 4]>, } impl<'gc> AttrSet<'gc> { - pub(crate) fn from_sorted_unchecked(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self { + pub fn from_sorted_unchecked(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> { + pub fn lookup(&self, key: StringId) -> Option> { self.entries .binary_search_by_key(&key, |(k, _)| *k) .ok() .map(|i| self.entries[i].1) } - pub(crate) fn has(&self, key: StringId) -> bool { + pub 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> { + pub 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)); @@ -510,12 +509,12 @@ impl<'gc> AttrSet<'gc> { #[derive(Collect, Debug, Default)] #[repr(transparent)] #[collect(no_drop)] -pub(crate) struct List<'gc> { - pub(crate) inner: RefLock; 4]>>, +pub struct List<'gc> { + pub inner: RefLock; 4]>>, } impl<'gc> List<'gc> { - pub(crate) fn new(mc: &Mutation<'gc>, data: SmallVec<[Value<'gc>; 4]>) -> Gc<'gc, Self> { + pub fn new(mc: &Mutation<'gc>, data: SmallVec<[Value<'gc>; 4]>) -> Gc<'gc, Self> { Gc::new( mc, Self { @@ -524,7 +523,7 @@ impl<'gc> List<'gc> { ) } - pub(crate) fn new_gc(mc: &Mutation<'gc>) -> Gc<'gc, Self> { + pub fn new_gc(mc: &Mutation<'gc>) -> Gc<'gc, Self> { Gc::new(mc, Self::default()) } } @@ -536,11 +535,11 @@ impl<'gc> Unlock for List<'gc> { } } -pub(crate) type Thunk<'gc> = RefLock>; +pub type Thunk<'gc> = RefLock>; #[derive(Collect, Debug)] #[collect(no_drop)] -pub(crate) enum ThunkState<'gc> { +pub enum ThunkState<'gc> { Pending { ip: usize, env: GcEnv<'gc> }, Apply { func: Value<'gc>, arg: Value<'gc> }, Blackhole, @@ -549,33 +548,29 @@ pub(crate) enum ThunkState<'gc> { #[derive(Collect, Debug)] #[collect(no_drop)] -pub(crate) struct Env<'gc> { - pub(crate) locals: SmallVec<[Value<'gc>; 4]>, - pub(crate) prev: Option>, +pub struct Env<'gc> { + pub locals: SmallVec<[Value<'gc>; 4]>, + pub prev: Option>, } -pub(crate) type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>; +pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>; #[derive(Collect, Debug)] #[collect(no_drop)] -pub(crate) struct WithEnv<'gc> { - pub(crate) env: Value<'gc>, - pub(crate) prev: Option>, +pub struct WithEnv<'gc> { + pub env: Value<'gc>, + pub prev: Option>, } -pub(crate) type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>; +pub type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>; impl<'gc> Env<'gc> { - pub(crate) fn empty() -> Self { + pub fn empty() -> Self { Env { locals: SmallVec::new(), prev: None, } } - pub(crate) fn with_arg( - arg: Value<'gc>, - n_locals: u32, - prev: Gc<'gc, RefLock>>, - ) -> Self { + pub 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 { @@ -587,29 +582,29 @@ impl<'gc> Env<'gc> { #[derive(Collect, Debug)] #[collect(no_drop)] -pub(crate) struct Closure<'gc> { - pub(crate) ip: u32, - pub(crate) n_locals: u32, - pub(crate) env: Gc<'gc, RefLock>>, - pub(crate) pattern: Option>, +pub struct Closure<'gc> { + pub ip: u32, + pub n_locals: u32, + pub env: Gc<'gc, RefLock>>, + pub pattern: Option>, } #[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)]>, +pub struct PatternInfo { + pub required: SmallVec<[StringId; 4]>, + pub optional: SmallVec<[StringId; 4]>, + pub ellipsis: bool, + pub param_spans: Box<[(StringId, u32)]>, } #[repr(packed, Rust)] #[derive(Clone, Copy, Debug, Collect)] #[collect(require_static)] -pub(crate) struct PrimOp { - pub(crate) id: BuiltinId, - pub(crate) arity: u8, - pub(crate) dispatch_ip: u32, +pub struct PrimOp { + pub id: BuiltinId, + pub arity: u8, + pub dispatch_ip: u32, } impl RawStore for PrimOp { @@ -636,20 +631,20 @@ impl RawStore for PrimOp { #[derive(Collect, Debug)] #[collect(no_drop)] -pub(crate) struct PrimOpApp<'gc> { - pub(crate) primop: PrimOp, - pub(crate) arity: u8, - pub(crate) args: [Value<'gc>; 3], +pub struct PrimOpApp<'gc> { + pub primop: PrimOp, + pub arity: u8, + pub args: [Value<'gc>; 3], } #[derive(Copy, Clone, Default, Collect)] #[repr(transparent)] #[collect(no_drop)] -pub(crate) struct StrictValue<'gc>(Value<'gc>); +pub struct StrictValue<'gc>(Value<'gc>); impl<'gc> StrictValue<'gc> { #[inline] - pub(crate) fn relax(self) -> Value<'gc> { + pub fn relax(self) -> Value<'gc> { self.0 } } @@ -670,7 +665,7 @@ impl fmt::Debug for StrictValue<'_> { #[derive(Clone, Copy, Debug, PartialEq, Eq, Collect)] #[collect(require_static)] -pub(crate) enum NixType { +pub enum NixType { Int, Float, Bool, @@ -710,3 +705,8 @@ impl std::fmt::Display for NixType { write!(f, "{}", self.display()) } } + +pub enum NixNum { + Int(i64), + Float(f64), +} diff --git a/fix-builtins/src/lib.rs b/fix-builtins/src/lib.rs index 7ada8c2..11cee31 100644 --- a/fix-builtins/src/lib.rs +++ b/fix-builtins/src/lib.rs @@ -124,7 +124,7 @@ define_builtins! { } #[repr(u8)] -#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)] +#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)] pub enum PrimOpPhase { Abort, Add, @@ -257,6 +257,17 @@ pub enum PrimOpPhase { Illegal, } +impl TryFrom for PrimOpPhase { + type Error = u8; + fn try_from(value: u8) -> Result { + if (0..Self::Illegal as u8).contains(&value) { + Ok(unsafe { std::mem::transmute::(value) }) + } else { + Err(value) + } + } +} + impl BuiltinId { #[inline(always)] pub fn entry_phase(self) -> PrimOpPhase { diff --git a/fix-primops/Cargo.toml b/fix-primops/Cargo.toml new file mode 100644 index 0000000..7bd1c06 --- /dev/null +++ b/fix-primops/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "fix-primops" +version = "0.1.0" +edition = "2024" + +[dependencies] +gc-arena = { workspace = true } +hashbrown = { workspace = true } +num_enum = { workspace = true } +smallvec = { workspace = true } +string-interner = { workspace = true } + +fix-abstract-vm = { path = "../fix-abstract-vm" } +fix-builtins = { path = "../fix-builtins" } +fix-codegen = { path = "../fix-codegen" } +fix-common = { path = "../fix-common" } +fix-error = { path = "../fix-error" } diff --git a/fix-primops/src/control.rs b/fix-primops/src/control.rs new file mode 100644 index 0000000..fee6756 --- /dev/null +++ b/fix-primops/src/control.rs @@ -0,0 +1,362 @@ +use fix_abstract_vm::{ + AttrSet, BytecodeReader, Closure, Env, List, Machine, MachineExt, Step, StrictValue, Value, + VmRuntimeCtx, VmRuntimeCtxExt, +}; +use fix_builtins::PrimOpPhase; +use fix_error::Error; +use gc_arena::{Gc, Mutation, RefLock}; +use smallvec::SmallVec; + +pub fn seq<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack: [e1, e2] - force e1, return e2 + m.force_slot(1, reader, mc)?; + let e2 = m.pop(); + let _ = m.pop(); + m.return_from_primop(e2, reader) +} + +pub fn abort<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack: [msg] - force msg, then abort with it + m.force_slot(0, reader, mc)?; + let msg_val = m.peek_forced(0); + let msg = ctx.get_string(msg_val).unwrap_or(""); + m.finish_err(Error::eval_error(format!( + "evaluation aborted with the following error message: '{msg}'" + ))) +} + +pub fn deep_seq_force_top<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack: [e1, e2] - force e1, return e2 + m.force_slot(1, reader, mc)?; + + let e1 = m.peek_forced(1); + + let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::() { + let attrs = &attrs.entries; + if attrs.is_empty() { + SmallVec::new() + } else { + attrs.iter().map(|&(_, v)| v).collect() + } + } else if let Some(list) = e1.as_gc::>() { + let inner = list.inner.borrow(); + if inner.is_empty() { + SmallVec::new() + } else { + inner.iter().copied().collect() + } + } else { + SmallVec::new() + }; + + if children.is_empty() { + let e2 = m.pop(); + let _ = m.pop(); + return m.return_from_primop(e2, reader); + } + + let count = children.len() as i32; + let seen: Gc<'gc, List<'gc>> = Gc::new(mc, List::default()); + let worklist: Gc<'gc, List<'gc>> = List::new(mc, children); + + let e2 = m.pop(); + let _ = m.pop(); + m.push(e2); + m.push(Value::new_gc(seen)); + m.push(Value::new_gc(worklist)); + m.push(Value::new_inline(count)); + reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize); + Step::Continue(()) +} + +pub fn deep_seq_push<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack: [e2, seen, worklist, counter] + #[allow(clippy::unwrap_used)] + let counter = m.peek(0).as_inline::().unwrap(); + if counter == 0 { + let _ = m.pop(); // counter + let _ = m.pop(); // worklist + let _ = m.pop(); // seen + let val = m.pop(); + return m.return_from_primop(val, reader); + } + + #[allow(clippy::unwrap_used)] + let worklist = m.peek_forced(1).as_gc::>().unwrap(); + #[allow(clippy::unwrap_used)] + let item = worklist.unlock(mc).borrow_mut().pop().unwrap(); + m.replace(0, Value::new_inline(counter - 1)); + m.push(item); + + // force item at TOS, resume at DeepSeqLoop after force + m.force_slot_to_pc(0, reader, mc, PrimOpPhase::DeepSeqLoop.ip() as usize)?; + reader.set_pc(PrimOpPhase::DeepSeqLoop.ip() as usize); + Step::Continue(()) +} + +pub fn deep_seq_loop<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack after pop: [e2, seen, worklist, counter] + let item = m.pop(); + #[allow(clippy::unwrap_used)] + let counter = m.peek(0).as_inline::().unwrap(); + + let mut added: usize = 0; + if let Some(attrs) = item.as_gc::() { + let attrs = &attrs.entries; + #[allow(clippy::unwrap_used)] + let seen = m.peek_forced(2).as_gc::>().unwrap(); + if !is_value_in_seen(seen, item) { + add_value_to_seen(seen, mc, item); + #[allow(clippy::unwrap_used)] + let worklist = m.peek_forced(1).as_gc::>().unwrap(); + { + let mut wl = worklist.unlock(mc).borrow_mut(); + for &(_, v) in attrs.iter() { + wl.push(v); + } + added = attrs.len(); + } + } + } else if let Some(list) = item.as_gc::>() { + #[allow(clippy::unwrap_used)] + let seen = m.peek_forced(2).as_gc::>().unwrap(); + if !is_value_in_seen(seen, item) { + add_value_to_seen(seen, mc, item); + #[allow(clippy::unwrap_used)] + let worklist = m.peek_forced(1).as_gc::>().unwrap(); + { + let inner = list.inner.borrow(); + let mut wl = worklist.unlock(mc).borrow_mut(); + for &v in inner.iter() { + wl.push(v); + } + added = inner.len(); + } + } + } + + m.replace(0, Value::new_inline(counter + added as i32)); + reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize); + Step::Continue(()) +} + +pub fn force_result_shallow<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + m.force_slot(0, reader, mc)?; + let val = m.peek_forced(0); + + let (count, has_children) = if let Some(attrs) = val.as_gc::() { + let len = attrs.entries.len(); + (len, len > 0) + } else if let Some(list) = val.as_gc::>() { + let len = list.inner.borrow().len(); + (len, len > 0) + } else { + (0, false) + }; + + if !has_children { + let val = m.pop(); + return m.finish_ok(ctx.convert_value(val)); + } + + m.push(Value::new_inline(0i32)); + m.push(Value::new_inline(count as i32)); + reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize); + Step::Continue(()) +} + +pub fn force_result_shallow_push<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + #[allow(clippy::unwrap_used)] + let idx = m.peek(1).as_inline::().unwrap(); + #[allow(clippy::unwrap_used)] + let len = m.peek(0).as_inline::().unwrap(); + + if idx == len { + let _ = m.pop(); // len + let _ = m.pop(); // idx + let val = m.pop(); + return m.finish_ok(ctx.convert_value(val)); + } + + let val = m.peek_forced(2); + let child = if let Some(attrs) = val.as_gc::() { + attrs.entries.get(idx as usize).map(|&(_, v)| v) + } else if let Some(list) = val.as_gc::>() { + list.inner.borrow().get(idx as usize).copied() + } else { + None + }; + + if let Some(child) = child { + m.replace(1, Value::new_inline(idx + 1)); + m.push(child); + m.force_slot_to_pc( + 0, + reader, + mc, + PrimOpPhase::ForceResultShallowLoop.ip() as usize, + )?; + reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize); + } + Step::Continue(()) +} + +pub fn force_result_shallow_loop<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, +) -> Step { + let _ = m.pop(); // forced child + reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize); + Step::Continue(()) +} + +pub fn force_result_deep_finish<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let val = m.force_and_retry::(reader, mc)?; + m.finish_ok(ctx.convert_value(val.relax())) +} + +fn is_value_in_seen<'gc>(seen: Gc<'gc, List<'gc>>, val: Value<'gc>) -> bool { + if !is_container(val) { + return false; + } + let target = val.to_bits(); + for &v in seen.inner.borrow().iter() { + if v.to_bits() == target { + return true; + } + } + false +} + +fn add_value_to_seen<'gc>(seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) { + if is_container(val) { + seen.unlock(mc).borrow_mut().push(val); + } +} + +pub fn call_functor_1<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // Stack invariant on every (re-)entry: [..., orig_arg, self, functor] + // where `functor` is TOS. Retries during force land back here safely. + let functor = m.force_and_retry::(reader, mc)?; + // Stack now: [..., orig_arg, self] + let self_val = m.pop(); + m.push(functor.relax()); + // Stack: [..., orig_arg, functor] + // Call 1: functor(self). Resume into CallFunctor2 once it returns. + m.call( + reader, + mc, + self_val, + PrimOpPhase::CallFunctor2.ip() as usize, + ) +} + +pub fn call_functor_2<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // Stack on entry: [..., orig_arg, intermediate] + // call_stack top: synthetic frame with caller's resume_pc. + let intermediate = m.pop(); + let orig_arg = m.pop(); + let saved = m.pop_call_frame().expect("functor outer frame missing"); + m.set_env(saved.env); + m.push(intermediate); + // Call 2: intermediate(orig_arg). Resume to caller. + m.call(reader, mc, orig_arg, saved.pc) +} + +pub fn call_pattern<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let (func, attrset) = m.force_and_retry::<(Gc, Gc)>(reader, mc)?; + + let Closure { + ip, + n_locals, + env, + pattern, + } = *func; + let Some(pattern) = pattern else { + unreachable!() + }; + // TODO: get function name + // TODO: param spans + if !pattern.ellipsis { + for key in pattern.required.iter().copied() { + if attrset.lookup(key).is_none() { + let name = ctx.resolve_string(key); + return m.finish_err(Error::eval_error(format!( + "function 'anonymous lambda' called without required argument '{name}'" + ))); + } + } + for &(key, _) in attrset.entries.iter() { + let is_expected = pattern.required.contains(&key) || pattern.optional.contains(&key); + if !is_expected { + let name = ctx.resolve_string(key); + return m.finish_err(Error::eval_error(format!( + "function 'anonymous lambda' called with unexpected argument '{name}'" + ))); + } + } + } + + let new_env = Gc::new( + mc, + RefLock::new(Env::with_arg(Value::new_gc(attrset), n_locals, env)), + ); + reader.set_pc(ip as usize); + m.set_env(new_env); + + Step::Continue(()) +} + +fn is_container(val: Value<'_>) -> bool { + val.is::() || val.is::>() +} diff --git a/fix-primops/src/conv.rs b/fix-primops/src/conv.rs new file mode 100644 index 0000000..ea8f59c --- /dev/null +++ b/fix-primops/src/conv.rs @@ -0,0 +1,51 @@ +use fix_abstract_vm::{ + BytecodeReader, Machine, MachineExt, NixString, NixType, Path, Step, StrictValue, Value, + VmRuntimeCtx, +}; +use fix_common::StringId; +use fix_error::Error; +use gc_arena::Mutation; + +pub fn to_string<'gc, M: Machine<'gc>>( + m: &mut M, + _ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let val = m.force_and_retry::(reader, mc)?; + if val.is::() || val.is::() { + return m.return_from_primop(val.relax(), reader); + } + if let Some(p) = val.as_inline::() { + return m.return_from_primop(Value::new_inline(p.0), reader); + } + // TODO: derivations / `__toString` / `outPath`, + // numbers, lists. + m.finish_err(Error::eval_error(format!( + "cannot coerce {} to a string", + val.ty() + ))) +} + +pub fn type_of<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let val = m.force_and_retry::(reader, mc)?; + let name: &str = match val.ty() { + NixType::Int => "int", + NixType::Float => "float", + NixType::Bool => "bool", + NixType::Null => "null", + NixType::String => "string", + NixType::Path => "path", + NixType::AttrSet => "set", + NixType::List => "list", + NixType::Closure | NixType::PrimOp | NixType::PrimOpApp => "lambda", + NixType::Thunk => unreachable!("forced"), + }; + let sid = ctx.intern_string(name); + m.return_from_primop(Value::new_inline(sid), reader) +} diff --git a/fix-primops/src/io.rs b/fix-primops/src/io.rs new file mode 100644 index 0000000..a7a8913 --- /dev/null +++ b/fix-primops/src/io.rs @@ -0,0 +1,190 @@ +use std::path::PathBuf; + +use fix_abstract_vm::{ + AttrSet, Break, BytecodeReader, CallFrame, Machine, MachineExt, Path, PendingLoad, + PendingScope, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, canon_path_str, +}; +use fix_builtins::PrimOpPhase; +use fix_common::StringId; +use fix_error::Error; +use gc_arena::{Gc, Mutation}; +use hashbrown::HashSet; + +pub fn import<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack: [path] + let path_val = m.force_and_retry::(reader, mc)?; + let path_str = match ctx.get_string_or_path(path_val) { + Some(s) => s.to_owned(), + None => { + return m.finish_err(Error::eval_error(format!( + "expected a path or string, got {}", + path_val.ty() + ))); + } + }; + let abs = match resolve_import_target(&path_str) { + Ok(p) => p, + Err(e) => return m.finish_err(e), + }; + + if let Some(cached) = m.import_cache_get(&abs) { + return m.return_from_primop(cached, reader); + } + + // Stash the resolved path on the stack as a string-id so the + // finalizer can use it as the cache key. The slot we pop here was + // freed by `force_and_retry`, so we simply push. + let path_sid = ctx.intern_string(abs.to_string_lossy()); + m.push(Value::new_inline(path_sid)); + let env = m.env(); + m.push_call_frame(CallFrame { + pc: PrimOpPhase::ImportFinalize.ip() as usize, + thunk: None, + env, + }); + + m.set_pending_load(PendingLoad { + path: abs, + scope: None, + }); + Step::Break(Break::LoadFile) +} + +pub fn import_finalize<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, +) -> Step { + // stack: [path_sid, return_value] + let val = m.pop(); + #[allow(clippy::unwrap_used)] + let path_sid = m.pop().as_inline::().unwrap(); + // The cache key is keyed by the absolute path string we interned in + // `import`. Resolve it back to the host PathBuf. + let path_str = ctx.resolve_string(path_sid).to_owned(); + m.import_cache_insert(PathBuf::from(path_str), val); + m.push(val); + let Some(CallFrame { + pc: ret_pc, + thunk: _, + env, + }) = m.pop_call_frame() + else { + unreachable!() + }; + reader.set_pc(ret_pc); + // FIXME: + // m.dec_call_depth(); + m.set_env(env); + Step::Continue(()) +} + +pub fn scoped_import<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // stack: [scope, path] + let (scope_attrs, path_val) = m.force_and_retry::<(Gc, StrictValue)>(reader, mc)?; + let path_str = match ctx.get_string_or_path(path_val) { + Some(s) => s.to_owned(), + None => { + return m.finish_err(Error::eval_error(format!( + "expected a path or string, got {}", + path_val.ty() + ))); + } + }; + let abs = match resolve_import_target(&path_str) { + Ok(p) => p, + Err(e) => return m.finish_err(e), + }; + + let keys: HashSet = scope_attrs.entries.iter().map(|&(k, _)| k).collect(); + let slot_id = m.scope_slots_push(Value::new_gc(scope_attrs)); + + let env = m.env(); + m.push_call_frame(CallFrame { + pc: PrimOpPhase::ScopedImportFinalize.ip() as usize, + thunk: None, + env, + }); + + m.set_pending_load(PendingLoad { + path: abs, + scope: Some(PendingScope { keys, slot_id }), + }); + Step::Break(Break::LoadFile) +} + +pub fn scoped_import_finalize<'gc, M: Machine<'gc>>( + m: &mut M, + _ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, +) -> Step { + // stack: [return_value] + // We intentionally do NOT pop the slot from `scope_slots` so that + // closures or thunks created inside the imported file can still + // resolve their scope after `scopedImport` returns. + let val = m.pop(); + m.return_from_primop(val, reader) +} + +pub fn path_exists<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let path_val = m.force_and_retry::(reader, mc)?; + // pathExists requires an absolute path. A `Path` value is + // always absolute; a string is accepted only if it starts with `/`. + let (path, is_path_value) = if let Some(p) = path_val.as_inline::() { + (ctx.resolve_string(p.0).to_owned(), true) + } else if let Some(s) = ctx.get_string(path_val) { + (s.to_owned(), false) + } else { + return m.finish_err(Error::eval_error(format!( + "expected a path or string, got {}", + path_val.ty() + ))); + }; + if !is_path_value && !path.starts_with('/') { + return m.finish_err(Error::eval_error(format!( + "string '{path}' doesn't represent an absolute path" + ))); + } + // CppNix collapses consecutive slashes and resolves `.` / `..` lexically + // before checking. Trailing-slash / trailing-dot mean "must be a directory". + let must_be_dir = path.ends_with('/') || path.ends_with("/."); + let canon = canon_path_str(&path); + let p = std::path::Path::new(&canon); + let exists = if must_be_dir { + std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false) + } else { + std::fs::symlink_metadata(p).is_ok() + }; + m.return_from_primop(Value::new_inline(exists), reader) +} + +/// Convert the user-supplied path string into an absolute, dotted-segment +/// resolved `PathBuf` and append `default.nix` if the target is a directory. +fn resolve_import_target(path: &str) -> Result> { + let mut abs = PathBuf::from(path); + if !abs.is_absolute() { + return Err(Error::eval_error(format!( + "import: expected an absolute path, got '{path}'" + ))); + } + if abs.is_dir() { + abs.push("default.nix"); + } + Ok(abs) +} diff --git a/fix-primops/src/lib.rs b/fix-primops/src/lib.rs new file mode 100644 index 0000000..46395a3 --- /dev/null +++ b/fix-primops/src/lib.rs @@ -0,0 +1,69 @@ +mod control; +mod conv; +mod io; +mod list; +mod path; + +pub use control::*; +pub use conv::*; +use fix_abstract_vm::{BytecodeReader, Machine, Step, VmRuntimeCtx}; +use fix_builtins::PrimOpPhase; +use fix_error::Error; +use gc_arena::Mutation; +pub use io::*; +pub use list::*; +pub use path::*; + +#[allow(clippy::too_many_lines)] +pub fn dispatch_primop<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + use PrimOpPhase::*; + let phase_disc = reader.read_u8(); + let Ok(phase) = PrimOpPhase::try_from(phase_disc) else { + return m.finish_err(Error::eval_error("invalid primop phase")); + }; + match phase { + Abort => abort(m, ctx, reader, mc), + + DeepSeq => deep_seq_force_top(m, reader, mc), + DeepSeqPush => deep_seq_push(m, reader, mc), + DeepSeqLoop => deep_seq_loop(m, reader, mc), + Seq => seq(m, reader, mc), + + FilterForceList => filter_force_list(m, reader, mc), + FilterCallPred => filter_call_pred(m, reader, mc), + FilterCheck => filter_check(m, reader, mc), + + FoldlStrict => foldl_strict_entry(m, reader, mc), + FoldlStrictEmpty => foldl_strict_empty(m, reader, mc), + FoldlStrictCall1 => foldl_strict_call1(m, reader, mc), + FoldlStrictCall2 => foldl_strict_call2(m, reader, mc), + FoldlStrictUpdate => foldl_strict_update(m, reader, mc), + + ForceResultShallow => force_result_shallow(m, ctx, reader, mc), + ForceResultShallowPush => force_result_shallow_push(m, ctx, reader, mc), + ForceResultShallowLoop => force_result_shallow_loop(m, reader, mc), + ForceResultDeepFinish => force_result_deep_finish(m, ctx, reader, mc), + + CallPattern => call_pattern(m, ctx, reader, mc), + CallFunctor1 => call_functor_1(m, reader, mc), + CallFunctor2 => call_functor_2(m, reader, mc), + + Import => import(m, ctx, reader, mc), + ImportFinalize => import_finalize(m, ctx, reader), + ScopedImport => scoped_import(m, ctx, reader, mc), + ScopedImportFinalize => scoped_import_finalize(m, ctx, reader, mc), + + PathExists => path_exists(m, ctx, reader, mc), + ToPath => to_path(m, ctx, reader, mc), + IsPath => is_path(m, reader, mc), + ToString => to_string(m, ctx, reader, mc), + TypeOf => type_of(m, ctx, reader, mc), + + phase => todo!("primop phase {phase:?}"), + } +} diff --git a/fix-primops/src/list.rs b/fix-primops/src/list.rs new file mode 100644 index 0000000..f53937d --- /dev/null +++ b/fix-primops/src/list.rs @@ -0,0 +1,166 @@ +use fix_abstract_vm::{ + BytecodeReader, List, Machine, MachineExt, NixType, Step, StrictValue, Value, +}; +use fix_builtins::PrimOpPhase; +use gc_arena::Mutation; + +pub fn filter_force_list<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + m.force_slot(0, reader, mc)?; + let list = match m.peek_forced(0).expect_gc::() { + Ok(list) => list, + Err(got) => return m.finish_type_err(NixType::List, got), + }; + if list.inner.borrow().is_empty() { + let val = m.pop(); + return m.return_from_primop(val, reader); + } + // prepare stack layout: [ pred list idx acc ] + m.push(Value::new_inline(0)); + m.push(Value::new_gc(List::new_gc(mc))); + reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize); + Step::Continue(()) +} + +pub fn filter_call_pred<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + m.force_slot(3, reader, mc)?; + let pred = m.peek_forced(3); + #[allow(clippy::unwrap_used)] + let idx = m.peek(1).as_inline::().unwrap(); + #[allow(clippy::unwrap_used)] + let elem = m.peek_forced(2).as_gc::().unwrap().inner.borrow()[idx as usize]; + m.push(pred.relax()); + m.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize) +} + +pub fn filter_check<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let ret = m.force_and_retry::(reader, mc)?; + #[allow(clippy::unwrap_used)] + let idx = m.peek(1).as_inline::().unwrap(); + #[allow(clippy::unwrap_used)] + let list = m.peek_forced(2).as_gc::().unwrap(); + let list = list.inner.borrow(); + #[allow(clippy::unwrap_used)] + let acc = m.peek_forced(0).as_gc::().unwrap(); + if ret { + let mut acc = acc.unlock(mc).borrow_mut(); + acc.push(list[idx as usize]); + } + if idx as usize == list.len() - 1 { + let acc = m.pop(); + let _ = m.pop(); // idx + let _ = m.pop(); // list + let _ = m.pop(); // pred + return m.return_from_primop(acc, reader); + } + m.replace(1, Value::new_inline(idx + 1)); + reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize); + Step::Continue(()) +} + +// foldl' op nul list +// +// Stack layouts across phases: +// Entry: [op, nul, list] +// Empty: [op, nul] +// Call1: [op, list, idx, acc] +// Call2: [op, list, idx, acc, intermediate] +// Update: [op, list, idx, acc, result] +pub fn foldl_strict_entry<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + m.force_slot(0, reader, mc)?; + let list_val = m.peek_forced(0); + let Some(list) = list_val.as_gc::() else { + return m.finish_type_err(NixType::List, list_val.ty()); + }; + if list.inner.borrow().is_empty() { + let _ = m.pop(); // list + reader.set_pc(PrimOpPhase::FoldlStrictEmpty.ip() as usize); + return Step::Continue(()); + } + let list_val = m.pop(); + let nul_val = m.pop(); + m.push(list_val); + m.push(Value::new_inline(0i32)); + m.push(nul_val); + reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize); + Step::Continue(()) +} + +pub fn foldl_strict_empty<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let nul = m.force_and_retry::(reader, mc)?; + let _ = m.pop(); // op + m.return_from_primop(nul.relax(), reader) +} + +pub fn foldl_strict_call1<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + m.force_slot(3, reader, mc)?; + let op = m.peek_forced(3); + let acc = m.peek(0); + m.push(op.relax()); + m.call(reader, mc, acc, PrimOpPhase::FoldlStrictCall2.ip() as usize) +} + +pub fn foldl_strict_call2<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + #[allow(clippy::unwrap_used)] + let idx = m.peek(2).as_inline::().unwrap(); + #[allow(clippy::unwrap_used)] + let list = m.peek_forced(3).as_gc::().unwrap(); + let elem = list.inner.borrow()[idx as usize]; + m.call( + reader, + mc, + elem, + PrimOpPhase::FoldlStrictUpdate.ip() as usize, + ) +} + +pub fn foldl_strict_update<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + _mc: &Mutation<'gc>, +) -> Step { + let result = m.pop(); + m.replace(0, result); + #[allow(clippy::unwrap_used)] + let idx = m.peek(1).as_inline::().unwrap(); + #[allow(clippy::unwrap_used)] + let list = m.peek_forced(2).as_gc::().unwrap(); + let len = list.inner.borrow().len(); + if (idx as usize) + 1 == len { + let acc = m.pop(); + let _ = m.pop(); // idx + let _ = m.pop(); // list + let _ = m.pop(); // op + return m.return_from_primop(acc, reader); + } + m.replace(1, Value::new_inline(idx + 1)); + reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize); + Step::Continue(()) +} diff --git a/fix-primops/src/path.rs b/fix-primops/src/path.rs new file mode 100644 index 0000000..5dba511 --- /dev/null +++ b/fix-primops/src/path.rs @@ -0,0 +1,43 @@ +use fix_abstract_vm::{ + BytecodeReader, Machine, MachineExt, Path, Step, StrictValue, Value, VmRuntimeCtx, + VmRuntimeCtxExt, canon_path_str, +}; +use fix_error::Error; +use gc_arena::Mutation; + +pub fn to_path<'gc, M: Machine<'gc>>( + m: &mut M, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + // coerce to path THEN TO STRING + let val = m.force_and_retry::(reader, mc)?; + if let Some(Path(s)) = val.as_inline::() { + return m.return_from_primop(Value::new_inline(s), reader); + } + let Some(s) = ctx.get_string(val) else { + return m.finish_err(Error::eval_error(format!( + "cannot coerce {} to a path", + val.ty() + ))); + }; + if !s.starts_with('/') { + return m.finish_err(Error::eval_error(format!( + "string '{s}' doesn't represent an absolute path" + ))); + } + let canon = canon_path_str(s); + let sid = ctx.intern_string(canon); + m.return_from_primop(Value::new_inline(sid), reader) +} + +pub fn is_path<'gc, M: Machine<'gc>>( + m: &mut M, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, +) -> Step { + let val = m.force_and_retry::(reader, mc)?; + let is_path = val.is::(); + m.return_from_primop(Value::new_inline(is_path), reader) +} diff --git a/fix-vm/Cargo.toml b/fix-vm/Cargo.toml index edeb8a9..93cbe21 100644 --- a/fix-vm/Cargo.toml +++ b/fix-vm/Cargo.toml @@ -19,3 +19,5 @@ fix-builtins = { path = "../fix-builtins" } fix-codegen = { path = "../fix-codegen" } fix-common = { path = "../fix-common" } fix-error = { path = "../fix-error" } +fix-abstract-vm = { path = "../fix-abstract-vm" } +fix-primops = { path = "../fix-primops" } diff --git a/fix-vm/src/helpers.rs b/fix-vm/src/helpers.rs deleted file mode 100644 index ce0705e..0000000 --- a/fix-vm/src/helpers.rs +++ /dev/null @@ -1,7 +0,0 @@ -use fix_error::Error; - -use crate::VmError; - -pub(crate) fn vm_err(msg: impl Into) -> VmError { - VmError::Uncatchable(Error::eval_error(msg.into())) -} diff --git a/fix-vm/src/instructions/arithmetic.rs b/fix-vm/src/instructions/arithmetic.rs index 0d2c514..8e619eb 100644 --- a/fix-vm/src/instructions/arithmetic.rs +++ b/fix-vm/src/instructions/arithmetic.rs @@ -1,10 +1,9 @@ use std::cmp::Ordering; +use fix_abstract_vm::*; use gc_arena::{Gc, Mutation, RefLock}; -use crate::instructions::misc::canon_path_str; -use crate::value::*; -use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx, VmRuntimeCtxExt as _}; +use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx}; impl<'gc> crate::Vm<'gc> { #[inline(always)] @@ -15,7 +14,7 @@ impl<'gc> crate::Vm<'gc> { mc: &Mutation<'gc>, ) -> Step { let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?; - // CppNix: if the LHS is a path, the result is a path obtained by + // if the LHS is a path, the result is a path obtained by // canonicalizing the concatenated string. RHS may be a path or a // string. (A `string + path` keeps the string-typed result, handled // by the next branch.) @@ -30,7 +29,7 @@ impl<'gc> crate::Vm<'gc> { let combined = format!("{ls}{rs}"); let canon = canon_path_str(&combined); let sid = ctx.intern_string(canon); - self.push(Value::new_inline(crate::value::Path(sid))); + self.push(Value::new_inline(fix_abstract_vm::Path(sid))); return Step::Continue(()); } if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string_or_path(rhs)) { diff --git a/fix-vm/src/instructions/calls.rs b/fix-vm/src/instructions/calls.rs index 72c515c..a6e1c71 100644 --- a/fix-vm/src/instructions/calls.rs +++ b/fix-vm/src/instructions/calls.rs @@ -1,8 +1,8 @@ +use fix_abstract_vm::{resolve_operand, *}; use fix_builtins::PrimOpPhase; use fix_error::Error; use gc_arena::{Gc, Mutation, RefLock}; -use crate::value::*; use crate::{ BytecodeReader, CallFrame, Closure, Env, ForceMode, Step, ThunkState, VmRuntimeCtx, VmRuntimeCtxExt, @@ -120,7 +120,7 @@ impl<'gc> crate::Vm<'gc> { reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>, ) -> Step { - let arg = reader.read_operand_data(ctx).resolve(mc, self); + let arg = resolve_operand(&reader.read_operand_data(ctx), mc, self); let pc = reader.pc(); self.call(reader, mc, arg, pc) } @@ -170,4 +170,14 @@ impl<'gc> crate::Vm<'gc> { self.env = env; Step::Continue(()) } + + #[inline(always)] + pub(crate) fn op_dispatch_primop( + &mut self, + ctx: &mut impl VmRuntimeCtx, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + ) -> Step { + fix_primops::dispatch_primop(self, ctx, reader, mc) + } } diff --git a/fix-vm/src/instructions/collections.rs b/fix-vm/src/instructions/collections.rs index bd1f0e3..d42e84b 100644 --- a/fix-vm/src/instructions/collections.rs +++ b/fix-vm/src/instructions/collections.rs @@ -1,9 +1,9 @@ +use fix_abstract_vm::{NixType, resolve_operand}; use fix_common::StringId; use fix_error::Error; use gc_arena::{Gc, RefLock}; use smallvec::SmallVec; -use crate::value::NixType; use crate::{ AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt, }; @@ -43,13 +43,13 @@ impl<'gc> crate::Vm<'gc> { for _ in 0..static_count { let key = reader.read_string_id(); - let val = reader.read_operand_data(ctx).resolve(mc, self); + let val = resolve_operand(&reader.read_operand_data(ctx), mc, self); let _span_id = reader.read_u32(); kv.push((key, val)); } for key in dyn_keys { - let val = reader.read_operand_data(ctx).resolve(mc, self); + let val = resolve_operand(&reader.read_operand_data(ctx), mc, self); let _span_id = reader.read_u32(); if let Some(key) = key { kv.push((key, val)) @@ -314,7 +314,7 @@ impl<'gc> crate::Vm<'gc> { let count = reader.read_u32() as usize; let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count); for _ in 0..count { - items.push(reader.read_operand_data(ctx).resolve(mc, self)); + items.push(resolve_operand(&reader.read_operand_data(ctx), mc, self)); } let list = Gc::new( mc, diff --git a/fix-vm/src/instructions/control.rs b/fix-vm/src/instructions/control.rs index e30509f..2540921 100644 --- a/fix-vm/src/instructions/control.rs +++ b/fix-vm/src/instructions/control.rs @@ -1,7 +1,7 @@ +use fix_abstract_vm::*; use fix_error::Error; use gc_arena::Mutation; -use crate::value::*; use crate::{BytecodeReader, Step, VmRuntimeCtx}; impl<'gc> crate::Vm<'gc> { diff --git a/fix-vm/src/instructions/misc.rs b/fix-vm/src/instructions/misc.rs index 7a2d12c..4745e74 100644 --- a/fix-vm/src/instructions/misc.rs +++ b/fix-vm/src/instructions/misc.rs @@ -1,11 +1,11 @@ -use std::path::{Component, PathBuf}; +use std::path::PathBuf; +use fix_abstract_vm::{AttrSet, NixString, Path, StrictValue, canon_path_str}; use fix_builtins::BuiltinId; use fix_common::StringId; use fix_error::Error; use num_enum::TryFromPrimitive; -use crate::value::{AttrSet, NixString, Path, StrictValue}; use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt}; impl<'gc> crate::Vm<'gc> { @@ -163,20 +163,3 @@ fn resolve_path_str(current_dir: &str, path: &str) -> Result> }; Ok(canon_path_str(&raw)) } - -pub(crate) fn canon_path_str(path: impl AsRef) -> String { - let p = path.as_ref(); - let mut normalized = PathBuf::new(); - for component in p.components() { - match component { - Component::Prefix(p) => normalized.push(p.as_os_str()), - Component::RootDir => normalized.push("/"), - Component::CurDir => {} - Component::ParentDir => { - normalized.pop(); - } - Component::Normal(c) => normalized.push(c), - } - } - normalized.to_string_lossy().into_owned() -} diff --git a/fix-vm/src/instructions/with_scope.rs b/fix-vm/src/instructions/with_scope.rs index c8e0555..da608c6 100644 --- a/fix-vm/src/instructions/with_scope.rs +++ b/fix-vm/src/instructions/with_scope.rs @@ -1,8 +1,8 @@ +use fix_abstract_vm::{resolve_operand, *}; use fix_common::Symbol; use fix_error::Error; use smallvec::SmallVec; -use crate::value::*; use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx}; impl<'gc> crate::Vm<'gc> { @@ -20,7 +20,7 @@ impl<'gc> crate::Vm<'gc> { let n = reader.read_u8(); let mut namespaces = SmallVec::<[_; 2]>::new(); for _ in 0..n { - namespaces.push(reader.read_operand_data(ctx).resolve(mc, self)); + namespaces.push(resolve_operand(&reader.read_operand_data(ctx), mc, self)); } let resume_pc = reader.inst_start_pc(); diff --git a/fix-vm/src/lib.rs b/fix-vm/src/lib.rs index 51601fa..0ed0b4e 100644 --- a/fix-vm/src/lib.rs +++ b/fix-vm/src/lib.rs @@ -13,222 +13,17 @@ use fix_common::StringId; use fix_error::{Error, Result, Source}; use gc_arena::arena::CollectionPhase; use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable}; -use hashbrown::{HashMap, HashSet}; +use hashbrown::HashMap; use num_enum::TryFromPrimitive; use smallvec::SmallVec; -mod boxing; -mod bytecode_reader; #[cfg(feature = "tailcall")] mod dispatch_tailcall; -mod forced; -mod value; -pub use value::StaticValue; -use value::*; -mod helpers; +pub use fix_abstract_vm::*; mod instructions; -use bytecode_reader::BytecodeReader; -use forced::Forced; -use helpers::*; -mod primops; type VmResult = std::result::Result; -#[allow(dead_code)] -enum VmError { - Catchable(String), - Uncatchable(Box), -} - -impl From> for VmError { - fn from(e: Box) -> Self { - VmError::Uncatchable(e) - } -} - -impl VmError { - fn into_error(self) -> Box { - match self { - VmError::Catchable(_) => todo!("Check for tryEval catch frames"), - VmError::Uncatchable(e) => e, - } - } -} - -#[derive(Collect, Clone, Copy, Debug, PartialEq, Eq, Default)] -#[collect(require_static)] -pub enum ForceMode { - #[default] - AsIs, - Shallow, - Deep, -} - -pub trait VmContext { - fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx); -} - -pub trait VmRuntimeCtx { - fn intern_string(&mut self, s: impl AsRef) -> StringId; - fn resolve_string(&self, id: StringId) -> &str; - fn get_const(&self, id: u32) -> StaticValue; - fn add_const(&mut self, val: StaticValue) -> u32; -} - -pub trait VmCode { - fn bytecode(&self) -> &[u8]; - fn compile_with_scope( - &mut self, - source: Source, - extra_scope: Option, - ctx: &mut impl VmRuntimeCtx, - ) -> fix_error::Result; -} - -/// Extra scope passed to a re-entrant compile from inside a running VM. -/// -/// Currently only `ScopedImport` is produced (by the `scopedImport` builtin), -/// but the variant is kept open so REPL bindings could later land here too. -pub enum ExtraScope { - ScopedImport { - keys: HashSet, - slot_id: u32, - }, -} - -trait VmRuntimeCtxExt: VmRuntimeCtx { - fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>; - fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str>; - fn get_string_id<'a, 'gc: 'a>( - &'a mut self, - val: StrictValue<'gc>, - ) -> std::result::Result; - fn convert_value(&self, val: Value) -> fix_common::Value; -} - -impl VmRuntimeCtxExt for T { - fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> { - if let Some(sid) = val.as_inline::() { - Some(self.resolve_string(sid)) - } else { - val.as_gc::().map(|ns| ns.as_ref().as_str()) - } - } - - /// Like `get_string`, but also accepts `Path` values (returning their - /// underlying canonical-path string). Use this in places where Nix - /// would coerce a path to a string (string interpolation, file IO - /// builtins, etc.). - fn get_string_or_path<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> { - if let Some(p) = val.as_inline::() { - Some(self.resolve_string(p.0)) - } else { - self.get_string(val) - } - } - - fn get_string_id<'a, 'gc: 'a>( - &'a mut self, - val: StrictValue<'gc>, - ) -> std::result::Result { - if let Some(sid) = val.as_inline::() { - Ok(sid) - } else if let Some(s) = val.as_gc::().map(|ns| ns.as_ref().as_str()) { - Ok(self.intern_string(s)) - } else { - Err(val.ty()) - } - } - - fn convert_value(&self, val: Value) -> fix_common::Value { - self.convert_value_with_seen(val, &mut HashSet::new()) - } -} - -trait ConvertValueWithSeen: VmRuntimeCtx { - fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet) -> fix_common::Value; -} - -impl ConvertValueWithSeen for T { - fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet) -> fix_common::Value { - use fix_common::Value; - if let Some(i) = val.as_inline::() { - Value::Int(i as i64) - } else if let Some(gc_i) = val.as_gc::() { - Value::Int(*gc_i) - } else if let Some(f) = val.as_float() { - Value::Float(f) - } else if let Some(b) = val.as_inline::() { - Value::Bool(b) - } else if val.is::() { - Value::Null - } else if let Some(sid) = val.as_inline::() { - let s = self.resolve_string(sid).to_owned(); - Value::String(s) - } else if let Some(ns) = val.as_gc::() { - Value::String(ns.as_str().to_owned()) - } else if let Some(p) = val.as_inline::() { - Value::Path(self.resolve_string(p.0).to_owned()) - } else if let Some(attrs) = val.as_gc::() { - let bits = val.to_bits(); - if attrs.entries.is_empty() { - return Value::AttrSet(Default::default()); - } - if !seen.insert(bits) { - return Value::Repeated; - } - let mut map = std::collections::BTreeMap::new(); - for &(key, val) in attrs.entries.iter() { - let key = self.resolve_string(key).to_owned(); - let converted = self.convert_value_with_seen(val, seen); - map.insert(fix_common::Symbol::from(key), converted); - } - Value::AttrSet(fix_common::AttrSet::new(map)) - } else if let Some(list) = val.as_gc::() { - let bits = val.to_bits(); - if list.inner.borrow().is_empty() { - return Value::List(Default::default()); - } - if !seen.insert(bits) { - return Value::Repeated; - } - let items: Vec<_> = list - .inner - .borrow() - .iter() - .copied() - .map(|v| self.convert_value_with_seen(v, seen)) - .collect(); - Value::List(fix_common::List::new(items)) - } else if val.is::() { - Value::Func - } else if let Some(thunk) = val.as_gc::() { - if let ThunkState::Evaluated(v) = *thunk.borrow() { - self.convert_value_with_seen(v.relax(), seen) - } else { - Value::Thunk - } - } else if let Some(primop) = val.as_inline::() { - let name = fix_builtins::BUILTINS[primop.id as usize].0; - Value::PrimOp(name.strip_prefix("__").unwrap_or(name)) - } else if let Some(app) = val.as_gc::() { - let name = fix_builtins::BUILTINS[app.primop.id as usize].0; - Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name)) - } else { - Value::Null - } - } -} - -#[repr(u8)] -enum Break { - Force, - Done, - LoadFile, -} - -type Step = std::ops::ControlFlow; - #[derive(Collect)] #[collect(no_drop)] pub struct Vm<'gc> { @@ -259,66 +54,6 @@ pub struct Vm<'gc> { functor_sym: StringId, } -#[derive(Debug)] -pub(crate) struct PendingLoad { - pub path: PathBuf, - pub scope: Option, -} - -#[derive(Debug)] -pub(crate) struct PendingScope { - pub keys: HashSet, - pub slot_id: u32, -} - -enum OperandData { - Const(StaticValue), - BigInt(i64), - Local { layer: u8, idx: u32 }, - BuiltinConst(StringId), - Builtins, - ReplBinding(StringId), - ScopedImportBinding { slot_id: u32, name: StringId }, -} - -impl OperandData { - fn resolve<'gc>(&self, mc: &Mutation<'gc>, root: &Vm<'gc>) -> Value<'gc> { - use OperandData::*; - match *self { - Const(sv) => sv.into(), - BigInt(val) => Value::new_gc(Gc::new(mc, val)), - Local { layer, idx } => { - let mut cur = root.env; - for _ in 0..layer { - let prev = cur.borrow().prev.expect("env chain too short"); - cur = prev; - } - cur.borrow().locals[idx as usize] - } - #[allow(clippy::unwrap_used)] - BuiltinConst(id) => root - .builtins - .as_gc::() - .unwrap() - .lookup(id) - .unwrap(), - Builtins => root.builtins, - ReplBinding(_id) => todo!(), - ScopedImportBinding { slot_id, name } => { - #[allow(clippy::unwrap_used)] - let scope = root - .scope_slots - .get(slot_id as usize) - .expect("invalid scope slot"); - #[allow(clippy::unwrap_used)] - let attrs = scope.as_gc::().expect("scope must be attrset"); - #[allow(clippy::unwrap_used)] - attrs.lookup(name).expect("scoped binding not found") - } - } - } -} - fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> { let mut entries = SmallVec::with_capacity(BUILTINS.len()); @@ -441,7 +176,7 @@ impl<'gc> Vm<'gc> { #[inline(always)] #[must_use] - fn peek(&mut self, depth: usize) -> Value<'gc> { + fn peek(&self, depth: usize) -> Value<'gc> { *self .stack .get(self.stack.len() - depth - 1) @@ -450,7 +185,7 @@ impl<'gc> Vm<'gc> { #[inline(always)] #[must_use] - fn peek_forced(&mut self, depth: usize) -> StrictValue<'gc> { + fn peek_forced(&self, depth: usize) -> StrictValue<'gc> { self.stack .get(self.stack.len() - depth - 1) .expect("stack underflow") @@ -580,18 +315,168 @@ impl<'gc> Vm<'gc> { } } -#[allow(dead_code)] -struct ErrorFrame { - span_id: u32, - message: Option, -} +impl<'gc> Machine<'gc> for Vm<'gc> { + #[inline(always)] + fn push(&mut self, val: Value<'gc>) { + self.push(val); + } -#[derive(Collect, Debug)] -#[collect(no_drop)] -struct CallFrame<'gc> { - pc: usize, - thunk: Option>>, - env: Gc<'gc, RefLock>>, + #[inline(always)] + fn pop(&mut self) -> Value<'gc> { + self.pop() + } + + #[inline(always)] + fn peek(&self, depth: usize) -> Value<'gc> { + Vm::peek(self, depth) + } + + #[inline(always)] + fn peek_forced(&self, depth: usize) -> StrictValue<'gc> { + Vm::peek_forced(self, depth) + } + + #[inline(always)] + fn pop_forced(&mut self) -> StrictValue<'gc> { + self.pop_forced() + } + + #[inline(always)] + fn replace(&mut self, depth: usize, val: Value<'gc>) { + self.replace(depth, val); + } + + #[inline(always)] + fn stack_len(&self) -> usize { + self.stack.len() + } + + #[inline(always)] + fn force_slot_to_pc( + &mut self, + depth: usize, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + resume_pc: usize, + ) -> Step { + self.force_slot_to_pc(depth, reader, mc, resume_pc) + } + + #[inline(always)] + fn call( + &mut self, + reader: &mut BytecodeReader<'_>, + mc: &Mutation<'gc>, + arg: Value<'gc>, + resume_pc: usize, + ) -> Step { + self.call(reader, mc, arg, resume_pc) + } + + #[inline(always)] + fn push_call_frame(&mut self, frame: CallFrame<'gc>) { + self.call_stack.push(frame); + } + + #[inline(always)] + fn pop_call_frame(&mut self) -> Option> { + self.call_stack.pop() + } + + #[inline(always)] + fn call_depth(&self) -> usize { + self.call_depth + } + + #[inline(always)] + fn inc_call_depth(&mut self) { + self.call_depth += 1; + } + + #[inline(always)] + fn dec_call_depth(&mut self) { + self.call_depth -= 1; + } + + #[inline(always)] + fn env(&self) -> GcEnv<'gc> { + self.env + } + + #[inline(always)] + fn set_env(&mut self, env: GcEnv<'gc>) { + self.env = env; + } + + #[inline(always)] + fn finish_ok(&mut self, val: fix_common::Value) -> Step { + self.finish_ok(val) + } + + #[inline(always)] + fn finish_err(&mut self, err: Box) -> Step { + self.finish_err(err) + } + + #[inline(always)] + fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step { + self.finish_type_err(expected, got) + } + + #[inline(always)] + fn builtins(&self) -> Value<'gc> { + self.builtins + } + + #[inline(always)] + fn functor_sym(&self) -> StringId { + self.functor_sym + } + + #[inline(always)] + fn empty_list(&self) -> Value<'gc> { + self.empty_list + } + + #[inline(always)] + fn empty_attrs(&self) -> Value<'gc> { + self.empty_attrs + } + + #[inline(always)] + fn force_mode(&self) -> ForceMode { + self.force_mode + } + + #[inline(always)] + fn import_cache_get(&self, path: &std::path::Path) -> Option> { + self.import_cache.get(path).copied() + } + + #[inline(always)] + fn import_cache_insert(&mut self, path: PathBuf, val: Value<'gc>) { + self.import_cache.insert(path, val); + } + + #[inline(always)] + fn scope_slot(&self, idx: u32) -> Value<'gc> { + *self + .scope_slots + .get(idx as usize) + .expect("invalid scope slot") + } + + #[inline(always)] + fn scope_slots_push(&mut self, val: Value<'gc>) -> u32 { + let idx = self.scope_slots.len() as u32; + self.scope_slots.push(val); + idx + } + + #[inline(always)] + fn set_pending_load(&mut self, load: PendingLoad) { + self.pending_load = Some(load); + } } enum Action { @@ -600,11 +485,6 @@ enum Action { LoadFile(PendingLoad), } -enum NixNum { - Int(i64), - Float(f64), -} - impl Vm<'_> { pub fn run( ctx: &mut C, diff --git a/fix-vm/src/primops/attrs.rs b/fix-vm/src/primops/attrs.rs deleted file mode 100644 index 8b13789..0000000 --- a/fix-vm/src/primops/attrs.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fix-vm/src/primops/control.rs b/fix-vm/src/primops/control.rs deleted file mode 100644 index f6c3e2c..0000000 --- a/fix-vm/src/primops/control.rs +++ /dev/null @@ -1,364 +0,0 @@ -use fix_builtins::PrimOpPhase; -use fix_error::Error; -use gc_arena::{Gc, Mutation, RefLock}; -use smallvec::SmallVec; - -use crate::value::*; -use crate::{BytecodeReader, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt}; - -impl<'gc> Vm<'gc> { - pub(crate) fn primop_seq( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack: [e1, e2] - force e1, return e2 - self.force_slot(1, reader, mc)?; - let e2 = self.pop(); - let _ = self.pop(); - self.return_from_primop(e2, reader) - } - - pub(crate) fn primop_abort( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack: [msg] - force msg, then abort with it - self.force_slot(0, reader, mc)?; - let msg_val = self.peek_forced(0); - let msg = ctx.get_string(msg_val).unwrap_or(""); - self.finish_err(Error::eval_error(format!( - "evaluation aborted with the following error message: '{msg}'" - ))) - } - - pub(crate) fn primop_deep_seq_force_top( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack: [e1, e2] - force e1, return e2 - self.force_slot(1, reader, mc)?; - - let e1 = self.peek_forced(1); - - let children: SmallVec<_> = if let Some(attrs) = e1.as_gc::() { - let attrs = &attrs.entries; - if attrs.is_empty() { - SmallVec::new() - } else { - attrs.iter().map(|&(_, v)| v).collect() - } - } else if let Some(list) = e1.as_gc::>() { - let inner = list.inner.borrow(); - if inner.is_empty() { - SmallVec::new() - } else { - inner.iter().copied().collect() - } - } else { - SmallVec::new() - }; - - if children.is_empty() { - let e2 = self.pop(); - let _ = self.pop(); - return self.return_from_primop(e2, reader); - } - - let count = children.len() as i32; - let seen: Gc<'gc, List<'gc>> = Gc::new(mc, List::default()); - let worklist: Gc<'gc, List<'gc>> = List::new(mc, children); - - let e2 = self.pop(); - let _ = self.pop(); - self.push(e2); - self.push(Value::new_gc(seen)); - self.push(Value::new_gc(worklist)); - self.push(Value::new_inline(count)); - reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_deep_seq_push( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack: [e2, seen, worklist, counter] - #[allow(clippy::unwrap_used)] - let counter = self.peek(0).as_inline::().unwrap(); - if counter == 0 { - let _ = self.pop(); // counter - let _ = self.pop(); // worklist - let _ = self.pop(); // seen - let val = self.pop(); - return self.return_from_primop(val, reader); - } - - #[allow(clippy::unwrap_used)] - let worklist = self.peek_forced(1).as_gc::>().unwrap(); - #[allow(clippy::unwrap_used)] - let item = worklist.unlock(mc).borrow_mut().pop().unwrap(); - self.replace(0, Value::new_inline(counter - 1)); - self.push(item); - - // force item at TOS, resume at DeepSeqLoop after force - self.force_slot_to_pc(0, reader, mc, PrimOpPhase::DeepSeqLoop.ip() as usize)?; - reader.set_pc(PrimOpPhase::DeepSeqLoop.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_deep_seq_loop( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack after pop: [e2, seen, worklist, counter] - let item = self.pop(); - #[allow(clippy::unwrap_used)] - let counter = self.peek(0).as_inline::().unwrap(); - - let mut added: usize = 0; - if let Some(attrs) = item.as_gc::() { - let attrs = &attrs.entries; - #[allow(clippy::unwrap_used)] - let seen = self.peek_forced(2).as_gc::>().unwrap(); - if !self.is_value_in_seen(seen, item) { - self.add_value_to_seen(seen, mc, item); - #[allow(clippy::unwrap_used)] - let worklist = self.peek_forced(1).as_gc::>().unwrap(); - { - let mut wl = worklist.unlock(mc).borrow_mut(); - for &(_, v) in attrs.iter() { - wl.push(v); - } - added = attrs.len(); - } - } - } else if let Some(list) = item.as_gc::>() { - #[allow(clippy::unwrap_used)] - let seen = self.peek_forced(2).as_gc::>().unwrap(); - if !self.is_value_in_seen(seen, item) { - self.add_value_to_seen(seen, mc, item); - #[allow(clippy::unwrap_used)] - let worklist = self.peek_forced(1).as_gc::>().unwrap(); - { - let inner = list.inner.borrow(); - let mut wl = worklist.unlock(mc).borrow_mut(); - for &v in inner.iter() { - wl.push(v); - } - added = inner.len(); - } - } - } - - self.replace(0, Value::new_inline(counter + added as i32)); - reader.set_pc(PrimOpPhase::DeepSeqPush.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_force_result_shallow( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - self.force_slot(0, reader, mc)?; - let val = self.peek_forced(0); - - let (count, has_children) = if let Some(attrs) = val.as_gc::() { - let len = attrs.entries.len(); - (len, len > 0) - } else if let Some(list) = val.as_gc::>() { - let len = list.inner.borrow().len(); - (len, len > 0) - } else { - (0, false) - }; - - if !has_children { - let val = self.pop(); - return self.finish_ok(ctx.convert_value(val)); - } - - self.push(Value::new_inline(0i32)); - self.push(Value::new_inline(count as i32)); - reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_force_result_shallow_push( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - #[allow(clippy::unwrap_used)] - let idx = self.peek(1).as_inline::().unwrap(); - #[allow(clippy::unwrap_used)] - let len = self.peek(0).as_inline::().unwrap(); - - if idx == len { - let _ = self.pop(); // len - let _ = self.pop(); // idx - let val = self.pop(); - return self.finish_ok(ctx.convert_value(val)); - } - - let val = self.peek_forced(2); - let child = if let Some(attrs) = val.as_gc::() { - attrs.entries.get(idx as usize).map(|&(_, v)| v) - } else if let Some(list) = val.as_gc::>() { - list.inner.borrow().get(idx as usize).copied() - } else { - None - }; - - if let Some(child) = child { - self.replace(1, Value::new_inline(idx + 1)); - self.push(child); - self.force_slot_to_pc( - 0, - reader, - mc, - PrimOpPhase::ForceResultShallowLoop.ip() as usize, - )?; - reader.set_pc(PrimOpPhase::ForceResultShallowLoop.ip() as usize); - } - Step::Continue(()) - } - - pub(crate) fn primop_force_result_shallow_loop( - &mut self, - reader: &mut BytecodeReader<'_>, - _mc: &Mutation<'gc>, - ) -> Step { - let _ = self.pop(); // forced child - reader.set_pc(PrimOpPhase::ForceResultShallowPush.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_force_result_deep_finish( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let val = self.force_and_retry::(reader, mc)?; - self.finish_ok(ctx.convert_value(val.relax())) - } - - fn is_value_in_seen(&self, seen: Gc<'gc, List<'gc>>, val: Value<'gc>) -> bool { - if !is_container(val) { - return false; - } - let target = val.to_bits(); - for &v in seen.inner.borrow().iter() { - if v.to_bits() == target { - return true; - } - } - false - } - - fn add_value_to_seen(&self, seen: Gc<'gc, List<'gc>>, mc: &Mutation<'gc>, val: Value<'gc>) { - if is_container(val) { - seen.unlock(mc).borrow_mut().push(val); - } - } - - pub(crate) fn primop_call_functor_1( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // Stack invariant on every (re-)entry: [..., orig_arg, self, functor] - // where `functor` is TOS. Retries during force land back here safely. - let functor = self.force_and_retry::(reader, mc)?; - // Stack now: [..., orig_arg, self] - let self_val = self.pop(); - self.push(functor.relax()); - // Stack: [..., orig_arg, functor] - // Call 1: functor(self). Resume into CallFunctor2 once it returns. - self.call( - reader, - mc, - self_val, - PrimOpPhase::CallFunctor2.ip() as usize, - ) - } - - pub(crate) fn primop_call_functor_2( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // Stack on entry: [..., orig_arg, intermediate] - // call_stack top: synthetic frame with caller's resume_pc. - let intermediate = self.pop(); - let orig_arg = self.pop(); - let saved = self.call_stack.pop().expect("functor outer frame missing"); - self.env = saved.env; - self.push(intermediate); - // Call 2: intermediate(orig_arg). Resume to caller. - self.call(reader, mc, orig_arg, saved.pc) - } - - pub(crate) fn primop_call_pattern( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let (func, attrset) = self.force_and_retry::<(Gc, Gc)>(reader, mc)?; - - let Closure { - ip, - n_locals, - env, - pattern, - } = *func; - let Some(pattern) = pattern else { - unreachable!() - }; - // TODO: get function name - // TODO: param spans - if !pattern.ellipsis { - for key in pattern.required.iter().copied() { - if attrset.lookup(key).is_none() { - let name = ctx.resolve_string(key); - return self.finish_err(Error::eval_error(format!( - "function 'anonymous lambda' called without required argument '{name}'" - ))); - } - } - for &(key, _) in attrset.entries.iter() { - let is_expected = - pattern.required.contains(&key) || pattern.optional.contains(&key); - if !is_expected { - let name = ctx.resolve_string(key); - return self.finish_err(Error::eval_error(format!( - "function 'anonymous lambda' called with unexpected argument '{name}'" - ))); - } - } - } - - let new_env = Gc::new( - mc, - RefLock::new(Env::with_arg(Value::new_gc(attrset), n_locals, env)), - ); - reader.set_pc(ip as usize); - self.env = new_env; - - Step::Continue(()) - } -} - -fn is_container(val: Value<'_>) -> bool { - val.is::() || val.is::>() -} diff --git a/fix-vm/src/primops/conv.rs b/fix-vm/src/primops/conv.rs deleted file mode 100644 index 1dd1aef..0000000 --- a/fix-vm/src/primops/conv.rs +++ /dev/null @@ -1,53 +0,0 @@ -use fix_common::StringId; -use fix_error::Error; -use gc_arena::Mutation; - -use crate::bytecode_reader::BytecodeReader; -use crate::value::*; -use crate::{Step, Vm, VmRuntimeCtx}; - -impl<'gc> Vm<'gc> { - pub(crate) fn primop_to_string( - &mut self, - _ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let val = self.force_and_retry::(reader, mc)?; - if val.is::() || val.is::() { - return self.return_from_primop(val.relax(), reader); - } - if let Some(p) = val.as_inline::() { - return self.return_from_primop(Value::new_inline(p.0), reader); - } - // TODO: derivations / `__toString` / `outPath`, - // numbers, lists. - self.finish_err(Error::eval_error(format!( - "cannot coerce {} to a string", - val.ty() - ))) - } - - pub(crate) fn primop_type_of( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let val = self.force_and_retry::(reader, mc)?; - let name: &str = match val.ty() { - NixType::Int => "int", - NixType::Float => "float", - NixType::Bool => "bool", - NixType::Null => "null", - NixType::String => "string", - NixType::Path => "path", - NixType::AttrSet => "set", - NixType::List => "list", - NixType::Closure | NixType::PrimOp | NixType::PrimOpApp => "lambda", - NixType::Thunk => unreachable!("forced"), - }; - let sid = ctx.intern_string(name); - self.return_from_primop(Value::new_inline(sid), reader) - } -} diff --git a/fix-vm/src/primops/io.rs b/fix-vm/src/primops/io.rs deleted file mode 100644 index 8f1d9ff..0000000 --- a/fix-vm/src/primops/io.rs +++ /dev/null @@ -1,193 +0,0 @@ -use std::path::PathBuf; - -use fix_builtins::PrimOpPhase; -use fix_common::StringId; -use fix_error::Error; -use gc_arena::{Gc, Mutation}; -use hashbrown::HashSet; - -use crate::bytecode_reader::BytecodeReader; -use crate::instructions::misc::canon_path_str; -use crate::value::*; -use crate::{Break, CallFrame, PendingLoad, PendingScope, Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt}; - -impl<'gc> Vm<'gc> { - pub(crate) fn primop_import( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack: [path] - let path_val = self.force_and_retry::(reader, mc)?; - let path_str = match ctx.get_string_or_path(path_val) { - Some(s) => s.to_owned(), - None => { - return self.finish_err(Error::eval_error(format!( - "expected a path or string, got {}", - path_val.ty() - ))); - } - }; - let abs = match resolve_import_target(&path_str) { - Ok(p) => p, - Err(e) => return self.finish_err(e), - }; - - if let Some(&cached) = self.import_cache.get(&abs) { - return self.return_from_primop(cached, reader); - } - - // Stash the resolved path on the stack as a string-id so the - // finalizer can use it as the cache key. The slot we pop here was - // freed by `force_and_retry`, so we simply push. - let path_sid = ctx.intern_string(abs.to_string_lossy()); - self.push(Value::new_inline(path_sid)); - self.call_stack.push(CallFrame { - pc: PrimOpPhase::ImportFinalize.ip() as usize, - thunk: None, - env: self.env, - }); - - self.pending_load = Some(PendingLoad { - path: abs, - scope: None, - }); - Step::Break(Break::LoadFile) - } - - pub(crate) fn primop_import_finalize( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - ) -> Step { - // stack: [path_sid, return_value] - let val = self.pop(); - #[allow(clippy::unwrap_used)] - let path_sid = self.pop().as_inline::().unwrap(); - // The cache key is keyed by the absolute path string we interned in - // `primop_import`. Resolve it back to the host PathBuf. - let path_str = ctx.resolve_string(path_sid).to_owned(); - self.import_cache.insert(PathBuf::from(path_str), val); - self.push(val); - let Some(CallFrame { - pc: ret_pc, - thunk: _, - env, - }) = self.call_stack.pop() - else { - unreachable!() - }; - reader.set_pc(ret_pc); - // FIXME: - // self.call_depth -= 1; - self.env = env; - Step::Continue(()) - } - - pub(crate) fn primop_scoped_import( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // stack: [scope, path] - let (scope_attrs, path_val) = - self.force_and_retry::<(Gc, StrictValue)>(reader, mc)?; - let path_str = match ctx.get_string_or_path(path_val) { - Some(s) => s.to_owned(), - None => { - return self.finish_err(Error::eval_error(format!( - "expected a path or string, got {}", - path_val.ty() - ))); - } - }; - let abs = match resolve_import_target(&path_str) { - Ok(p) => p, - Err(e) => return self.finish_err(e), - }; - - let keys: HashSet = scope_attrs.entries.iter().map(|&(k, _)| k).collect(); - let slot_id = self.scope_slots.len() as u32; - self.scope_slots.push(Value::new_gc(scope_attrs)); - - self.call_stack.push(CallFrame { - pc: PrimOpPhase::ScopedImportFinalize.ip() as usize, - thunk: None, - env: self.env, - }); - - self.pending_load = Some(PendingLoad { - path: abs, - scope: Some(PendingScope { keys, slot_id }), - }); - Step::Break(Break::LoadFile) - } - - pub(crate) fn primop_scoped_import_finalize( - &mut self, - _ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - _mc: &Mutation<'gc>, - ) -> Step { - // stack: [return_value] - // We intentionally do NOT pop the slot from `scope_slots` so that - // closures or thunks created inside the imported file can still - // resolve their scope after `scopedImport` returns. - let val = self.pop(); - self.return_from_primop(val, reader) - } - - pub(crate) fn primop_path_exists( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let path_val = self.force_and_retry::(reader, mc)?; - // CppNix: pathExists requires an absolute path. A `Path` value is - // always absolute; a string is accepted only if it starts with `/`. - let (path, is_path_value) = if let Some(p) = path_val.as_inline::() { - (ctx.resolve_string(p.0).to_owned(), true) - } else if let Some(s) = ctx.get_string(path_val) { - (s.to_owned(), false) - } else { - return self.finish_err(Error::eval_error(format!( - "expected a path or string, got {}", - path_val.ty() - ))); - }; - if !is_path_value && !path.starts_with('/') { - return self.finish_err(Error::eval_error(format!( - "string '{path}' doesn't represent an absolute path" - ))); - } - // CppNix collapses consecutive slashes and resolves `.` / `..` lexically - // before checking. Trailing-slash / trailing-dot mean "must be a directory". - let must_be_dir = path.ends_with('/') || path.ends_with("/."); - let canon = canon_path_str(&path); - let p = std::path::Path::new(&canon); - let exists = if must_be_dir { - std::fs::metadata(p).map(|m| m.is_dir()).unwrap_or(false) - } else { - std::fs::symlink_metadata(p).is_ok() - }; - self.return_from_primop(Value::new_inline(exists), reader) - } -} - -/// Convert the user-supplied path string into an absolute, dotted-segment -/// resolved `PathBuf` and append `default.nix` if the target is a directory. -fn resolve_import_target(path: &str) -> Result> { - let mut abs = PathBuf::from(path); - if !abs.is_absolute() { - return Err(Error::eval_error(format!( - "import: expected an absolute path, got '{path}'" - ))); - } - if abs.is_dir() { - abs.push("default.nix"); - } - Ok(abs) -} diff --git a/fix-vm/src/primops/list.rs b/fix-vm/src/primops/list.rs deleted file mode 100644 index ca17b6b..0000000 --- a/fix-vm/src/primops/list.rs +++ /dev/null @@ -1,169 +0,0 @@ -use fix_builtins::PrimOpPhase; -use gc_arena::Mutation; - -use crate::bytecode_reader::BytecodeReader; -use crate::value::*; -use crate::{Step, Vm}; - -impl<'gc> Vm<'gc> { - pub(crate) fn primop_filter_force_list( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - self.force_slot(0, reader, mc)?; - let list = match self.peek_forced(0).expect_gc::() { - Ok(list) => list, - Err(got) => return self.finish_type_err(NixType::List, got), - }; - if list.inner.borrow().is_empty() { - let val = self.pop(); - return self.return_from_primop(val, reader); - } - // prepare stack layout: [ pred list idx acc ] - self.push(Value::new_inline(0)); - self.push(Value::new_gc(List::new_gc(mc))); - reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_filter_call_pred( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - self.force_slot(3, reader, mc)?; - let pred = self.peek_forced(3); - #[allow(clippy::unwrap_used)] - let idx = self.peek(1).as_inline::().unwrap(); - #[allow(clippy::unwrap_used)] - let elem = self.peek_forced(2).as_gc::().unwrap().inner.borrow()[idx as usize]; - self.push(pred.relax()); - self.call(reader, mc, elem, PrimOpPhase::FilterCheck.ip() as usize) - } - - pub(crate) fn primop_filter_check( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let ret = self.force_and_retry::(reader, mc)?; - #[allow(clippy::unwrap_used)] - let idx = self.peek(1).as_inline::().unwrap(); - #[allow(clippy::unwrap_used)] - let list = self.peek_forced(2).as_gc::().unwrap(); - let list = list.inner.borrow(); - #[allow(clippy::unwrap_used)] - let acc = self.peek_forced(0).as_gc::().unwrap(); - if ret { - let mut acc = acc.unlock(mc).borrow_mut(); - acc.push(list[idx as usize]); - } - if idx as usize == list.len() - 1 { - let acc = self.pop(); - let _ = self.pop(); // idx - let _ = self.pop(); // list - let _ = self.pop(); // pred - return self.return_from_primop(acc, reader); - } - self.replace(1, Value::new_inline(idx + 1)); - reader.set_pc(PrimOpPhase::FilterCallPred.ip() as usize); - Step::Continue(()) - } - - // foldl' op nul list - // - // Stack layouts across phases: - // Entry: [op, nul, list] - // Empty: [op, nul] - // Call1: [op, list, idx, acc] - // Call2: [op, list, idx, acc, intermediate] - // Update: [op, list, idx, acc, result] - pub(crate) fn primop_foldl_strict_entry( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - self.force_slot(0, reader, mc)?; - let list_val = self.peek_forced(0); - let Some(list) = list_val.as_gc::() else { - return self.finish_type_err(NixType::List, list_val.ty()); - }; - if list.inner.borrow().is_empty() { - let _ = self.pop(); // list - reader.set_pc(PrimOpPhase::FoldlStrictEmpty.ip() as usize); - return Step::Continue(()); - } - let list_val = self.pop(); - let nul_val = self.pop(); - self.push(list_val); - self.push(Value::new_inline(0i32)); - self.push(nul_val); - reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize); - Step::Continue(()) - } - - pub(crate) fn primop_foldl_strict_empty( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let nul = self.force_and_retry::(reader, mc)?; - let _ = self.pop(); // op - self.return_from_primop(nul.relax(), reader) - } - - pub(crate) fn primop_foldl_strict_call1( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - self.force_slot(3, reader, mc)?; - let op = self.peek_forced(3); - let acc = self.peek(0); - self.push(op.relax()); - self.call(reader, mc, acc, PrimOpPhase::FoldlStrictCall2.ip() as usize) - } - - pub(crate) fn primop_foldl_strict_call2( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - #[allow(clippy::unwrap_used)] - let idx = self.peek(2).as_inline::().unwrap(); - #[allow(clippy::unwrap_used)] - let list = self.peek_forced(3).as_gc::().unwrap(); - let elem = list.inner.borrow()[idx as usize]; - self.call( - reader, - mc, - elem, - PrimOpPhase::FoldlStrictUpdate.ip() as usize, - ) - } - - pub(crate) fn primop_foldl_strict_update( - &mut self, - reader: &mut BytecodeReader<'_>, - _mc: &Mutation<'gc>, - ) -> Step { - let result = self.pop(); - self.replace(0, result); - #[allow(clippy::unwrap_used)] - let idx = self.peek(1).as_inline::().unwrap(); - #[allow(clippy::unwrap_used)] - let list = self.peek_forced(2).as_gc::().unwrap(); - let len = list.inner.borrow().len(); - if (idx as usize) + 1 == len { - let acc = self.pop(); - let _ = self.pop(); // idx - let _ = self.pop(); // list - let _ = self.pop(); // op - return self.return_from_primop(acc, reader); - } - self.replace(1, Value::new_inline(idx + 1)); - reader.set_pc(PrimOpPhase::FoldlStrictCall1.ip() as usize); - Step::Continue(()) - } -} diff --git a/fix-vm/src/primops/mod.rs b/fix-vm/src/primops/mod.rs deleted file mode 100644 index ff9bc3b..0000000 --- a/fix-vm/src/primops/mod.rs +++ /dev/null @@ -1,90 +0,0 @@ -use fix_builtins::PrimOpPhase; -use fix_error::Error; -use gc_arena::Mutation; -use num_enum::TryFromPrimitive; - -use crate::bytecode_reader::BytecodeReader; -use crate::value::Value; -use crate::{CallFrame, Step, Vm, VmRuntimeCtx}; - -mod attrs; -mod control; -mod conv; -mod io; -mod list; -mod path; -mod regex; -mod version; - -impl<'gc> Vm<'gc> { - #[allow(clippy::too_many_lines)] - pub(crate) fn op_dispatch_primop( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - use PrimOpPhase::*; - let phase_disc = reader.read_u8(); - let Ok(phase) = PrimOpPhase::try_from_primitive(phase_disc) else { - return self.finish_err(Error::eval_error("invalid primop phase")); - }; - match phase { - Abort => self.primop_abort(ctx, reader, mc), - - DeepSeq => self.primop_deep_seq_force_top(reader, mc), - DeepSeqPush => self.primop_deep_seq_push(reader, mc), - DeepSeqLoop => self.primop_deep_seq_loop(reader, mc), - Seq => self.primop_seq(reader, mc), - - FilterForceList => self.primop_filter_force_list(reader, mc), - FilterCallPred => self.primop_filter_call_pred(reader, mc), - FilterCheck => self.primop_filter_check(reader, mc), - - FoldlStrict => self.primop_foldl_strict_entry(reader, mc), - FoldlStrictEmpty => self.primop_foldl_strict_empty(reader, mc), - FoldlStrictCall1 => self.primop_foldl_strict_call1(reader, mc), - FoldlStrictCall2 => self.primop_foldl_strict_call2(reader, mc), - FoldlStrictUpdate => self.primop_foldl_strict_update(reader, mc), - - ForceResultShallow => self.primop_force_result_shallow(ctx, reader, mc), - ForceResultShallowPush => self.primop_force_result_shallow_push(ctx, reader, mc), - ForceResultShallowLoop => self.primop_force_result_shallow_loop(reader, mc), - ForceResultDeepFinish => self.primop_force_result_deep_finish(ctx, reader, mc), - - CallPattern => self.primop_call_pattern(ctx, reader, mc), - CallFunctor1 => self.primop_call_functor_1(reader, mc), - CallFunctor2 => self.primop_call_functor_2(reader, mc), - - Import => self.primop_import(ctx, reader, mc), - ImportFinalize => self.primop_import_finalize(ctx, reader), - ScopedImport => self.primop_scoped_import(ctx, reader, mc), - ScopedImportFinalize => self.primop_scoped_import_finalize(ctx, reader, mc), - - PathExists => self.primop_path_exists(ctx, reader, mc), - ToPath => self.primop_to_path(ctx, reader, mc), - IsPath => self.primop_is_path(reader, mc), - ToString => self.primop_to_string(ctx, reader, mc), - TypeOf => self.primop_type_of(ctx, reader, mc), - - phase => todo!("primop phase {phase:?}"), - } - } - - #[inline(always)] - fn return_from_primop(&mut self, val: Value<'gc>, reader: &mut BytecodeReader<'_>) -> Step { - self.push(val); - let Some(CallFrame { - pc: ret_pc, - thunk: _, - env, - }) = self.call_stack.pop() - else { - unreachable!() - }; - reader.set_pc(ret_pc); - self.call_depth -= 1; - self.env = env; - Step::Continue(()) - } -} diff --git a/fix-vm/src/primops/path.rs b/fix-vm/src/primops/path.rs deleted file mode 100644 index 24550f6..0000000 --- a/fix-vm/src/primops/path.rs +++ /dev/null @@ -1,46 +0,0 @@ -use fix_error::Error; -use gc_arena::Mutation; - -use crate::bytecode_reader::BytecodeReader; -use crate::instructions::misc::canon_path_str; -use crate::value::*; -use crate::{Step, Vm, VmRuntimeCtx, VmRuntimeCtxExt}; - -impl<'gc> Vm<'gc> { - pub(crate) fn primop_to_path( - &mut self, - ctx: &mut impl VmRuntimeCtx, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - // coerce to path THEN TO STRING - let val = self.force_and_retry::(reader, mc)?; - if let Some(Path(s)) = val.as_inline::() { - return self.return_from_primop(Value::new_inline(s), reader); - } - let Some(s) = ctx.get_string(val) else { - return self.finish_err(Error::eval_error(format!( - "cannot coerce {} to a path", - val.ty() - ))); - }; - if !s.starts_with('/') { - return self.finish_err(Error::eval_error(format!( - "string '{s}' doesn't represent an absolute path" - ))); - } - let canon = canon_path_str(s); - let sid = ctx.intern_string(canon); - self.return_from_primop(Value::new_inline(sid), reader) - } - - pub(crate) fn primop_is_path( - &mut self, - reader: &mut BytecodeReader<'_>, - mc: &Mutation<'gc>, - ) -> Step { - let val = self.force_and_retry::(reader, mc)?; - let is_path = val.is::(); - self.return_from_primop(Value::new_inline(is_path), reader) - } -} diff --git a/fix-vm/src/primops/regex.rs b/fix-vm/src/primops/regex.rs deleted file mode 100644 index 8b13789..0000000 --- a/fix-vm/src/primops/regex.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fix-vm/src/primops/string.rs b/fix-vm/src/primops/string.rs deleted file mode 100644 index e69de29..0000000 diff --git a/fix-vm/src/primops/version.rs b/fix-vm/src/primops/version.rs deleted file mode 100644 index 8b13789..0000000 --- a/fix-vm/src/primops/version.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/fix/Cargo.toml b/fix/Cargo.toml index 493224e..620a2ec 100644 --- a/fix/Cargo.toml +++ b/fix/Cargo.toml @@ -33,6 +33,7 @@ rnix = { workspace = true } ere = { workspace = true } ghost-cell = { workspace = true } +fix-abstract-vm = { path = "../fix-abstract-vm" } fix-builtins = { path = "../fix-builtins" } fix-common = { path = "../fix-common" } fix-codegen = { path = "../fix-codegen" } diff --git a/fix/src/lib.rs b/fix/src/lib.rs index 42ea6af..7239630 100644 --- a/fix/src/lib.rs +++ b/fix/src/lib.rs @@ -2,6 +2,7 @@ #![allow(dead_code)] use bumpalo::Bump; +use fix_abstract_vm::{ForceMode, StaticValue, VmCode, VmContext, VmRuntimeCtx}; use fix_builtins::PrimOpPhase; use fix_codegen::disassembler::{Disassembler, DisassemblerContext}; use fix_codegen::{BytecodeContext, InstructionPtr, Op}; @@ -12,7 +13,7 @@ use fix_ir::{ GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef, ThunkId, }; -use fix_vm::{ForceMode, StaticValue, Vm, VmCode, VmContext, VmRuntimeCtx}; +use fix_vm::Vm; use ghost_cell::{GhostCell, GhostToken}; use hashbrown::{HashMap, HashSet}; use string_interner::{DefaultStringInterner, Symbol as _}; @@ -155,7 +156,7 @@ impl VmCode for CodeState { fn compile_with_scope( &mut self, source: Source, - extra_scope: Option, + extra_scope: Option, runtime: &mut impl VmRuntimeCtx, ) -> Result { let mut compiler = CompilerCtx { @@ -163,7 +164,7 @@ impl VmCode for CodeState { runtime, }; let extra = extra_scope.map(|s| match s { - fix_vm::ExtraScope::ScopedImport { keys, slot_id } => { + fix_abstract_vm::ExtraScope::ScopedImport { keys, slot_id } => { ExtraScope::ScopedImport { keys, slot_id } } });