Compare commits

..

26 Commits

Author SHA1 Message Date
imxyy1soope1 4aa694aa3a STW 2026-05-17 17:55:37 +08:00
imxyy1soope1 d98e389606 implement string context 2026-05-17 17:04:32 +08:00
imxyy1soope1 9a17990d5e deep equal 2026-05-17 15:30:25 +08:00
imxyy1soope1 29fab93cd1 refactor: abstract VM 2026-05-16 19:51:38 +08:00
imxyy1soope1 21899f7380 implement Path type 2026-05-16 19:51:00 +08:00
imxyy1soope1 3d07f89afe implement foldl' 2026-05-08 21:09:54 +08:00
imxyy1soope1 cfd2df5d0e implement import & scopedImport (WIP, ResolvePath resolves to string) 2026-05-08 18:52:18 +08:00
imxyy1soope1 49392f66f8 fix: null dynamic attrs 2026-05-08 17:46:23 +08:00
imxyy1soope1 4aff27142c implement __functor 2026-05-08 17:46:23 +08:00
imxyy1soope1 62d65b2e5f refactor primops 2026-05-08 17:46:23 +08:00
imxyy1soope1 b3e6591809 refactor with 2026-05-08 17:46:23 +08:00
imxyy1soope1 fca00b04ba implement Assert 2026-05-08 17:46:23 +08:00
imxyy1soope1 88a205f419 document trying mechanism 2026-05-08 17:46:23 +08:00
imxyy1soope1 7401f1ba5e chore: update flake.lock 2026-05-08 17:46:23 +08:00
imxyy1soope1 9d10fa7da3 ConcatStrings 2026-05-08 17:46:23 +08:00
imxyy1soope1 1550868e90 implement dynamic key; implement __curPos; other small changes 2026-05-08 17:46:23 +08:00
imxyy1soope1 035ebd3808 implement |> and <| 2026-05-08 17:46:23 +08:00
imxyy1soope1 06e73fc9be avoid thunking trivial values 2026-05-08 17:46:23 +08:00
imxyy1soope1 47b1344ebe refactor: use GAT in enum Ir 2026-05-08 17:46:23 +08:00
imxyy1soope1 6659b22dce layer: usize -> u8 2026-05-08 17:46:22 +08:00
imxyy1soope1 045f0bd6de temp 2026-05-08 17:46:22 +08:00
imxyy1soope1 fe96f6d9c5 implement pattern calling 2026-05-08 17:46:22 +08:00
imxyy1soope1 a28dfada30 implement unary operations 2026-05-08 17:46:22 +08:00
imxyy1soope1 103928779f ForceMode 2026-05-08 17:46:22 +08:00
imxyy1soope1 c08e0b81c4 implement __seq and __deepSeq 2026-05-08 17:46:22 +08:00
imxyy1soope1 26717a8184 implement primop (filter) 2026-05-08 17:46:22 +08:00
50 changed files with 3028 additions and 1574 deletions
Generated
+184 -1
View File
@@ -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,20 +544,39 @@ 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",
"sptr",
"string-interner",
"sysinfo",
]
[[package]]
@@ -841,6 +878,15 @@ dependencies = [
"libc",
]
[[package]]
name = "ntapi"
version = "0.4.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3b335231dfd352ffb0f8017f3b6027a4917f7df785ea2143d8af2adc66980ae"
dependencies = [
"winapi",
]
[[package]]
name = "nu-ansi-term"
version = "0.50.3"
@@ -881,6 +927,25 @@ dependencies = [
"syn",
]
[[package]]
name = "objc2-core-foundation"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a180dd8642fa45cdb7dd721cd4c11b1cadd4929ce112ebd8b9f5803cc79d536"
dependencies = [
"bitflags",
]
[[package]]
name = "objc2-io-kit"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33fafba39597d6dc1fb709123dfa8289d39406734be322956a69f0931c73bb15"
dependencies = [
"libc",
"objc2-core-foundation",
]
[[package]]
name = "object"
version = "0.37.3"
@@ -1286,6 +1351,20 @@ dependencies = [
"syn",
]
[[package]]
name = "sysinfo"
version = "0.38.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92ab6a2f8bfe508deb3c6406578252e491d299cbbf3bc0529ecc3313aee4a52f"
dependencies = [
"libc",
"memchr",
"ntapi",
"objc2-core-foundation",
"objc2-io-kit",
"windows",
]
[[package]]
name = "tempfile"
version = "3.27.0"
@@ -1679,12 +1758,107 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
[[package]]
name = "windows"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "527fadee13e0c05939a6a05d5bd6eec6cd2e3dbd648b9f8e447c6518133d8580"
dependencies = [
"windows-collections",
"windows-core",
"windows-future",
"windows-numerics",
]
[[package]]
name = "windows-collections"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b2d95af1a8a14a3c7367e1ed4fc9c20e0a26e79551b1454d72583c97cc6610"
dependencies = [
"windows-core",
]
[[package]]
name = "windows-core"
version = "0.62.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
dependencies = [
"windows-implement",
"windows-interface",
"windows-link",
"windows-result",
"windows-strings",
]
[[package]]
name = "windows-future"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e1d6f90251fe18a279739e78025bd6ddc52a7e22f921070ccdc67dde84c605cb"
dependencies = [
"windows-core",
"windows-link",
"windows-threading",
]
[[package]]
name = "windows-implement"
version = "0.60.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-interface"
version = "0.59.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-numerics"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e2e40844ac143cdb44aead537bbf727de9b044e107a0f1220392177d15b0f26"
dependencies = [
"windows-core",
"windows-link",
]
[[package]]
name = "windows-result"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-strings"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
dependencies = [
"windows-link",
]
[[package]]
name = "windows-sys"
version = "0.61.2"
@@ -1694,6 +1868,15 @@ dependencies = [
"windows-link",
]
[[package]]
name = "windows-threading"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3949bd5b99cafdf1c7ca86b43ca564028dfe27d66958f2470940f73d86d75b37"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.1"
+3 -1
View File
@@ -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",
]
@@ -27,7 +29,7 @@ bumpalo = { version = "3.20", features = [
ghost-cell = "0.2"
hashbrown = "0.16"
num_enum = "0.7.5"
smallvec = "1.15"
smallvec = { version = "1.15", features = ["const_new", "const_generics"] }
ere = "0.2"
string-interner = "0.19"
rnix = "0.14"
-70
View File
@@ -1,70 +0,0 @@
{
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
"vcs": {
"enabled": true,
"clientKind": "git",
"useIgnoreFile": true
},
"files": {
"includes": ["**", "!!**/dist"]
},
"formatter": {
"enabled": true,
"formatWithErrors": true,
"attributePosition": "auto",
"indentStyle": "space",
"indentWidth": 2,
"lineWidth": 110,
"lineEnding": "lf"
},
"linter": {
"rules": {
"style": {
"useNamingConvention": {
"level": "warn",
"options": {
"strictCase": false,
"conventions": [
{
"selector": { "kind": "objectLiteralProperty" },
"formats": ["camelCase", "PascalCase", "CONSTANT_CASE"]
},
{
"selector": { "kind": "typeProperty" },
"formats": ["camelCase", "snake_case"]
}
]
}
}
}
}
},
"overrides": [
{
"includes": ["**/global.d.ts"],
"linter": {
"rules": {
"style": {
"useNamingConvention": "off"
}
}
}
}
],
"javascript": {
"formatter": {
"arrowParentheses": "always",
"bracketSameLine": false,
"bracketSpacing": true,
"jsxQuoteStyle": "double",
"quoteProperties": "asNeeded",
"semicolons": "always",
"trailingCommas": "all"
}
},
"json": {
"formatter": {
"trailingCommas": "none"
}
}
}
+18
View File
@@ -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" }
@@ -24,7 +24,7 @@ impl<T: Default + Copy, const N: usize> ArrayExt<N> 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
}
@@ -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<C: VmRuntimeCtx>(&mut self, ctx: &C) -> OperandData {
pub fn read_operand_data<C: VmRuntimeCtx>(&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
}
}
@@ -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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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: Machine<'gc>>(
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: Machine<'gc>>(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)
}
}
+164
View File
@@ -0,0 +1,164 @@
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, StringContext, 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<str>) -> 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<ExtraScope>,
ctx: &mut impl VmRuntimeCtx,
) -> fix_error::Result<InstructionPtr>;
}
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<StringId, NixType>;
/// Returns the string context attached to `val`, or `&[]` if `val` is
/// either a non-string or a string without context.
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext;
fn convert_value(&self, val: Value) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().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::<Path>() {
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<StringId, NixType> {
if let Some(sid) = val.as_inline::<StringId>() {
Ok(sid)
} else if let Some(s) = val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str()) {
Ok(self.intern_string(s))
} else {
Err(val.ty())
}
}
fn get_string_context<'gc>(&self, val: StrictValue<'gc>) -> &'gc StringContext {
if let Some(ns) = val.as_gc::<NixString>() {
ns.as_ref().context()
} else {
StringContext::empty()
}
}
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<u64>) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value {
use fix_common::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(p) = val.as_inline::<Path>() {
Value::Path(self.resolve_string(p.0).to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
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::<List>() {
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::<Closure>() {
Value::Func
} else if let Some(thunk) = val.as_gc::<Thunk>() {
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::<PrimOp>() {
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::<PrimOpApp>() {
let name = fix_builtins::BUILTINS[app.primop.id as usize].0;
Value::PrimOpApp(name.strip_prefix("__").unwrap_or(name))
} else {
Value::Null
}
}
}
+20
View File
@@ -0,0 +1,20 @@
mod boxing;
mod bytecode_reader;
mod forced;
mod host;
mod machine;
mod path_util;
mod resolve;
mod state;
mod string_context;
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 string_context::*;
pub use value::*;
+174
View File
@@ -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<CallFrame<'gc>>;
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<Error>) -> 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<Value<'gc>>;
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<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> ControlFlow<Break, T>
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<T: Forced<'gc>>(
&mut self,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
resume_pc: usize,
) -> ControlFlow<Break, T>
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 {}
+18
View File
@@ -0,0 +1,18 @@
use std::path::{Component, PathBuf};
pub fn canon_path_str(path: impl AsRef<std::path::Path>) -> 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()
}
+33
View File
@@ -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::<AttrSet>().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::<AttrSet>().expect("scope must be attrset");
#[allow(clippy::unwrap_used)]
attrs.lookup(name).expect("scoped binding not found")
}
}
}
+99
View File
@@ -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<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
pub fn into_error(self) -> Box<Error> {
match self {
VmError::Catchable(_) => todo!("Check for tryEval catch frames"),
VmError::Uncatchable(e) => e,
}
}
}
pub fn vm_err(msg: impl Into<String>) -> 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<Break>;
#[allow(dead_code)]
pub struct ErrorFrame {
pub span_id: u32,
pub message: Option<String>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub struct CallFrame<'gc> {
pub pc: usize,
pub thunk: Option<Gc<'gc, Thunk<'gc>>>,
pub env: GcEnv<'gc>,
}
#[derive(Debug)]
pub struct PendingLoad {
pub path: PathBuf,
pub scope: Option<PendingScope>,
}
#[derive(Debug)]
pub struct PendingScope {
pub keys: HashSet<StringId>,
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<StringId>,
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 },
}
+161
View File
@@ -0,0 +1,161 @@
use std::cmp::Ordering;
use smallvec::SmallVec;
/// A string context element
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum StringContextElem {
// Plain store path reference
Opaque {
path: Box<str>,
},
// All outputs of a derivation
// encoded `=<drvPath>`
DrvDeep {
drv_path: Box<str>,
},
// A specific output of a derivation
// encoded `!<output>!<drvPath>`
Built {
drv_path: Box<str>,
output: Box<str>,
},
}
impl StringContextElem {
/// Decode the CppNix wire form (`!out!/p`, `=/p`, `/p`). Falls back to
/// `Opaque` for malformed `!`-prefixed inputs (matching nix-js).
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
Self::DrvDeep {
drv_path: drv_path.into(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') {
Self::Built {
output: rest[..second_bang].into(),
drv_path: rest[second_bang + 1..].into(),
}
} else {
Self::Opaque {
path: encoded.into(),
}
}
} else {
Self::Opaque {
path: encoded.into(),
}
}
}
pub fn encode(&self) -> String {
match self {
Self::Opaque { path } => path.to_string(),
Self::DrvDeep { drv_path } => format!("={drv_path}"),
Self::Built { drv_path, output } => format!("!{output}!{drv_path}"),
}
}
}
#[derive(Debug, Clone, Default)]
pub struct StringContext {
data: SmallVec<[StringContextElem; 1]>,
}
impl IntoIterator for StringContext {
type Item = StringContextElem;
type IntoIter = <SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.into_iter()
}
}
impl<'a> IntoIterator for &'a StringContext {
type Item = &'a StringContextElem;
type IntoIter = <&'a SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.iter()
}
}
impl<'a> IntoIterator for &'a mut StringContext {
type Item = &'a mut StringContextElem;
type IntoIter = <&'a mut SmallVec<[StringContextElem; 1]> as IntoIterator>::IntoIter;
fn into_iter(self) -> Self::IntoIter {
self.data.iter_mut()
}
}
impl FromIterator<StringContextElem> for StringContext {
fn from_iter<T: IntoIterator<Item = StringContextElem>>(iter: T) -> Self {
Self {
data: iter.into_iter().collect()
}
}
}
impl StringContext {
pub fn empty() -> &'static Self {
static EMPTY: StringContext = StringContext {
data: SmallVec::new_const(),
};
&EMPTY
}
pub fn new() -> Self {
Self::default()
}
pub fn is_empty(&self) -> bool {
self.data.is_empty()
}
pub fn insert(&mut self, elem: StringContextElem) {
match self.data.binary_search(&elem) {
Ok(_) => {}
Err(pos) => self.data.insert(pos, elem),
}
}
pub fn merge(&self, other: &Self) -> Self {
if self.data.is_empty() {
return other.clone();
}
if other.data.is_empty() {
return self.clone();
}
let a = &self.data;
let b = &other.data;
let mut out = SmallVec::with_capacity(a.len() + b.len());
let (mut i, mut j) = (0, 0);
while i < a.len() && j < b.len() {
match a[i].cmp(&b[j]) {
Ordering::Less => {
out.push(a[i].clone());
i += 1;
}
Ordering::Greater => {
out.push(b[j].clone());
j += 1;
}
Ordering::Equal => {
out.push(a[i].clone());
i += 1;
j += 1;
}
}
}
out.extend(a[i..].iter().cloned());
out.extend(b[j..].iter().cloned());
Self { data: out }
}
pub fn iter(&self) -> <&Self as IntoIterator>::IntoIter {
self.into_iter()
}
pub fn iter_mut(&mut self) -> <&mut Self as IntoIterator>::IntoIter {
self.into_iter()
}
}
@@ -16,8 +16,8 @@ use smallvec::SmallVec;
use string_interner::Symbol;
use string_interner::symbol::SymbolU32;
use crate::NixNum;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::string_context::StringContext;
mod private {
pub trait Cealed {}
@@ -117,6 +117,7 @@ define_value_types! {
Null => RawTag::P3, "Null";
StringId => RawTag::P4, "SmallString";
PrimOp => RawTag::P5, "PrimOp";
Path => RawTag::N6, "Path";
}
gc {
i64 => RawTag::P6, "BigInt";
@@ -249,7 +250,7 @@ impl<'gc> Value<'gc> {
}
#[inline]
pub(crate) fn to_bits(self) -> u64 {
pub fn to_bits(self) -> u64 {
self.raw.to_bits()
}
@@ -289,6 +290,8 @@ impl<'gc> Value<'gc> {
NixType::PrimOp
} else if self.is::<NixString>() {
NixType::String
} else if self.is::<Path>() {
NixType::Path
} else if self.is::<AttrSet>() {
NixType::AttrSet
} else if self.is::<List>() {
@@ -305,27 +308,27 @@ impl<'gc> Value<'gc> {
}
#[inline]
pub(crate) fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> {
pub fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> {
self.as_inline::<T>().ok_or_else(|| self.ty())
}
#[inline]
pub(crate) fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> {
pub fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> {
self.as_gc::<T>().ok_or_else(|| self.ty())
}
#[inline]
pub(crate) fn expect_num(self) -> Result<NixNum, NixType> {
pub fn expect_num(self) -> Result<NixNum, NixType> {
self.as_num().ok_or_else(|| self.ty())
}
#[inline]
pub(crate) fn expect_bool(self) -> Result<bool, NixType> {
pub fn expect_bool(self) -> Result<bool, NixType> {
self.as_inline::<bool>().ok_or_else(|| self.ty())
}
#[inline]
pub(crate) fn expect_float(self) -> Result<f64, NixType> {
pub fn expect_float(self) -> Result<f64, NixType> {
self.as_float().ok_or_else(|| self.ty())
}
}
@@ -382,7 +385,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]);
@@ -404,26 +407,55 @@ impl RawStore for StringId {
}
}
/// Heap-allocated Nix string.
///
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
/// lives in a standard `Box<str>` owned by this struct; the GC only manages
/// the outer allocation.
/// 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 struct Path(pub StringId);
impl RawStore for Path {
fn to_val(self, value: &mut RawValue) {
self.0.to_val(value);
}
fn from_val(value: &RawValue) -> Self {
Self(StringId::from_val(value))
}
}
#[derive(Collect)]
#[collect(require_static)]
pub(crate) struct NixString {
pub struct NixString {
data: Box<str>,
// TODO: string context for derivation dependency tracking
context: StringContext,
}
impl NixString {
pub(crate) fn new(s: impl Into<Box<str>>) -> Self {
Self { data: s.into() }
pub fn new(s: impl Into<Box<str>>) -> Self {
Self {
data: s.into(),
context: StringContext::new(),
}
}
pub(crate) fn as_str(&self) -> &str {
/// Construct a `NixString` whose `context` is already sorted+deduped.
/// The caller is responsible for invariant maintenance.
pub fn with_context(s: impl Into<Box<str>>, context: StringContext) -> Self {
Self {
data: s.into(),
context,
}
}
pub fn as_str(&self) -> &str {
&self.data
}
pub fn context(&self) -> &StringContext {
&self.context
}
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
}
impl fmt::Debug for NixString {
@@ -434,28 +466,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<Value<'gc>> {
pub fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
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));
@@ -493,12 +525,12 @@ impl<'gc> AttrSet<'gc> {
#[derive(Collect, Debug, Default)]
#[repr(transparent)]
#[collect(no_drop)]
pub(crate) struct List<'gc> {
pub(crate) inner: RefLock<SmallVec<[Value<'gc>; 4]>>,
pub struct List<'gc> {
pub inner: RefLock<SmallVec<[Value<'gc>; 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 {
@@ -507,7 +539,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())
}
}
@@ -519,11 +551,11 @@ impl<'gc> Unlock for List<'gc> {
}
}
pub(crate) type Thunk<'gc> = RefLock<ThunkState<'gc>>;
pub type Thunk<'gc> = RefLock<ThunkState<'gc>>;
#[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,
@@ -532,33 +564,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<GcEnv<'gc>>,
pub struct Env<'gc> {
pub locals: SmallVec<[Value<'gc>; 4]>,
pub prev: Option<GcEnv<'gc>>,
}
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<GcWithEnv<'gc>>,
pub struct WithEnv<'gc> {
pub env: Value<'gc>,
pub prev: Option<GcWithEnv<'gc>>,
}
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<Env<'gc>>>,
) -> Self {
pub fn with_arg(arg: Value<'gc>, n_locals: u32, prev: Gc<'gc, RefLock<Env<'gc>>>) -> Self {
let mut locals = smallvec::smallvec![Value::default(); 1 + n_locals as usize];
locals[0] = arg;
Env {
@@ -570,29 +598,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<Env<'gc>>>,
pub(crate) pattern: Option<Gc<'gc, PatternInfo>>,
pub struct Closure<'gc> {
pub ip: u32,
pub n_locals: u32,
pub env: Gc<'gc, RefLock<Env<'gc>>>,
pub pattern: Option<Gc<'gc, PatternInfo>>,
}
#[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 {
@@ -619,20 +647,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
}
}
@@ -653,12 +681,13 @@ 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,
Null,
String,
Path,
AttrSet,
List,
Thunk,
@@ -676,6 +705,7 @@ impl NixType {
Bool => "a boolean",
Null => "null",
String => "a string",
Path => "a path",
AttrSet => "a set",
List => "a list",
Thunk => "a thunk",
@@ -691,3 +721,8 @@ impl std::fmt::Display for NixType {
write!(f, "{}", self.display())
}
}
pub enum NixNum {
Int(i64),
Float(f64),
}
+29 -1
View File
@@ -118,13 +118,14 @@ define_builtins! {
("__tryEval", TryEval, 1),
("__typeOf", TypeOf, 1),
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("__warn", Warn, 2),
("__zipAttrsWith", ZipAttrsWith, 2),
}
#[repr(u8)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq, TryFromPrimitive)]
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub enum PrimOpPhase {
Abort,
Add,
@@ -171,6 +172,10 @@ pub enum PrimOpPhase {
FindFile,
Floor,
FoldlStrict,
FoldlStrictEmpty,
FoldlStrictCall1,
FoldlStrictCall2,
FoldlStrictUpdate,
FromJSON,
FromTOML,
FunctionArgs,
@@ -242,6 +247,9 @@ pub enum PrimOpPhase {
ForceResultShallowLoop,
ForceResultDeepFinish,
EqStep,
EqForce,
// TODO: split into separate enums
CallPattern,
CallFunctor1,
@@ -250,9 +258,28 @@ pub enum PrimOpPhase {
ImportFinalize,
ScopedImportFinalize,
AppendContextLoop,
AppendContextEntryForced,
AppendContextOutputsForced,
AppendContextOutputElementLoop,
AppendContextOutputElementForced,
UnsafeDiscardOutputDependency,
Illegal,
}
impl TryFrom<u8> for PrimOpPhase {
type Error = u8;
fn try_from(value: u8) -> Result<Self, Self::Error> {
if (0..Self::Illegal as u8).contains(&value) {
Ok(unsafe { std::mem::transmute::<u8, Self>(value) })
} else {
Err(value)
}
}
}
impl BuiltinId {
#[inline(always)]
pub fn entry_phase(self) -> PrimOpPhase {
@@ -357,6 +384,7 @@ impl BuiltinId {
TryEval => PrimOpPhase::TryEval,
TypeOf => PrimOpPhase::TypeOf,
UnsafeDiscardStringContext => PrimOpPhase::UnsafeDiscardStringContext,
UnsafeDiscardOutputDependency => PrimOpPhase::UnsafeDiscardOutputDependency,
UnsafeGetAttrPos => PrimOpPhase::UnsafeGetAttrPos,
Warn => PrimOpPhase::Warn,
ZipAttrsWith => PrimOpPhase::ZipAttrsWith,
+1 -1
View File
@@ -30,7 +30,7 @@ impl<'id, 'ir> Ir<'ir, GhostRoRef<'id, 'ir>> {
// - `Ir<'ir, R>` is `#[repr(C)]`, and for every field that depends on
// `R`, instantiating `R = GhostRef<'id, 'ir>` vs `R = RawRef<'ir>`
// produces types of identical layout:
// - `R::IrRef` becomes `&'ir GhostCell<'id, Ir<>>` vs `&'ir Ir<>`
// - `R::IrRef` becomes `&'ir GhostCell<'id, Ir<...>>` vs `&'ir Ir<...>`
// - `R::MaybeThunkRef` becomes `&'ir GhostCell<'id, MaybeThunk>`
// vs `&'ir MaybeThunk`
// - `R::Ref<Ir<'ir, R>>` (used in `ConcatStrings::parts`) reduces
+17
View File
@@ -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" }
+447
View File
@@ -0,0 +1,447 @@
//! `builtins.hasContext`, `builtins.getContext`, `builtins.appendContext`,
//! `builtins.unsafeDiscardStringContext`,
//! `builtins.unsafeDiscardOutputDependency`.
//!
//! See `fix-abstract-vm/src/string_context.rs` for the
//! `StringContextElem` type.
use fix_abstract_vm::{
AttrSet, BytecodeReader, List as VmList, Machine, MachineExt, NixString, NixType, Step,
StrictValue, StringContext, StringContextElem, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use fix_builtins::PrimOpPhase;
use fix_common::StringId;
use fix_error::Error;
use gc_arena::{Gc, Mutation};
use smallvec::SmallVec;
pub fn has_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.as_gc::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty());
}
let has_ctx = !ctx.get_string_context(val).is_empty();
m.return_from_primop(Value::new_inline(has_ctx), reader)
}
pub fn unsafe_discard_string_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.as_inline::<StringId>() {
return m.return_from_primop(Value::new_inline(sid), reader);
}
let Some(ns) = val.as_gc::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty());
};
let sid = ctx.intern_string(ns.as_str());
m.return_from_primop(Value::new_inline(sid), reader)
}
pub fn unsafe_discard_output_dependency<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if let Some(sid) = val.as_inline::<StringId>() {
return m.return_from_primop(Value::new_inline(sid), reader);
}
let Some(ns) = val.as_gc::<NixString>() else {
return m.finish_type_err(NixType::String, val.ty());
};
if ns.context().is_empty() {
let sid = ctx.intern_string(ns.as_str());
return m.return_from_primop(Value::new_inline(sid), reader);
}
let mut new_ctx = StringContext::new();
for elem in ns.context() {
let replacement = match elem {
StringContextElem::DrvDeep { drv_path } => StringContextElem::Opaque {
path: drv_path.clone(),
},
other => other.clone(),
};
new_ctx.insert(replacement);
}
let s: Box<str> = ns.as_str().into();
let new_ns = Gc::new(mc, NixString::with_context(s, new_ctx));
m.return_from_primop(Value::new_gc(new_ns), reader)
}
pub fn get_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let val = m.force_and_retry::<StrictValue>(reader, mc)?;
if !val.is::<StringId>() && val.as_gc::<NixString>().is_none() {
return m.finish_type_err(NixType::String, val.ty());
}
let elems = ctx.get_string_context(val);
struct Info {
path: bool,
all_outputs: bool,
outputs: SmallVec<[Box<str>; 2]>,
}
impl Info {
fn new() -> Self {
Self {
path: false,
all_outputs: false,
outputs: SmallVec::new(),
}
}
}
let mut by_path: std::collections::BTreeMap<Box<str>, Info> = std::collections::BTreeMap::new();
for elem in elems {
match elem {
StringContextElem::Opaque { path } => {
by_path.entry(path.clone()).or_insert_with(Info::new).path = true;
}
StringContextElem::DrvDeep { drv_path } => {
by_path
.entry(drv_path.clone())
.or_insert_with(Info::new)
.all_outputs = true;
}
StringContextElem::Built { drv_path, output } => {
by_path
.entry(drv_path.clone())
.or_insert_with(Info::new)
.outputs
.push(output.clone());
}
}
}
let mut outer_entries: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
for (path, mut info) in by_path {
info.outputs.sort();
info.outputs.dedup();
let mut sub: SmallVec<[(StringId, Value<'gc>); 4]> = SmallVec::new();
if info.all_outputs {
sub.push((ctx.intern_string("allOutputs"), Value::new_inline(true)));
}
if !info.outputs.is_empty() {
let items: smallvec::SmallVec<[Value<'gc>; 4]> = info
.outputs
.iter()
.map(|o| Value::new_inline(ctx.intern_string(o)))
.collect();
let list = VmList::new(mc, items);
sub.push((ctx.intern_string("outputs"), Value::new_gc(list)));
}
if info.path {
sub.push((ctx.intern_string("path"), Value::new_inline(true)));
}
sub.sort_by_key(|(k, _)| *k);
let sub_attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(sub));
outer_entries.push((ctx.intern_string(&path), Value::new_gc(sub_attrs)));
}
outer_entries.sort_by_key(|(k, _)| *k);
let outer = Gc::new(mc, AttrSet::from_sorted_unchecked(outer_entries));
m.return_from_primop(Value::new_gc(outer), reader)
}
/// appendContext :: String -> AttrSet -> String
/// The context AttrSet maps store-path strings to `{ path?: Bool, allOutputs?:
/// Bool, outputs?: [String] }`. Each present field contributes one
/// StringContextElem to the result.
///
/// Requires forcing nested attrset values and list elements lazily, so it's
/// structured as a state machine with the following stack layout:
///
/// [strVal, attrs, idx, acc] - outer loop
/// [strVal, attrs, idx, acc, entryAttrs] - after entry forced
/// [strVal, attrs, idx, acc, list] - after `outputs` forced
/// [strVal, attrs, idx, acc, list, oidx] - output-element loop
/// [strVal, attrs, idx, acc, list, oidx, outElem] - after element forced
///
/// `acc` is a sentinel `NixString` whose `data` is empty and whose `context`
/// is the accumulator. The string value itself is preserved in `strVal` and
/// retrieved at finalization.
///
// TODO: handle thunk-valued `path` and `allOutputs` sub-attrs; currently they
// must be already-evaluated booleans.
pub fn append_context<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (str_val, attrs) = m.force_and_retry::<(StrictValue, Gc<AttrSet>)>(reader, mc)?;
let initial_ctx: StringContext = ctx.get_string_context(str_val).clone();
let acc = Gc::new(mc, NixString::with_context("", initial_ctx));
m.push(str_val.relax());
m.push(Value::new_gc(attrs));
m.push(Value::new_inline(0i32));
m.push(Value::new_gc(acc));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_loop<'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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let attrs = m.peek_forced(2).as_gc::<AttrSet>().unwrap();
if idx as usize >= attrs.entries.len() {
return append_context_finalize(m, ctx, reader, mc);
}
let entry_val = attrs.entries[idx as usize].1;
m.push(entry_val);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::AppendContextEntryForced.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::AppendContextEntryForced.ip() as usize);
Step::Continue(())
}
pub fn append_context_entry_forced<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack: [strVal, attrs, idx, acc, entryAttrs(thunk)]
// The slot still holds the Thunk pointer; re-force to extract the now-
// Evaluated value into the slot.
m.force_slot(0, reader, mc)?;
let entry_val = m.peek_forced(0);
let Some(entry_attrs) = entry_val.as_gc::<AttrSet>() else {
return m.finish_type_err(NixType::AttrSet, entry_val.ty());
};
#[allow(clippy::unwrap_used)]
let idx = m.peek(2).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let outer = m.peek_forced(3).as_gc::<AttrSet>().unwrap();
let path_key = outer.entries[idx as usize].0;
let path_str_owned: Box<str> = ctx.resolve_string(path_key).into();
if !path_str_owned.starts_with("/nix/store/") {
return m.finish_err(Error::eval_error(format!(
"context key '{path_str_owned}' is not a store path"
)));
}
// Eagerly handle `path` and `allOutputs` (assumed already-forced
// booleans - most callers either set them to literal `true` or omit
// them entirely).
// TODO: force these two attributes correctly
let path_id = ctx.intern_string("path");
let all_outputs_id = ctx.intern_string("allOutputs");
let outputs_id = ctx.intern_string("outputs");
#[allow(clippy::unwrap_used)]
let acc_gc = m.peek(1).as_gc::<NixString>().unwrap();
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
if let Some(v) = entry_attrs.lookup(path_id)
&& v.as_inline::<bool>() == Some(true)
{
new_acc.insert(StringContextElem::Opaque {
path: path_str_owned.clone(),
});
}
if let Some(v) = entry_attrs.lookup(all_outputs_id)
&& v.as_inline::<bool>() == Some(true)
{
if !path_str_owned.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!(
"tried to add all-outputs context of {path_str_owned}, which is not a derivation, to a string"
)));
}
new_acc.insert(StringContextElem::DrvDeep {
drv_path: path_str_owned.clone(),
});
}
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(1, Value::new_gc(new_acc_gc));
if let Some(outputs_val) = entry_attrs.lookup(outputs_id) {
m.replace(0, outputs_val);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::AppendContextOutputsForced.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::AppendContextOutputsForced.ip() as usize);
return Step::Continue(());
}
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let idx_back = m.peek(1).as_inline::<i32>().unwrap();
m.replace(1, Value::new_inline(idx_back + 1));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_outputs_forced<'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 list_val = m.peek_forced(0);
let Some(list) = list_val.as_gc::<VmList>() else {
return m.finish_type_err(NixType::List, list_val.ty());
};
if list.inner.borrow().is_empty() {
// Stack: [strVal, attrs, idx, acc, list] -> drop list, bump idx.
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let idx_back = m.peek(1).as_inline::<i32>().unwrap();
m.replace(1, Value::new_inline(idx_back + 1));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
return Step::Continue(());
}
m.push(Value::new_inline(0i32));
reader.set_pc(PrimOpPhase::AppendContextOutputElementLoop.ip() as usize);
Step::Continue(())
}
pub fn append_context_output_element_loop<'gc, M: Machine<'gc>>(
m: &mut M,
_ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
#[allow(clippy::unwrap_used)]
let oidx = m.peek(0).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(1).as_gc::<VmList>().unwrap();
let len = list.inner.borrow().len();
if oidx as usize >= len {
// Stack: [strVal, attrs, idx, acc, list, oidx] -> drop oidx & list,
// bump idx in place.
let _ = m.pop();
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let idx_back = m.peek(1).as_inline::<i32>().unwrap();
m.replace(1, Value::new_inline(idx_back + 1));
reader.set_pc(PrimOpPhase::AppendContextLoop.ip() as usize);
return Step::Continue(());
}
let elem = list.inner.borrow()[oidx as usize];
m.push(elem);
m.force_slot_to_pc(
0,
reader,
mc,
PrimOpPhase::AppendContextOutputElementForced.ip() as usize,
)?;
reader.set_pc(PrimOpPhase::AppendContextOutputElementForced.ip() as usize);
Step::Continue(())
}
pub fn append_context_output_element_forced<'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 elem = m.peek_forced(0);
let Some(output_name) = ctx.get_string(elem) else {
return m.finish_type_err(NixType::String, elem.ty());
};
let output_name: Box<str> = output_name.into();
#[allow(clippy::unwrap_used)]
let idx = m.peek(4).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let outer = m.peek_forced(5).as_gc::<AttrSet>().unwrap();
let path_key = outer.entries[idx as usize].0;
let path_str: Box<str> = ctx.resolve_string(path_key).into();
if !path_str.ends_with(".drv") {
return m.finish_err(Error::eval_error(format!(
"tried to add derivation output context of {path_str}, which is not a derivation, to a string"
)));
}
#[allow(clippy::unwrap_used)]
let acc_gc = m.peek(3).as_gc::<NixString>().unwrap();
let mut new_acc: StringContext = acc_gc.context().iter().cloned().collect();
new_acc.insert(StringContextElem::Built {
drv_path: path_str,
output: output_name,
});
let new_acc_gc = Gc::new(mc, NixString::with_context("", new_acc));
m.replace(3, Value::new_gc(new_acc_gc));
// Stack: [strVal, attrs, idx, acc, list, oidx, outElem] -> drop outElem,
// bump oidx in place.
let _ = m.pop();
#[allow(clippy::unwrap_used)]
let oidx = m.peek(0).as_inline::<i32>().unwrap();
m.replace(0, Value::new_inline(oidx + 1));
reader.set_pc(PrimOpPhase::AppendContextOutputElementLoop.ip() as usize);
Step::Continue(())
}
fn append_context_finalize<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
// Stack: [strVal, attrs, idx, acc]
#[allow(clippy::unwrap_used)]
let acc_gc = m.pop().as_gc::<NixString>().unwrap();
let _ = m.pop(); // idx
let _ = m.pop(); // attrs
let str_val_raw = m.pop();
// The strVal was already forced at entry; restrict() is infallible here.
let str_val = str_val_raw
.restrict()
.unwrap_or_else(|_| panic!("appendContext: strVal unexpectedly a thunk"));
let s_str = ctx.get_string(str_val).unwrap_or("").to_owned();
let context: StringContext = acc_gc.context().iter().cloned().collect();
let result = if context.is_empty() {
let sid = ctx.intern_string(s_str);
Value::new_inline(sid)
} else {
let ns = Gc::new(mc, NixString::with_context(s_str, context));
Value::new_gc(ns)
};
m.return_from_primop(result, reader)
}
+362
View File
@@ -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("<non-string-value>");
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::<AttrSet>() {
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::<List<'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::<i32>().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::<List<'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::<i32>().unwrap();
let mut added: usize = 0;
if let Some(attrs) = item.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'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::<List<'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::<List<'gc>>() {
#[allow(clippy::unwrap_used)]
let seen = m.peek_forced(2).as_gc::<List<'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::<List<'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::<AttrSet>() {
let len = attrs.entries.len();
(len, len > 0)
} else if let Some(list) = val.as_gc::<List<'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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let len = m.peek(0).as_inline::<i32>().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::<AttrSet>() {
attrs.entries.get(idx as usize).map(|&(_, v)| v)
} else if let Some(list) = val.as_gc::<List<'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::<StrictValue>(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::<StrictValue>(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<Closure>, Gc<AttrSet>)>(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::<AttrSet>() || val.is::<List<'_>>()
}
+51
View File
@@ -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::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
return m.return_from_primop(val.relax(), reader);
}
if let Some(p) = val.as_inline::<Path>() {
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::<StrictValue>(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)
}
+238
View File
@@ -0,0 +1,238 @@
use fix_abstract_vm::{
AttrSet, BytecodeReader, CallFrame, List, Machine, MachineExt, NixNum, Null, Path, Step,
StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
};
use fix_builtins::PrimOpPhase;
use gc_arena::{Gc, Mutation};
use smallvec::SmallVec;
pub fn start_eq<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
negate: bool,
) -> Step {
match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {
m.push(Value::new_inline(!negate));
Step::Continue(())
}
ShallowEq::False => {
m.push(Value::new_inline(negate));
Step::Continue(())
}
ShallowEq::RecurseList(la, lb) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> = la.inner.borrow().iter().copied().collect();
let rhs_init: SmallVec<[Value<'gc>; 4]> = lb.inner.borrow().iter().copied().collect();
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
}
ShallowEq::RecurseAttrs(a, b) => {
let lhs_init: SmallVec<[Value<'gc>; 4]> =
a.entries.iter().map(|&(_, v)| v).collect();
let rhs_init: SmallVec<[Value<'gc>; 4]> =
b.entries.iter().map(|&(_, v)| v).collect();
enter_eq_machine(m, reader, mc, negate, lhs_init, rhs_init)
}
}
}
pub fn eq_step<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let rhs_q = m
.peek(0)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: rhs_queue");
let lhs_q = m
.peek(1)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: lhs_queue");
let result = m
.peek(2)
.as_inline::<bool>()
.expect("eq state corrupted: result");
if !result || lhs_q.inner.borrow().is_empty() {
return finalize(m, reader);
}
let lhs = lhs_q
.unlock(mc)
.borrow_mut()
.pop()
.expect("non-empty lhs queue");
let rhs = rhs_q
.unlock(mc)
.borrow_mut()
.pop()
.expect("non-empty rhs queue");
m.push(lhs);
m.push(rhs);
reader.set_pc(PrimOpPhase::EqForce.ip() as usize);
Step::Continue(())
}
pub fn eq_force<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = m.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
apply_pair(m, ctx, mc, lhs, rhs);
reader.set_pc(PrimOpPhase::EqStep.ip() as usize);
Step::Continue(())
}
fn finalize<'gc, M: Machine<'gc>>(m: &mut M, reader: &mut BytecodeReader<'_>) -> Step {
let _ = m.pop();
let _ = m.pop();
let result = m
.pop()
.as_inline::<bool>()
.expect("eq state corrupted: result");
let negate = m
.pop()
.as_inline::<bool>()
.expect("eq state corrupted: negate");
m.return_from_primop(Value::new_inline(result ^ negate), reader)
}
fn apply_pair<'gc, M: Machine<'gc>>(
m: &mut M,
ctx: &impl VmRuntimeCtx,
mc: &Mutation<'gc>,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) {
match shallow_eq(ctx, lhs, rhs) {
ShallowEq::True => {}
ShallowEq::False => {
m.replace(2, Value::new_inline(false));
}
ShallowEq::RecurseList(la, lb) => {
extend_queues(m, mc, la.inner.borrow().iter().copied(), lb.inner.borrow().iter().copied());
}
ShallowEq::RecurseAttrs(a, b) => {
extend_queues(
m,
mc,
a.entries.iter().map(|&(_, v)| v),
b.entries.iter().map(|&(_, v)| v),
);
}
}
}
fn extend_queues<'gc, M, L, R>(m: &mut M, mc: &Mutation<'gc>, lhs_iter: L, rhs_iter: R)
where
M: Machine<'gc>,
L: IntoIterator<Item = Value<'gc>>,
R: IntoIterator<Item = Value<'gc>>,
{
let rhs_q = m
.peek(0)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: rhs_queue");
let lhs_q = m
.peek(1)
.as_gc::<List<'gc>>()
.expect("eq state corrupted: lhs_queue");
let mut lq = lhs_q.unlock(mc).borrow_mut();
let mut rq = rhs_q.unlock(mc).borrow_mut();
for (x, y) in lhs_iter.into_iter().zip(rhs_iter) {
lq.push(x);
rq.push(y);
}
}
fn enter_eq_machine<'gc, M: Machine<'gc>>(
m: &mut M,
reader: &mut BytecodeReader<'_>,
mc: &Mutation<'gc>,
negate: bool,
lhs_init: SmallVec<[Value<'gc>; 4]>,
rhs_init: SmallVec<[Value<'gc>; 4]>,
) -> Step {
let resume_pc = reader.pc();
m.push_call_frame(CallFrame {
pc: resume_pc,
thunk: None,
env: m.env(),
});
m.inc_call_depth();
m.push(Value::new_inline(negate));
m.push(Value::new_inline(true));
m.push(Value::new_gc(List::new(mc, lhs_init)));
m.push(Value::new_gc(List::new(mc, rhs_init)));
reader.set_pc(PrimOpPhase::EqStep.ip() as usize);
Step::Continue(())
}
enum ShallowEq<'gc> {
True,
False,
RecurseList(Gc<'gc, List<'gc>>, Gc<'gc, List<'gc>>),
RecurseAttrs(Gc<'gc, AttrSet<'gc>>, Gc<'gc, AttrSet<'gc>>),
}
fn shallow_eq<'gc>(
ctx: &impl VmRuntimeCtx,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> ShallowEq<'gc> {
if let (Some(a), Some(b)) = (lhs.as_num(), rhs.as_num()) {
let eq = match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b,
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
};
return bool_outcome(eq);
}
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) {
return bool_outcome(a == b);
}
if lhs.is::<Null>() && rhs.is::<Null>() {
return ShallowEq::True;
}
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
return bool_outcome(a.0 == b.0);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return bool_outcome(a == b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<List<'gc>>(), rhs.as_gc::<List<'gc>>()) {
if a.inner.borrow().len() != b.inner.borrow().len() {
return ShallowEq::False;
}
return ShallowEq::RecurseList(a, b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<AttrSet<'gc>>(), rhs.as_gc::<AttrSet<'gc>>()) {
let ae = &a.entries;
let be = &b.entries;
if ae.len() != be.len() {
return ShallowEq::False;
}
for (l, r) in ae.iter().zip(be.iter()) {
if l.0 != r.0 {
return ShallowEq::False;
}
}
return ShallowEq::RecurseAttrs(a, b);
}
ShallowEq::False
}
fn bool_outcome<'gc>(b: bool) -> ShallowEq<'gc> {
if b {
ShallowEq::True
} else {
ShallowEq::False
}
}
+190
View File
@@ -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::<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),
};
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::<StringId>().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<AttrSet>, 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<StringId> = 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::<StrictValue>(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::<Path>() {
(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<PathBuf, Box<Error>> {
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)
}
+87
View File
@@ -0,0 +1,87 @@
mod context;
mod control;
mod conv;
mod eq;
mod io;
mod list;
mod path;
pub use context::*;
pub use control::*;
pub use conv::*;
pub use eq::*;
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),
EqStep => eq_step(m, reader, mc),
EqForce => eq_force(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),
HasContext => has_context(m, ctx, reader, mc),
GetContext => get_context(m, ctx, reader, mc),
AppendContext => append_context(m, ctx, reader, mc),
AppendContextLoop => append_context_loop(m, ctx, reader, mc),
AppendContextEntryForced => append_context_entry_forced(m, ctx, reader, mc),
AppendContextOutputsForced => append_context_outputs_forced(m, ctx, reader, mc),
AppendContextOutputElementLoop => append_context_output_element_loop(m, ctx, reader, mc),
AppendContextOutputElementForced => append_context_output_element_forced(m, ctx, reader, mc),
UnsafeDiscardStringContext => unsafe_discard_string_context(m, ctx, reader, mc),
UnsafeDiscardOutputDependency => unsafe_discard_output_dependency(m, ctx, reader, mc),
phase => todo!("primop phase {phase:?}"),
}
}
+166
View File
@@ -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::<List>() {
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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = m.peek_forced(2).as_gc::<List>().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::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = m.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().unwrap();
let list = list.inner.borrow();
#[allow(clippy::unwrap_used)]
let acc = m.peek_forced(0).as_gc::<List>().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::<List>() 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::<StrictValue>(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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(3).as_gc::<List>().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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = m.peek_forced(2).as_gc::<List>().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(())
}
+43
View File
@@ -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::<StrictValue>(reader, mc)?;
if let Some(Path(s)) = val.as_inline::<Path>() {
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::<StrictValue>(reader, mc)?;
let is_path = val.is::<Path>();
m.return_from_primop(Value::new_inline(is_path), reader)
}
+3
View File
@@ -14,8 +14,11 @@ smallvec = { workspace = true }
string-interner = { workspace = true }
likely_stable = { workspace = true }
sptr = "0.3"
sysinfo = { version = "0.38", default-features = false, features = ["system"] }
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" }
+4 -2
View File
@@ -7,6 +7,7 @@ use crate::{Break, BytecodeReader, Step, Vm, VmRuntimeCtx};
pub(crate) enum TailResult {
YieldFuel(u32),
Done,
LoadFile,
}
pub(crate) type OpFn<'gc, C> = extern "rust-preserve-none" fn(
@@ -37,6 +38,7 @@ macro_rules! tail_dispatch_after {
($result:expr, $new_pc:expr, $vm:ident, $mc:ident, $ctx:ident, $bc:ident, $table:ident, $fuel:ident) => {{
match $result {
Step::Continue(()) | Step::Break(Break::Force) => {}
Step::Break(Break::LoadFile) => return TailResult::LoadFile,
Step::Break(Break::Done) => return TailResult::Done,
}
let new_pc: u32 = $new_pc;
@@ -183,7 +185,7 @@ tail_fn!(op_jump, (reader));
tail_fn!(op_coerce_to_string, (reader, mc));
tail_fn!(op_concat_strings, (ctx, reader, mc));
tail_fn!(op_resolve_path, (ctx));
tail_fn!(op_resolve_path, (ctx, reader, mc));
tail_fn!(op_assert, (ctx, reader, mc));
@@ -193,7 +195,7 @@ tail_fn!(op_load_builtins, ());
tail_fn!(op_load_builtin, (reader));
tail_fn!(op_load_repl_binding, (reader));
tail_fn!(op_load_scoped_binding, (reader));
tail_fn!(op_load_scoped_binding, (ctx, reader, mc));
macro_rules! table {
($($variant:ident => $fn:ident),* $(,)?) => {
-7
View File
@@ -1,7 +0,0 @@
use fix_error::Error;
use crate::VmError;
pub(crate) fn vm_err(msg: impl Into<String>) -> VmError {
VmError::Uncatchable(Error::eval_error(msg.into()))
}
+36 -73
View File
@@ -1,9 +1,9 @@
use std::cmp::Ordering;
use fix_abstract_vm::*;
use gc_arena::{Gc, Mutation, RefLock};
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)]
@@ -14,8 +14,32 @@ impl<'gc> crate::Vm<'gc> {
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
if let (Some(ls), Some(rs)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
let ns = Gc::new(mc, crate::NixString::new(format!("{ls}{rs}")));
// 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.)
if lhs.is::<Path>() {
let (Some(ls), Some(rs)) = (ctx.get_string_or_path(lhs), ctx.get_string_or_path(rhs))
else {
return self.finish_err(fix_error::Error::eval_error(format!(
"cannot append {} to a path",
rhs.ty()
)));
};
let combined = format!("{ls}{rs}");
let canon = canon_path_str(&combined);
let sid = ctx.intern_string(canon);
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)) {
let merged = ctx
.get_string_context(lhs)
.merge(ctx.get_string_context(rhs));
let ns = Gc::new(
mc,
crate::NixString::with_context(format!("{ls}{rs}"), merged),
);
self.push(Value::new_gc(ns));
return Step::Continue(());
}
@@ -87,12 +111,7 @@ impl<'gc> crate::Vm<'gc> {
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let eq = match self.values_equal(ctx, lhs, rhs) {
Ok(eq) => eq,
Err(e) => return self.finish_vm_err(e),
};
self.push(Value::new_inline(eq));
Step::Continue(())
fix_primops::start_eq(self, ctx, reader, mc, lhs, rhs, false)
}
#[inline(always)]
@@ -103,12 +122,7 @@ impl<'gc> crate::Vm<'gc> {
mc: &Mutation<'gc>,
) -> Step {
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
let eq = match self.values_equal(ctx, lhs, rhs) {
Ok(eq) => eq,
Err(e) => return self.finish_vm_err(e),
};
self.push(Value::new_inline(!eq));
Step::Continue(())
fix_primops::start_eq(self, ctx, reader, mc, lhs, rhs, true)
}
#[inline(always)]
@@ -212,63 +226,6 @@ impl<'gc> crate::Vm<'gc> {
Step::Continue(())
}
pub(crate) fn values_equal(
&mut self,
ctx: &impl VmRuntimeCtx,
lhs: StrictValue<'gc>,
rhs: StrictValue<'gc>,
) -> crate::VmResult<bool> {
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
return Ok(match (a, b) {
(NixNum::Int(a), NixNum::Int(b)) => a == b,
(NixNum::Float(a), NixNum::Float(b)) => a == b,
(NixNum::Int(a), NixNum::Float(b)) => a as f64 == b,
(NixNum::Float(a), NixNum::Int(b)) => a == b as f64,
});
}
if let (Some(a), Some(b)) = (lhs.as_inline::<bool>(), rhs.as_inline::<bool>()) {
return Ok(a == b);
}
if lhs.is::<crate::Null>() && rhs.is::<crate::Null>() {
return Ok(true);
}
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
return Ok(a == b);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::List>(), rhs.as_gc::<crate::List>()) {
if a.inner.borrow().len() != b.inner.borrow().len() {
return Ok(false);
}
for (x, y) in a.inner.borrow().iter().zip(b.inner.borrow().iter()) {
let lx = x.restrict().expect("forced");
let ly = y.restrict().expect("forced");
if !self.values_equal(ctx, lx, ly)? {
return Ok(false);
}
}
return Ok(true);
}
if let (Some(a), Some(b)) = (lhs.as_gc::<crate::AttrSet>(), rhs.as_gc::<crate::AttrSet>()) {
let a = &a.entries;
let b = &b.entries;
if a.len() != b.len() {
return Ok(false);
}
for ((k1, v1), (k2, v2)) in a.iter().zip(b.iter()) {
if k1 != k2 {
return Ok(false);
}
let lv1 = v1.restrict().expect("forced");
let lv2 = v2.restrict().expect("forced");
if !self.values_equal(ctx, lv1, lv2)? {
return Ok(false);
}
}
return Ok(true);
}
Ok(false)
}
fn compare_values_inner(
&mut self,
ctx: &impl VmRuntimeCtx,
@@ -294,6 +251,12 @@ impl<'gc> crate::Vm<'gc> {
self.push(Value::new_inline(pred(a.cmp(b))));
return Ok(());
}
if let (Some(a), Some(b)) = (lhs.as_inline::<Path>(), rhs.as_inline::<Path>()) {
let a = ctx.resolve_string(a.0);
let b = ctx.resolve_string(b.0);
self.push(Value::new_inline(pred(a.cmp(b))));
return Ok(());
}
// TODO: compare other types
Err(crate::vm_err("cannot compare these types"))
}
+15 -3
View File
@@ -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,
@@ -77,10 +77,12 @@ impl<'gc> crate::Vm<'gc> {
});
reader.set_pc(app.primop.dispatch_ip as usize)
} else {
let new_app = PrimOpApp {
let position = (app.primop.arity - app.arity) as usize;
let mut new_app = PrimOpApp {
arity: app.arity - 1,
..*app
};
new_app.args[position] = arg;
self.push(Value::new_gc(Gc::new(mc, new_app)))
}
} else if let Some(attrs) = func.as_gc::<AttrSet>()
@@ -118,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)
}
@@ -168,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)
}
}
+4 -4
View File
@@ -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,
+1 -1
View File
@@ -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> {
+37 -30
View File
@@ -1,11 +1,13 @@
use std::path::{Component, PathBuf};
use std::path::PathBuf;
use fix_abstract_vm::{
AttrSet, NixString, Path, StrictValue, StringContext, canon_path_str
};
use fix_builtins::BuiltinId;
use fix_common::StringId;
use fix_error::Error;
use num_enum::TryFromPrimitive;
use crate::value::{AttrSet, NixString, StrictValue};
use crate::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
impl<'gc> crate::Vm<'gc> {
@@ -38,6 +40,7 @@ impl<'gc> crate::Vm<'gc> {
&mut self,
ctx: &impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
) -> Step {
let slot_id = reader.read_u32();
let name = reader.read_string_id();
@@ -50,9 +53,7 @@ impl<'gc> crate::Vm<'gc> {
}
};
let Some(attrs) = scope.as_gc::<AttrSet>() else {
return self.finish_err(Error::eval_error(
"internal: scope slot is not an attrset",
));
return self.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
};
match attrs.lookup(name) {
Some(val) => {
@@ -75,6 +76,10 @@ impl<'gc> crate::Vm<'gc> {
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
if val.is::<StringId>() || val.is::<NixString>() {
self.push(val.relax());
} else if let Some(p) = val.as_inline::<Path>() {
// Coercing a path to a string yields the canonical path text.
// FIXME: copy to store
self.push(Value::new_inline(p.0));
} else {
todo!("coerce other types to string: {:?}", val.ty());
}
@@ -86,29 +91,45 @@ impl<'gc> crate::Vm<'gc> {
&mut self,
ctx: &mut impl VmRuntimeCtx,
reader: &mut BytecodeReader<'_>,
_mc: &gc_arena::Mutation<'gc>,
mc: &gc_arena::Mutation<'gc>,
) -> Step {
let count = reader.read_u16() as usize;
let _force_string = reader.read_u8() != 0;
let mut total_len = 0;
let mut has_any_context = false;
for i in 0..count {
let val = self.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
total_len += s.len();
if !ctx.get_string_context(val).is_empty() {
has_any_context = true;
}
}
let mut result = String::with_capacity(total_len);
let mut merged = StringContext::new();
for i in 0..count {
let val = self.peek_forced(count - 1 - i);
let s = ctx.get_string(val).expect("coerced");
result.push_str(s);
if has_any_context {
let ctx = ctx.get_string_context(val);
if !ctx.is_empty() {
merged = merged.merge(ctx);
}
}
}
self.stack.truncate(self.stack.len() - count);
if merged.is_empty() {
let sid = ctx.intern_string(result);
self.push(Value::new_inline(sid));
} else {
let ns = gc_arena::Gc::new(mc, NixString::with_context(result, merged));
self.push(Value::new_gc(ns));
}
Step::Continue(())
}
@@ -121,6 +142,11 @@ impl<'gc> crate::Vm<'gc> {
) -> Step {
let path_val = self.force_and_retry::<StrictValue>(reader, mc)?;
let dir_id = reader.read_string_id();
// Already a path: keep as-is. ResolvePath is idempotent on paths.
if let Some(p) = path_val.as_inline::<Path>() {
self.push(Value::new_inline(p));
return Step::Continue(());
}
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
@@ -135,24 +161,17 @@ impl<'gc> crate::Vm<'gc> {
Err(e) => return self.finish_err(e),
};
let sid = ctx.intern_string(resolved);
self.push(Value::new_inline(sid));
self.push(Value::new_inline(Path(sid)));
Step::Continue(())
}
}
/// Resolve a Nix path literal against `current_dir`.
///
/// Mirrors nix-js's `op_resolve_path`: absolute paths returned as-is, `~/X`
/// expanded against `$HOME`, otherwise joined onto `current_dir`. The result
/// is normalized by removing `.` components and resolving `..` lexically
/// (no symlink resolution).
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
let raw = if path.starts_with('/') {
return Ok(path.to_owned());
return Ok(canon_path_str(path));
} else if let Some(rest) = path.strip_prefix("~/") {
#[allow(deprecated)]
let mut dir = std::env::home_dir()
.ok_or_else(|| Error::eval_error("home dir not defined"))?;
let mut dir =
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
dir.push(rest);
dir
} else {
@@ -160,17 +179,5 @@ fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>>
dir.push(path);
dir
};
let mut normalized = PathBuf::new();
for component in raw.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),
}
}
Ok(normalized.to_string_lossy().into_owned())
Ok(canon_path_str(&raw))
}
+2 -2
View File
@@ -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();
+188 -285
View File
@@ -11,209 +11,19 @@ use fix_builtins::{BUILTINS, BuiltinId};
use fix_codegen::InstructionPtr;
use fix_common::StringId;
use fix_error::{Error, Result, Source};
use gc_arena::arena::CollectionPhase;
use gc_arena::metrics::Pacing;
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<T> = std::result::Result<T, VmError>;
#[allow(dead_code)]
enum VmError {
Catchable(String),
Uncatchable(Box<Error>),
}
impl From<Box<Error>> for VmError {
fn from(e: Box<Error>) -> Self {
VmError::Uncatchable(e)
}
}
impl VmError {
fn into_error(self) -> Box<Error> {
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<str>) -> 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<ExtraScope>,
ctx: &mut impl VmRuntimeCtx,
) -> fix_error::Result<InstructionPtr>;
}
/// 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<StringId>,
slot_id: u32,
},
}
trait VmRuntimeCtxExt: VmRuntimeCtx {
fn get_string<'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<StringId, NixType>;
fn convert_value(&self, val: Value) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> VmRuntimeCtxExt for T {
fn get_string<'a, 'gc: 'a>(&'a self, val: StrictValue<'gc>) -> Option<&'a str> {
if let Some(sid) = val.as_inline::<StringId>() {
Some(self.resolve_string(sid))
} else {
val.as_gc::<NixString>().map(|ns| ns.as_ref().as_str())
}
}
fn get_string_id<'a, 'gc: 'a>(
&'a mut self,
val: StrictValue<'gc>,
) -> std::result::Result<StringId, NixType> {
if let Some(sid) = val.as_inline::<StringId>() {
Ok(sid)
} else if let Some(s) = val.as_gc::<NixString>().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<u64>) -> fix_common::Value;
}
impl<T: VmRuntimeCtx> ConvertValueWithSeen for T {
fn convert_value_with_seen(&self, val: Value, seen: &mut HashSet<u64>) -> fix_common::Value {
use fix_common::Value;
if let Some(i) = val.as_inline::<i32>() {
Value::Int(i as i64)
} else if let Some(gc_i) = val.as_gc::<i64>() {
Value::Int(*gc_i)
} else if let Some(f) = val.as_float() {
Value::Float(f)
} else if let Some(b) = val.as_inline::<bool>() {
Value::Bool(b)
} else if val.is::<Null>() {
Value::Null
} else if let Some(sid) = val.as_inline::<StringId>() {
let s = self.resolve_string(sid).to_owned();
Value::String(s)
} else if let Some(ns) = val.as_gc::<NixString>() {
Value::String(ns.as_str().to_owned())
} else if let Some(attrs) = val.as_gc::<AttrSet>() {
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::<List>() {
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::<Closure>() {
Value::Func
} else if let Some(thunk) = val.as_gc::<Thunk>() {
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::<PrimOp>() {
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::<PrimOpApp>() {
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<Break>;
#[derive(Collect)]
#[collect(no_drop)]
pub struct Vm<'gc> {
@@ -227,10 +37,6 @@ pub struct Vm<'gc> {
env: GcEnv<'gc>,
import_cache: HashMap<PathBuf, Value<'gc>>,
/// Stack of attrsets, one per `scopedImport` invocation. Indexed by
/// `slot_id` baked into the bytecode at compile time. Slots are never
/// freed — they live for the lifetime of the [`Vm`] (i.e. one
/// `Vm::run` call).
scope_slots: Vec<Value<'gc>>,
builtins: Value<'gc>,
@@ -242,75 +48,12 @@ pub struct Vm<'gc> {
#[collect(require_static)]
result: Option<Result<fix_common::Value>>,
/// Set by `import` / `scopedImport` primops just before they yield
/// `Break::LoadFile`. The outer [`Vm::run`] loop consumes it via
/// `Action::LoadFile`.
#[collect(require_static)]
pending_load: Option<PendingLoad>,
functor_sym: StringId,
}
#[derive(Debug)]
pub(crate) struct PendingLoad {
pub path: PathBuf,
pub scope: Option<PendingScope>,
}
#[derive(Debug)]
pub(crate) struct PendingScope {
pub keys: HashSet<StringId>,
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::<AttrSet>()
.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::<AttrSet>().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());
@@ -433,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)
@@ -442,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")
@@ -572,18 +315,168 @@ impl<'gc> Vm<'gc> {
}
}
#[allow(dead_code)]
struct ErrorFrame {
span_id: u32,
message: Option<String>,
}
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<Gc<'gc, Thunk<'gc>>>,
env: Gc<'gc, RefLock<Env<'gc>>>,
#[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<CallFrame<'gc>> {
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<Error>) -> 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<Value<'gc>> {
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 {
@@ -592,9 +485,18 @@ enum Action {
LoadFile(PendingLoad),
}
enum NixNum {
Int(i64),
Float(f64),
/// Compute initial heap size mirroring CppNix's strategy: 25% of physical RAM,
/// clamped to [32 MiB, 384 MiB]. Used as `Pacing::min_sleep` so the collector
/// defers the first cycle until the heap reaches this size.
fn initial_heap_size() -> usize {
const MIN_SIZE: usize = 32 * 1024 * 1024;
const MAX_SIZE: usize = 384 * 1024 * 1024;
let mut sys = sysinfo::System::new();
sys.refresh_memory();
let total = sys.total_memory() as usize;
let quarter = total / 4;
quarter.clamp(MIN_SIZE, MAX_SIZE)
}
impl Vm<'_> {
@@ -605,8 +507,10 @@ impl Vm<'_> {
) -> Result<fix_common::Value> {
let (code, runtime) = ctx.split();
let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime));
const COLLECTOR_GRANULARITY: f64 = 1024.0;
arena.metrics().set_pacing(Pacing {
min_sleep: initial_heap_size(),
..Pacing::STOP_THE_WORLD
});
let mut pc = ip.0;
loop {
@@ -614,12 +518,8 @@ impl Vm<'_> {
match arena.mutate_root(|mc, root| root.dispatch_batch(bytecode, runtime, pc, mc)) {
Action::Continue { pc: new_pc } => {
pc = new_pc;
if arena.metrics().allocation_debt() > COLLECTOR_GRANULARITY {
if arena.collection_phase() == CollectionPhase::Sweeping {
arena.collect_debt();
} else if let Some(marked) = arena.mark_debt() {
marked.start_sweeping();
}
if arena.metrics().allocation_debt() > 0.0 {
arena.finish_cycle();
}
}
Action::LoadFile(load) => {
@@ -636,6 +536,9 @@ impl Vm<'_> {
Err(err) => break Err(err),
};
pc = new_ip.0;
arena.mutate_root(|mc, root| {
root.env = Gc::new(mc, RefLock::new(Env::empty()));
});
}
Action::Done(done) => break done,
}
@@ -768,7 +671,7 @@ impl<'gc> Vm<'gc> {
LoadBuiltin => self.op_load_builtin(&mut reader),
LoadReplBinding => self.op_load_repl_binding(&mut reader),
LoadScopedBinding => self.op_load_scoped_binding(ctx, &mut reader),
LoadScopedBinding => self.op_load_scoped_binding(ctx, &mut reader, mc),
Illegal => unreachable!(),
};
-1
View File
@@ -1 +0,0 @@
-364
View File
@@ -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("<non-string-value>");
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::<AttrSet>() {
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::<List<'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::<i32>().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::<List<'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::<i32>().unwrap();
let mut added: usize = 0;
if let Some(attrs) = item.as_gc::<AttrSet>() {
let attrs = &attrs.entries;
#[allow(clippy::unwrap_used)]
let seen = self.peek_forced(2).as_gc::<List<'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::<List<'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::<List<'gc>>() {
#[allow(clippy::unwrap_used)]
let seen = self.peek_forced(2).as_gc::<List<'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::<List<'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::<AttrSet>() {
let len = attrs.entries.len();
(len, len > 0)
} else if let Some(list) = val.as_gc::<List<'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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let len = self.peek(0).as_inline::<i32>().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::<AttrSet>() {
attrs.entries.get(idx as usize).map(|&(_, v)| v)
} else if let Some(list) = val.as_gc::<List<'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::<StrictValue>(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::<StrictValue>(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<Closure>, Gc<AttrSet>)>(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::<AttrSet>() || val.is::<List<'_>>()
}
-1
View File
@@ -1 +0,0 @@
-169
View File
@@ -1,169 +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::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::<StrictValue>(reader, mc)?;
let path_str = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
return self.finish_err(Error::eval_error(format!(
"expected a path 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<'_>,
_mc: &Mutation<'gc>,
) -> Step {
// stack: [path_sid, return_value]
let val = self.pop();
#[allow(clippy::unwrap_used)]
let path_sid = self.pop().as_inline::<StringId>().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.return_from_primop(val, reader)
}
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<AttrSet>, StrictValue)>(reader, mc)?;
let path_str = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
return self.finish_err(Error::eval_error(format!(
"expected a path 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<StringId> = 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::<StrictValue>(reader, mc)?;
let path = match ctx.get_string(path_val) {
Some(s) => s.to_owned(),
None => {
return self.finish_err(Error::eval_error(format!(
"expected a path string, got {}",
path_val.ty()
)));
}
};
let must_be_dir = path.ends_with('/') || path.ends_with("/.");
let p = std::path::Path::new(&path);
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<PathBuf, Box<Error>> {
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)
}
-73
View File
@@ -1,73 +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::<List>() {
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::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let elem = self.peek_forced(2).as_gc::<List>().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::<bool>(reader, mc)?;
#[allow(clippy::unwrap_used)]
let idx = self.peek(1).as_inline::<i32>().unwrap();
#[allow(clippy::unwrap_used)]
let list = self.peek_forced(2).as_gc::<List>().unwrap();
let list = list.inner.borrow();
#[allow(clippy::unwrap_used)]
let acc = self.peek_forced(0).as_gc::<List>().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(())
}
}
-79
View File
@@ -1,79 +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),
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, mc),
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),
phase => todo!("primop phase {phase:?}"),
}
}
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(())
}
}
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@
View File
-1
View File
@@ -1 +0,0 @@
+1
View File
@@ -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" }
+8 -6
View File
@@ -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 _};
@@ -34,8 +35,6 @@ pub struct CodeState {
pub spans: Vec<(usize, rnix::TextRange)>,
pub thunk_count: usize,
pub global_env: HashMap<StringId, MaybeThunk>,
/// Set during a compilation pass when the code is being compiled under a
/// `scopedImport` scope. Read by [`CompilerCtx::current_scope_slot`].
pub current_scope_slot: Option<u32>,
}
@@ -157,7 +156,7 @@ impl VmCode for CodeState {
fn compile_with_scope(
&mut self,
source: Source,
extra_scope: Option<fix_vm::ExtraScope>,
extra_scope: Option<fix_abstract_vm::ExtraScope>,
runtime: &mut impl VmRuntimeCtx,
) -> Result<InstructionPtr> {
let mut compiler = CompilerCtx {
@@ -165,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 }
}
});
@@ -638,7 +637,10 @@ enum Scope<'ctx, 'id, 'ir> {
slot_id: u32,
},
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
Param { sym: StringId, abs_layer: u8 },
Param {
sym: StringId,
abs_layer: u8,
},
}
pub enum ExtraScope<'ctx> {
-209
View File
@@ -1,209 +0,0 @@
use std::collections::{BTreeMap, BTreeSet, VecDeque};
pub enum StringContextElem {
Opaque { path: String },
DrvDeep { drv_path: String },
Built { drv_path: String, output: String },
}
impl StringContextElem {
pub fn decode(encoded: &str) -> Self {
if let Some(drv_path) = encoded.strip_prefix('=') {
StringContextElem::DrvDeep {
drv_path: drv_path.to_string(),
}
} else if let Some(rest) = encoded.strip_prefix('!') {
if let Some(second_bang) = rest.find('!') {
let output = rest[..second_bang].to_string();
let drv_path = rest[second_bang + 1..].to_string();
StringContextElem::Built { drv_path, output }
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
} else {
StringContextElem::Opaque {
path: encoded.to_string(),
}
}
}
}
pub type InputDrvs = BTreeMap<String, BTreeSet<String>>;
pub type Srcs = BTreeSet<String>;
pub fn extract_input_drvs_and_srcs(context: &[String]) -> Result<(InputDrvs, Srcs), String> {
let mut input_drvs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
let mut input_srcs: BTreeSet<String> = BTreeSet::new();
for encoded in context {
match StringContextElem::decode(encoded) {
StringContextElem::Opaque { path } => {
input_srcs.insert(path);
}
StringContextElem::DrvDeep { drv_path } => {
compute_fs_closure(&drv_path, &mut input_drvs, &mut input_srcs)?;
}
StringContextElem::Built { drv_path, output } => {
input_drvs.entry(drv_path).or_default().insert(output);
}
}
}
Ok((input_drvs, input_srcs))
}
fn compute_fs_closure(
drv_path: &str,
input_drvs: &mut BTreeMap<String, BTreeSet<String>>,
input_srcs: &mut BTreeSet<String>,
) -> Result<(), String> {
let mut queue: VecDeque<String> = VecDeque::new();
let mut visited: BTreeSet<String> = BTreeSet::new();
queue.push_back(drv_path.to_string());
while let Some(current_path) = queue.pop_front() {
if visited.contains(&current_path) {
continue;
}
visited.insert(current_path.clone());
input_srcs.insert(current_path.clone());
if !current_path.ends_with(".drv") {
continue;
}
let content = std::fs::read_to_string(&current_path)
.map_err(|e| format!("failed to read derivation {}: {}", current_path, e))?;
let inputs = parse_derivation_inputs(&content)
.ok_or_else(|| format!("failed to parse derivation {}", current_path))?;
for src in inputs.input_srcs {
input_srcs.insert(src.clone());
if !visited.contains(&src) {
queue.push_back(src);
}
}
for (dep_drv, outputs) in inputs.input_drvs {
input_srcs.insert(dep_drv.clone());
let entry = input_drvs.entry(dep_drv.clone()).or_default();
for output in outputs {
entry.insert(output);
}
if !visited.contains(&dep_drv) {
queue.push_back(dep_drv);
}
}
}
Ok(())
}
struct DerivationInputs {
input_drvs: Vec<(String, Vec<String>)>,
input_srcs: Vec<String>,
}
fn parse_derivation_inputs(aterm: &str) -> Option<DerivationInputs> {
let aterm = aterm.strip_prefix("Derive([")?;
let mut bracket_count: i32 = 1;
let mut pos = 0;
let bytes = aterm.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
_ => {}
}
pos += 1;
}
if bracket_count != 0 {
return None;
}
let rest = &aterm[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_drvs = Vec::new();
let mut bracket_count: i32 = 1;
let mut start = 0;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'(' if bracket_count == 1 => {
start = pos;
}
b')' if bracket_count == 1 => {
let entry = &rest[start + 1..pos];
if let Some((drv_path, outputs)) = parse_input_drv_entry(entry) {
input_drvs.push((drv_path, outputs));
}
}
_ => {}
}
pos += 1;
}
let rest = &rest[pos..];
let rest = rest.strip_prefix(",[")?;
let mut input_srcs = Vec::new();
bracket_count = 1;
pos = 0;
let bytes = rest.as_bytes();
while pos < bytes.len() && bracket_count > 0 {
match bytes[pos] {
b'[' => bracket_count += 1,
b']' => bracket_count -= 1,
b'"' if bracket_count == 1 => {
pos += 1;
let src_start = pos;
while pos < bytes.len() && bytes[pos] != b'"' {
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
pos += 2;
} else {
pos += 1;
}
}
let src = std::str::from_utf8(&bytes[src_start..pos]).ok()?;
input_srcs.push(src.to_string());
}
_ => {}
}
pos += 1;
}
Some(DerivationInputs {
input_drvs,
input_srcs,
})
}
fn parse_input_drv_entry(entry: &str) -> Option<(String, Vec<String>)> {
let entry = entry.strip_prefix('"')?;
let quote_end = entry.find('"')?;
let drv_path = entry[..quote_end].to_string();
let rest = &entry[quote_end + 1..];
let rest = rest.strip_prefix(",[")?;
let rest = rest.strip_suffix(']')?;
let mut outputs = Vec::new();
for part in rest.split(',') {
let part = part.trim();
if let Some(name) = part.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
outputs.push(name.to_string());
}
}
Some((drv_path, outputs))
}
Generated
+18 -18
View File
@@ -45,11 +45,11 @@
]
},
"locked": {
"lastModified": 1777369708,
"narHash": "sha256-1xW7cRZNsFNPQD+cE0fwnLVStnDth0HSoASEIFeT7uI=",
"lastModified": 1778445566,
"narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
"owner": "nix-community",
"repo": "bun2nix",
"rev": "e659e1cc4b8e1b21d0aa85f1c481f9db61ecfa98",
"rev": "2499dedd70744dba1815875b854818a3019e9e4c",
"type": "github"
},
"original": {
@@ -67,11 +67,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1777796307,
"narHash": "sha256-L7xLjorTwVf2aLu5b0ZZY2D0RFXwD/a/a/fFFDikB2w=",
"lastModified": 1778919578,
"narHash": "sha256-+z+jgTly48gsAiX8rOe/vs8C/2G4vdCpcEtqMJUpFqw=",
"owner": "nix-community",
"repo": "fenix",
"rev": "0f9881f2344c0b1c100bd9e774555759b7da6fd5",
"rev": "ecd6d4ff22cfdb1339b2915455a2ff4dc85bf52e",
"type": "github"
},
"original": {
@@ -102,11 +102,11 @@
]
},
"locked": {
"lastModified": 1777678872,
"narHash": "sha256-EPIFsulyon7Z1vLQq5Fk64GR8L7cQsT+IPhcsukVbgk=",
"lastModified": 1778716662,
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "5250617bffd85403b14dbf43c3870e7f255d2c16",
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
"type": "github"
},
"original": {
@@ -127,11 +127,11 @@
"treefmt-nix": "treefmt-nix"
},
"locked": {
"lastModified": 1777786380,
"narHash": "sha256-GGKC1WrEoTafJwIXn+fim6cZ/w1ZWVc+DUYdk2lvPvA=",
"lastModified": 1778929997,
"narHash": "sha256-iAfbBUHBbR0N4DFqFWr4Jtmpc1YcOK7kpVM4f0MK1V8=",
"owner": "numtide",
"repo": "llm-agents.nix",
"rev": "961b1096bc0b2ecc7096e360646cd2f29671c55e",
"rev": "0da8f0313c9f68c155e0932f880fc1913e7be846",
"type": "github"
},
"original": {
@@ -142,11 +142,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1777578337,
"narHash": "sha256-Ad49moKWeXtKBJNy2ebiTQUEgdLyvGmTeykAQ9xM+Z4=",
"lastModified": 1778443072,
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "15f4ee454b1dce334612fa6843b3e05cf546efab",
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
"type": "github"
},
"original": {
@@ -167,11 +167,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1777768857,
"narHash": "sha256-zfekJcaVctfAps1KDHwZpwkvAQn7GObRHh3Gl3xocGI=",
"lastModified": 1778854817,
"narHash": "sha256-iG+VuMy8W585geVVCUd7pR025WsY3ZkgSv5Yt5bxDmQ=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "1102c0b633599564919e36076d4362d7e68dbddc",
"rev": "1a68212c5683555ad80f0eab71db9715c6d52145",
"type": "github"
},
"original": {