Compare commits
1 Commits
main
..
791c20660c
| Author | SHA1 | Date | |
|---|---|---|---|
| 791c20660c |
@@ -7,7 +7,3 @@ flamegraph*.svg
|
|||||||
perf.data*
|
perf.data*
|
||||||
profile.json.gz
|
profile.json.gz
|
||||||
prof.json
|
prof.json
|
||||||
*.cpuprofile
|
|
||||||
*.cpuprofile.gz
|
|
||||||
*v8.log*
|
|
||||||
callgrind.*
|
|
||||||
|
|||||||
@@ -3,21 +3,12 @@ vim.lsp.config("biome", {
|
|||||||
on_dir(vim.fn.getcwd())
|
on_dir(vim.fn.getcwd())
|
||||||
end
|
end
|
||||||
})
|
})
|
||||||
vim.lsp.config("eslint", {
|
|
||||||
settings = {
|
|
||||||
eslint = {
|
|
||||||
options = {
|
|
||||||
configFile = "./nix-js/runtime-ts/eslint.config.mts"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
vim.lsp.config("rust_analyzer", {
|
vim.lsp.config("rust_analyzer", {
|
||||||
settings = {
|
settings = {
|
||||||
["rust-analyzer"] = {
|
["rust-analyzer"] = {
|
||||||
cargo = {
|
cargo = {
|
||||||
features = {
|
features = {
|
||||||
"tailcall"
|
"inspector"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Generated
+3034
-468
File diff suppressed because it is too large
Load Diff
+2
-34
@@ -1,42 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"fix",
|
"nix-js",
|
||||||
"fix-abstract-vm",
|
"nix-js-macros"
|
||||||
"fix-builtins",
|
|
||||||
"fix-codegen",
|
|
||||||
"fix-common",
|
|
||||||
"fix-error",
|
|
||||||
"fix-ir",
|
|
||||||
"fix-primops",
|
|
||||||
"fix-vm",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
[profile.profiling]
|
[profile.profiling]
|
||||||
inherits = "release"
|
inherits = "release"
|
||||||
debug = true
|
debug = true
|
||||||
|
|
||||||
[profile.lto]
|
|
||||||
inherits = "release"
|
|
||||||
lto = true
|
|
||||||
|
|
||||||
[workspace.dependencies]
|
|
||||||
bumpalo = { version = "3.20", features = [
|
|
||||||
"allocator-api2",
|
|
||||||
"boxed",
|
|
||||||
"collections",
|
|
||||||
] }
|
|
||||||
ghost-cell = "0.2"
|
|
||||||
hashbrown = "0.16"
|
|
||||||
num_enum = "0.7.5"
|
|
||||||
smallvec = { version = "1.15", features = ["const_new", "const_generics"] }
|
|
||||||
ere = "0.2"
|
|
||||||
string-interner = "0.19"
|
|
||||||
rnix = "0.14"
|
|
||||||
rowan = "0.16"
|
|
||||||
likely_stable = "0.1"
|
|
||||||
|
|
||||||
[workspace.dependencies.gc-arena]
|
|
||||||
git = "https://github.com/kyren/gc-arena"
|
|
||||||
rev = "75671ae03f53718357b741ed4027560f14e90836"
|
|
||||||
features = ["allocator-api2", "hashbrown", "smallvec"]
|
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@eval expr:
|
@eval expr:
|
||||||
cargo run -- eval --expr '{{expr}}'
|
cargo run -- eval '{{expr}}'
|
||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@replr:
|
@replr:
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@evalr expr:
|
@evalr expr:
|
||||||
cargo run --release -- eval --expr '{{expr}}'
|
cargo run --release -- eval '{{expr}}'
|
||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@repli:
|
@repli:
|
||||||
@@ -20,17 +20,4 @@
|
|||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@evali expr:
|
@evali expr:
|
||||||
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 eval --expr '{{expr}}'
|
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 eval '{{expr}}'
|
||||||
|
|
||||||
[no-exit-message]
|
|
||||||
@replp:
|
|
||||||
cargo run --release --features prof -- repl
|
|
||||||
|
|
||||||
[no-exit-message]
|
|
||||||
@evalp expr:
|
|
||||||
cargo run --release --features prof -- eval --expr '{{expr}}'
|
|
||||||
|
|
||||||
[no-exit-message]
|
|
||||||
[positional-arguments]
|
|
||||||
@cg *args='':
|
|
||||||
valgrind --tool=callgrind --dump-instr=yes --collect-jumps=yes "$@"
|
|
||||||
|
|||||||
+70
@@ -0,0 +1,70 @@
|
|||||||
|
{
|
||||||
|
"$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
[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" }
|
|
||||||
@@ -1,502 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::fmt;
|
|
||||||
use std::num::NonZeroU8;
|
|
||||||
|
|
||||||
use sptr::Strict;
|
|
||||||
|
|
||||||
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
|
|
||||||
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
|
|
||||||
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
|
|
||||||
|
|
||||||
pub(crate) trait ArrayExt<const LEN: usize> {
|
|
||||||
type Elem;
|
|
||||||
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
|
|
||||||
type Elem = T;
|
|
||||||
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
|
|
||||||
let copy_len = usize::min(N, M);
|
|
||||||
let mut out = [T::default(); M];
|
|
||||||
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
|
|
||||||
out
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RawStore: Sized {
|
|
||||||
fn to_val(self, value: &mut Value);
|
|
||||||
fn from_val(value: &Value) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawStore for [u8; 6] {
|
|
||||||
#[inline]
|
|
||||||
fn to_val(self, value: &mut Value) {
|
|
||||||
value.set_data(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn from_val(value: &Value) -> Self {
|
|
||||||
*value.data()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawStore for bool {
|
|
||||||
#[inline]
|
|
||||||
fn to_val(self, value: &mut Value) {
|
|
||||||
value.set_data([u8::from(self)].truncate_to());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn from_val(value: &Value) -> Self {
|
|
||||||
value.data()[0] == 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! int_store {
|
|
||||||
($ty:ty) => {
|
|
||||||
impl RawStore for $ty {
|
|
||||||
#[inline]
|
|
||||||
fn to_val(self, value: &mut Value) {
|
|
||||||
let bytes = self.to_ne_bytes();
|
|
||||||
value.set_data(bytes.truncate_to());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn from_val(value: &Value) -> Self {
|
|
||||||
<$ty>::from_ne_bytes(value.data().truncate_to())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
int_store!(u8);
|
|
||||||
int_store!(u16);
|
|
||||||
int_store!(u32);
|
|
||||||
|
|
||||||
int_store!(i8);
|
|
||||||
int_store!(i16);
|
|
||||||
int_store!(i32);
|
|
||||||
|
|
||||||
fn store_ptr<P: Strict + Copy>(value: &mut Value, ptr: P) {
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
{
|
|
||||||
assert!(
|
|
||||||
ptr.addr() <= 0x0000_FFFF_FFFF_FFFF,
|
|
||||||
"Pointer too large to store in NaN box"
|
|
||||||
);
|
|
||||||
|
|
||||||
let val = (unsafe { value.whole_mut() } as *mut [u8; 8]).cast::<P>();
|
|
||||||
|
|
||||||
let ptr = Strict::map_addr(ptr, |addr| {
|
|
||||||
addr | (usize::from(value.header().into_raw()) << 48)
|
|
||||||
});
|
|
||||||
|
|
||||||
unsafe { val.write(ptr) };
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
{
|
|
||||||
let _ = (value, ptr);
|
|
||||||
unimplemented!("32-bit pointer storage not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn load_ptr<P: Strict>(value: &Value) -> P {
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
{
|
|
||||||
let val = (unsafe { value.whole() } as *const [u8; 8]).cast::<P>();
|
|
||||||
let ptr = unsafe { val.read() };
|
|
||||||
Strict::map_addr(ptr, |addr| addr & 0x0000_FFFF_FFFF_FFFF)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
{
|
|
||||||
let _ = value;
|
|
||||||
unimplemented!("32-bit pointer storage not supported");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RawStore for *const T {
|
|
||||||
fn to_val(self, value: &mut Value) {
|
|
||||||
store_ptr::<*const T>(value, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_val(value: &Value) -> Self {
|
|
||||||
load_ptr::<*const T>(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<T> RawStore for *mut T {
|
|
||||||
fn to_val(self, value: &mut Value) {
|
|
||||||
store_ptr::<*mut T>(value, self);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn from_val(value: &Value) -> Self {
|
|
||||||
load_ptr::<*mut T>(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
||||||
enum TagVal {
|
|
||||||
_P1,
|
|
||||||
_P2,
|
|
||||||
_P3,
|
|
||||||
_P4,
|
|
||||||
_P5,
|
|
||||||
_P6,
|
|
||||||
_P7,
|
|
||||||
|
|
||||||
_N1,
|
|
||||||
_N2,
|
|
||||||
_N3,
|
|
||||||
_N4,
|
|
||||||
_N5,
|
|
||||||
_N6,
|
|
||||||
_N7,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, PartialEq, Eq)]
|
|
||||||
pub struct RawTag(TagVal);
|
|
||||||
|
|
||||||
impl RawTag {
|
|
||||||
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 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]
|
|
||||||
pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag {
|
|
||||||
unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn new_checked(neg: bool, val: u8) -> Option<RawTag> {
|
|
||||||
Some(RawTag(match (neg, val) {
|
|
||||||
(false, 1) => TagVal::_P1,
|
|
||||||
(false, 2) => TagVal::_P2,
|
|
||||||
(false, 3) => TagVal::_P3,
|
|
||||||
(false, 4) => TagVal::_P4,
|
|
||||||
(false, 5) => TagVal::_P5,
|
|
||||||
(false, 6) => TagVal::_P6,
|
|
||||||
(false, 7) => TagVal::_P7,
|
|
||||||
|
|
||||||
(true, 1) => TagVal::_N1,
|
|
||||||
(true, 2) => TagVal::_N2,
|
|
||||||
(true, 3) => TagVal::_N3,
|
|
||||||
(true, 4) => TagVal::_N4,
|
|
||||||
(true, 5) => TagVal::_N5,
|
|
||||||
(true, 6) => TagVal::_N6,
|
|
||||||
(true, 7) => TagVal::_N7,
|
|
||||||
|
|
||||||
_ => return None,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// `val` must be in the range `1..8`
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) const unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
|
|
||||||
RawTag(match (neg, val) {
|
|
||||||
(false, 1) => TagVal::_P1,
|
|
||||||
(false, 2) => TagVal::_P2,
|
|
||||||
(false, 3) => TagVal::_P3,
|
|
||||||
(false, 4) => TagVal::_P4,
|
|
||||||
(false, 5) => TagVal::_P5,
|
|
||||||
(false, 6) => TagVal::_P6,
|
|
||||||
(false, 7) => TagVal::_P7,
|
|
||||||
|
|
||||||
(true, 1) => TagVal::_N1,
|
|
||||||
(true, 2) => TagVal::_N2,
|
|
||||||
(true, 3) => TagVal::_N3,
|
|
||||||
(true, 4) => TagVal::_N4,
|
|
||||||
(true, 5) => TagVal::_N5,
|
|
||||||
(true, 6) => TagVal::_N6,
|
|
||||||
(true, 7) => TagVal::_N7,
|
|
||||||
|
|
||||||
_ => unsafe { core::hint::unreachable_unchecked() },
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn is_neg(self) -> bool {
|
|
||||||
matches!(self.0, |TagVal::_N1| TagVal::_N2
|
|
||||||
| TagVal::_N3
|
|
||||||
| TagVal::_N4
|
|
||||||
| TagVal::_N5
|
|
||||||
| TagVal::_N6
|
|
||||||
| TagVal::_N7)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn val(self) -> NonZeroU8 {
|
|
||||||
match self.0 {
|
|
||||||
TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN,
|
|
||||||
TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1),
|
|
||||||
TagVal::_P3 | TagVal::_N3 => NonZeroU8::MIN.saturating_add(2),
|
|
||||||
TagVal::_P4 | TagVal::_N4 => NonZeroU8::MIN.saturating_add(3),
|
|
||||||
TagVal::_P5 | TagVal::_N5 => NonZeroU8::MIN.saturating_add(4),
|
|
||||||
TagVal::_P6 | TagVal::_N6 => NonZeroU8::MIN.saturating_add(5),
|
|
||||||
TagVal::_P7 | TagVal::_N7 => NonZeroU8::MIN.saturating_add(6),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub const fn neg_val(self) -> (bool, u8) {
|
|
||||||
match self.0 {
|
|
||||||
TagVal::_P1 => (false, 1),
|
|
||||||
TagVal::_P2 => (false, 2),
|
|
||||||
TagVal::_P3 => (false, 3),
|
|
||||||
TagVal::_P4 => (false, 4),
|
|
||||||
TagVal::_P5 => (false, 5),
|
|
||||||
TagVal::_P6 => (false, 6),
|
|
||||||
TagVal::_P7 => (false, 7),
|
|
||||||
TagVal::_N1 => (true, 1),
|
|
||||||
TagVal::_N2 => (true, 2),
|
|
||||||
TagVal::_N3 => (true, 3),
|
|
||||||
TagVal::_N4 => (true, 4),
|
|
||||||
TagVal::_N5 => (true, 5),
|
|
||||||
TagVal::_N6 => (true, 6),
|
|
||||||
TagVal::_N7 => (true, 7),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for RawTag {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
f.debug_struct("RawTag")
|
|
||||||
.field("neg", &self.is_neg())
|
|
||||||
.field("val", &self.val())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
struct Header(u16);
|
|
||||||
|
|
||||||
impl Header {
|
|
||||||
#[inline]
|
|
||||||
fn new(tag: RawTag) -> Header {
|
|
||||||
let (neg, val) = tag.neg_val();
|
|
||||||
Header(0x7FF8 | (u16::from(neg) << 15) | u16::from(val))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
const fn tag(self) -> RawTag {
|
|
||||||
unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
const fn get_sign(self) -> bool {
|
|
||||||
self.0 & 0x8000 != 0
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
const fn get_tag(self) -> u8 {
|
|
||||||
(self.0 & 0x0007) as u8
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn into_raw(self) -> u16 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, PartialEq)]
|
|
||||||
#[repr(C, align(8))]
|
|
||||||
pub struct Value {
|
|
||||||
#[cfg(target_endian = "big")]
|
|
||||||
header: Header,
|
|
||||||
data: [u8; 6],
|
|
||||||
#[cfg(target_endian = "little")]
|
|
||||||
header: Header,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Value {
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn new(tag: RawTag, data: [u8; 6]) -> Value {
|
|
||||||
Value {
|
|
||||||
header: Header::new(tag),
|
|
||||||
data,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn empty(tag: RawTag) -> Value {
|
|
||||||
Value::new(tag, [0; 6])
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
|
|
||||||
let mut v = Value::new(tag, [0; 6]);
|
|
||||||
T::to_val(val, &mut v);
|
|
||||||
v
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn load<T: RawStore>(self) -> T {
|
|
||||||
T::from_val(&self)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) const fn tag(&self) -> RawTag {
|
|
||||||
self.header.tag()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn header(&self) -> &Header {
|
|
||||||
&self.header
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn set_data(&mut self, val: [u8; 6]) {
|
|
||||||
self.data = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub fn data(&self) -> &[u8; 6] {
|
|
||||||
&self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn data_mut(&mut self) -> &mut [u8; 6] {
|
|
||||||
&mut self.data
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
unsafe fn whole(&self) -> &[u8; 8] {
|
|
||||||
let ptr = (self as *const Value).cast::<[u8; 8]>();
|
|
||||||
unsafe { &*ptr }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
|
|
||||||
let ptr = (self as *mut Value).cast::<[u8; 8]>();
|
|
||||||
unsafe { &mut *ptr }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
pub(crate) union RawBox {
|
|
||||||
float: f64,
|
|
||||||
value: Value,
|
|
||||||
bits: u64,
|
|
||||||
#[cfg(target_pointer_width = "64")]
|
|
||||||
ptr: *const (),
|
|
||||||
#[cfg(target_pointer_width = "32")]
|
|
||||||
ptr: (u32, *const ()),
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawBox {
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn from_float(val: f64) -> RawBox {
|
|
||||||
match (val.is_nan(), val.is_sign_positive()) {
|
|
||||||
(true, true) => RawBox {
|
|
||||||
float: f64::from_bits(QUIET_NAN),
|
|
||||||
},
|
|
||||||
(true, false) => RawBox {
|
|
||||||
float: f64::from_bits(NEG_QUIET_NAN),
|
|
||||||
},
|
|
||||||
(false, _) => RawBox { float: val },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn from_value(value: Value) -> RawBox {
|
|
||||||
RawBox { value }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) const fn tag(&self) -> Option<RawTag> {
|
|
||||||
if self.is_value() {
|
|
||||||
Some(unsafe { self.value.tag() })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn is_float(&self) -> bool {
|
|
||||||
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) const fn is_value(&self) -> bool {
|
|
||||||
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn float(&self) -> Option<&f64> {
|
|
||||||
if self.is_float() {
|
|
||||||
Some(unsafe { &self.float })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn value(&self) -> Option<&Value> {
|
|
||||||
if self.is_value() {
|
|
||||||
Some(unsafe { &self.value })
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub(crate) fn into_float_unchecked(self) -> f64 {
|
|
||||||
unsafe { self.float }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
#[must_use]
|
|
||||||
pub(crate) fn to_bits(self) -> u64 {
|
|
||||||
unsafe { self.bits }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for RawBox {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self.float() {
|
|
||||||
Some(val) => f.debug_tuple("RawBox::Float").field(val).finish(),
|
|
||||||
None => {
|
|
||||||
let val = self.value().expect("RawBox is neither float nor value");
|
|
||||||
|
|
||||||
f.debug_struct("RawBox::Data")
|
|
||||||
.field("tag", &val.tag())
|
|
||||||
.field("data", val.data())
|
|
||||||
.finish()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,142 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use fix_codegen::OperandType;
|
|
||||||
use fix_common::StringId;
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use string_interner::Symbol as _;
|
|
||||||
|
|
||||||
use crate::{OperandData, VmRuntimeCtx};
|
|
||||||
|
|
||||||
pub struct BytecodeReader<'a> {
|
|
||||||
bytecode: &'a [u8],
|
|
||||||
pc: usize,
|
|
||||||
inst_start_pc: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a> BytecodeReader<'a> {
|
|
||||||
pub fn new(bytecode: &'a [u8], pc: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
bytecode,
|
|
||||||
pc,
|
|
||||||
inst_start_pc: pc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn from_after_op(bytecode: &'a [u8], inst_start_pc: usize) -> Self {
|
|
||||||
Self {
|
|
||||||
bytecode,
|
|
||||||
pc: inst_start_pc + 1,
|
|
||||||
inst_start_pc,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[cfg_attr(debug_assertions, track_caller)]
|
|
||||||
fn read_array<const N: usize>(&mut self) -> [u8; N] {
|
|
||||||
let ret = self.bytecode[self.pc..self.pc + N]
|
|
||||||
.try_into()
|
|
||||||
.expect("read_array failed");
|
|
||||||
self.pc += N;
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
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];
|
|
||||||
if !likely_stable::likely((0..Op::Illegal as u8).contains(&byte)) {
|
|
||||||
panic!("unknown opcode: {byte:#04x}")
|
|
||||||
}
|
|
||||||
self.pc += 1;
|
|
||||||
unsafe { std::mem::transmute::<u8, Op>(byte) }
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn read_u8(&mut self) -> u8 {
|
|
||||||
let val = self.bytecode[self.pc];
|
|
||||||
self.pc += 1;
|
|
||||||
val
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn read_u16(&mut self) -> u16 {
|
|
||||||
u16::from_le_bytes(self.read_array())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn read_u32(&mut self) -> u32 {
|
|
||||||
u32::from_le_bytes(self.read_array())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn read_i32(&mut self) -> i32 {
|
|
||||||
i32::from_le_bytes(self.read_array())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn read_i64(&mut self) -> i64 {
|
|
||||||
i64::from_le_bytes(self.read_array())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub fn read_f64(&mut self) -> f64 {
|
|
||||||
f64::from_le_bytes(self.read_array())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
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 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));
|
|
||||||
match ty {
|
|
||||||
OperandType::Const => {
|
|
||||||
let id = self.read_u32();
|
|
||||||
OperandData::Const(ctx.get_const(id))
|
|
||||||
}
|
|
||||||
OperandType::BigInt => {
|
|
||||||
let val = self.read_i64();
|
|
||||||
OperandData::BigInt(val)
|
|
||||||
}
|
|
||||||
OperandType::Local => {
|
|
||||||
let layer = self.read_u8();
|
|
||||||
let idx = self.read_u32();
|
|
||||||
OperandData::Local { layer, idx }
|
|
||||||
}
|
|
||||||
OperandType::BuiltinConst => {
|
|
||||||
let id = self.read_string_id();
|
|
||||||
OperandData::BuiltinConst(id)
|
|
||||||
}
|
|
||||||
OperandType::Builtins => OperandData::Builtins,
|
|
||||||
OperandType::ReplBinding => {
|
|
||||||
let id = self.read_string_id();
|
|
||||||
OperandData::ReplBinding(id)
|
|
||||||
}
|
|
||||||
OperandType::ScopedImportBinding => {
|
|
||||||
let slot_id = self.read_u32();
|
|
||||||
let name = self.read_string_id();
|
|
||||||
OperandData::ScopedImportBinding { slot_id, name }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn pc(&self) -> usize {
|
|
||||||
self.pc
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn set_pc(&mut self, pc: usize) {
|
|
||||||
self.pc = pc;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn inst_start_pc(&self) -> usize {
|
|
||||||
self.inst_start_pc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,272 +0,0 @@
|
|||||||
use fix_common::StringId;
|
|
||||||
use gc_arena::{Gc, Mutation};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
AttrSet, Break, BytecodeReader, Closure, List, Machine, NixNum, NixString, NixType, Null,
|
|
||||||
PrimOp, PrimOpApp, Step, StrictValue,
|
|
||||||
};
|
|
||||||
|
|
||||||
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<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base_depth: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step;
|
|
||||||
|
|
||||||
/// 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<M: Machine<'gc>>(m: &mut M) -> Self;
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Forced<'gc> for StrictValue<'gc> {
|
|
||||||
const WIDTH: usize = 1;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn force_and_check<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base_depth: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
m.force_slot_to_pc(base_depth, reader, mc, resume_pc)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
|
|
||||||
m.pop_forced()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_forced_inline {
|
|
||||||
($($ty:ty => $nix_ty:expr),* $(,)?) => {
|
|
||||||
$(
|
|
||||||
impl<'gc> Forced<'gc> for $ty {
|
|
||||||
const WIDTH: usize = 1;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn force_and_check<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base_depth: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
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 = m.finish_type_err($nix_ty, v.ty());
|
|
||||||
return Step::Break(Break::Done);
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
|
|
||||||
m.pop_forced()
|
|
||||||
.as_inline::<$ty>()
|
|
||||||
.expect("type checked in force_and_check")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! impl_forced_gc {
|
|
||||||
($($ty:ty => $nix_ty:expr),* $(,)?) => {
|
|
||||||
$(
|
|
||||||
impl<'gc> Forced<'gc> for Gc<'gc, $ty> {
|
|
||||||
const WIDTH: usize = 1;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn force_and_check<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base_depth: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
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 = m.finish_type_err($nix_ty, v.ty());
|
|
||||||
return Step::Break(Break::Done);
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
|
|
||||||
m.pop_forced()
|
|
||||||
.as_gc::<$ty>()
|
|
||||||
.expect("type checked in force_and_check")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)*
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_forced_inline! {
|
|
||||||
i32 => NixType::Int,
|
|
||||||
bool => NixType::Bool,
|
|
||||||
Null => NixType::Null,
|
|
||||||
StringId => NixType::String,
|
|
||||||
PrimOp => NixType::PrimOp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl_forced_gc! {
|
|
||||||
i64 => NixType::Int,
|
|
||||||
NixString => NixType::String,
|
|
||||||
AttrSet<'gc> => NixType::AttrSet,
|
|
||||||
List<'gc> => NixType::List,
|
|
||||||
Closure<'gc> => NixType::Closure,
|
|
||||||
PrimOpApp<'gc> => NixType::PrimOpApp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Forced<'gc> for NixNum {
|
|
||||||
const WIDTH: usize = 1;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn force_and_check<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base_depth: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
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 = m.finish_type_err(NixType::Int, v.ty());
|
|
||||||
return Step::Break(Break::Done);
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
|
|
||||||
m.pop_forced()
|
|
||||||
.as_num()
|
|
||||||
.expect("type checked in force_and_check")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Forced<'gc> for f64 {
|
|
||||||
const WIDTH: usize = 1;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn force_and_check<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base_depth: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
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 = m.finish_type_err(NixType::Float, v.ty());
|
|
||||||
return Step::Break(Break::Done);
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn pop_converted<M: Machine<'gc>>(m: &mut M) -> Self {
|
|
||||||
m.pop_forced()
|
|
||||||
.as_float()
|
|
||||||
.expect("type checked in force_and_check")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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<M: Machine<'gc>>(
|
|
||||||
m: &mut M,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
base: usize,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
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<M: Machine<'gc>>(m: &mut M) -> Self {
|
|
||||||
let b = B::pop_converted(m);
|
|
||||||
let a = A::pop_converted(m);
|
|
||||||
(a, b)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>> Forced<'gc> for (A, B, C) {
|
|
||||||
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
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(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<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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc, A: Forced<'gc>, B: Forced<'gc>, C: Forced<'gc>, D: Forced<'gc>> Forced<'gc>
|
|
||||||
for (A, B, C, D)
|
|
||||||
{
|
|
||||||
const WIDTH: usize = A::WIDTH + B::WIDTH + C::WIDTH + D::WIDTH;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
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(
|
|
||||||
m,
|
|
||||||
reader,
|
|
||||||
mc,
|
|
||||||
base + B::WIDTH + C::WIDTH + D::WIDTH,
|
|
||||||
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<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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,164 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
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::*;
|
|
||||||
@@ -1,174 +0,0 @@
|
|||||||
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 {}
|
|
||||||
@@ -1,18 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,99 +0,0 @@
|
|||||||
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 },
|
|
||||||
}
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
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()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,728 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::cell::RefCell;
|
|
||||||
use std::fmt;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
use std::mem::size_of;
|
|
||||||
use std::ops::Deref;
|
|
||||||
|
|
||||||
use fix_builtins::BuiltinId;
|
|
||||||
use fix_common::*;
|
|
||||||
use gc_arena::barrier::Unlock;
|
|
||||||
use gc_arena::collect::Trace;
|
|
||||||
use gc_arena::{Collect, Gc, GcRefLock, Mutation, RefLock};
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
use string_interner::Symbol;
|
|
||||||
use string_interner::symbol::SymbolU32;
|
|
||||||
|
|
||||||
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
|
|
||||||
use crate::string_context::StringContext;
|
|
||||||
|
|
||||||
mod private {
|
|
||||||
pub trait Cealed {}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// TAG must be unique among all implementors.
|
|
||||||
#[allow(private_interfaces)]
|
|
||||||
pub unsafe trait Storable: private::Cealed {
|
|
||||||
const TAG: RawTag;
|
|
||||||
}
|
|
||||||
#[allow(private_bounds)]
|
|
||||||
pub trait InlineStorable: Storable + RawStore {}
|
|
||||||
pub trait GcStorable: Storable {}
|
|
||||||
|
|
||||||
macro_rules! define_value_types {
|
|
||||||
(
|
|
||||||
inline { $($itype:ty => $itag:expr, $iname:literal;)* }
|
|
||||||
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
|
|
||||||
) => {
|
|
||||||
$(
|
|
||||||
#[allow(private_interfaces)]
|
|
||||||
unsafe impl Storable for $itype {
|
|
||||||
const TAG: RawTag = $itag;
|
|
||||||
}
|
|
||||||
impl InlineStorable for $itype {}
|
|
||||||
impl private::Cealed for $itype {}
|
|
||||||
)*
|
|
||||||
$(
|
|
||||||
#[allow(private_interfaces)]
|
|
||||||
unsafe impl Storable for $gtype {
|
|
||||||
const TAG: RawTag = $gtag;
|
|
||||||
}
|
|
||||||
impl GcStorable for $gtype {}
|
|
||||||
impl private::Cealed for $gtype {}
|
|
||||||
)*
|
|
||||||
|
|
||||||
const _: () = assert!(size_of::<Value<'static>>() == 8);
|
|
||||||
$(const _: () = assert!(size_of::<$itype>() <= 6);)*
|
|
||||||
|
|
||||||
const _: () = {
|
|
||||||
let tags: &[(bool, u8)] = &[$(RawTag::neg_val($itag)),*, $(RawTag::neg_val($gtag)),*];
|
|
||||||
let mut mask_false: u8 = 0;
|
|
||||||
let mut mask_true: u8 = 0;
|
|
||||||
let mut i = 0;
|
|
||||||
while i < tags.len() {
|
|
||||||
let (neg, val) = tags[i];
|
|
||||||
let bit = 1 << val;
|
|
||||||
if neg {
|
|
||||||
assert!(mask_true & bit == 0, "duplicate true tag id");
|
|
||||||
mask_true |= bit;
|
|
||||||
} else {
|
|
||||||
assert!(mask_false & bit == 0, "duplicate false tag id");
|
|
||||||
mask_false |= bit;
|
|
||||||
}
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
unsafe impl<'gc> Collect<'gc> for Value<'gc> {
|
|
||||||
const NEEDS_TRACE: bool = true;
|
|
||||||
fn trace<T: Trace<'gc>>(&self, cc: &mut T) {
|
|
||||||
let Some(tag) = self.raw.tag() else { return };
|
|
||||||
match tag {
|
|
||||||
$(<$gtype as Storable>::TAG => unsafe {
|
|
||||||
self.load_gc::<$gtype>().trace(cc)
|
|
||||||
},)*
|
|
||||||
$(<$itype as Storable>::TAG => (),)*
|
|
||||||
_ => unreachable!("invalid value tag"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for Value<'_> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
match self.tag() {
|
|
||||||
None => write!(f, "Float({:?})", unsafe {
|
|
||||||
self.raw.float().unwrap_unchecked()
|
|
||||||
}),
|
|
||||||
$(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe {
|
|
||||||
self.as_inline::<$itype>().unwrap_unchecked()
|
|
||||||
}),)*
|
|
||||||
$(Some(<$gtype as Storable>::TAG) =>
|
|
||||||
write!(f, "{}(..)", $gname),)*
|
|
||||||
_ => unreachable!("invalid value tag"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
define_value_types! {
|
|
||||||
inline {
|
|
||||||
i32 => RawTag::P1, "SmallInt";
|
|
||||||
bool => RawTag::P2, "Bool";
|
|
||||||
Null => RawTag::P3, "Null";
|
|
||||||
StringId => RawTag::P4, "SmallString";
|
|
||||||
PrimOp => RawTag::P5, "PrimOp";
|
|
||||||
Path => RawTag::N6, "Path";
|
|
||||||
}
|
|
||||||
gc {
|
|
||||||
i64 => RawTag::P6, "BigInt";
|
|
||||||
NixString => RawTag::P7, "String";
|
|
||||||
AttrSet<'_> => RawTag::N1, "AttrSet";
|
|
||||||
List<'_> => RawTag::N2, "List";
|
|
||||||
Thunk<'_> => RawTag::N3, "Thunk";
|
|
||||||
Closure<'_> => RawTag::N4, "Closure";
|
|
||||||
PrimOpApp<'_> => RawTag::N5, "PrimOpApp";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// # Nix runtime value representation
|
|
||||||
///
|
|
||||||
/// NaN-boxed value fitting in 8 bytes.
|
|
||||||
#[derive(Copy, Clone)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct Value<'gc> {
|
|
||||||
raw: RawBox,
|
|
||||||
_marker: PhantomData<Gc<'gc, ()>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Value<'_> {
|
|
||||||
#[inline(always)]
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new_inline(Null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Value<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
fn from_raw_value(rv: RawValue) -> Self {
|
|
||||||
Self {
|
|
||||||
raw: RawBox::from_value(rv),
|
|
||||||
_marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Load a GC pointer from a value with a negative tag.
|
|
||||||
///
|
|
||||||
/// # Safety
|
|
||||||
///
|
|
||||||
/// The value must actually store a `Gc<'gc, T>` with the matching type.
|
|
||||||
#[inline(always)]
|
|
||||||
unsafe fn load_gc<T: GcStorable>(self) -> Gc<'gc, T> {
|
|
||||||
unsafe {
|
|
||||||
let rv = self.raw.value().unwrap_unchecked();
|
|
||||||
let ptr: *const T = <*const T as RawStore>::from_val(rv);
|
|
||||||
Gc::from_ptr(ptr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
const fn tag(self) -> Option<RawTag> {
|
|
||||||
self.raw.tag()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Value<'gc> {
|
|
||||||
#[inline]
|
|
||||||
pub fn new_float(val: f64) -> Self {
|
|
||||||
Self {
|
|
||||||
raw: RawBox::from_float(val),
|
|
||||||
_marker: PhantomData,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
|
|
||||||
Self::from_raw_value(RawValue::store(T::TAG, val))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
|
|
||||||
let ptr = Gc::as_ptr(gc);
|
|
||||||
Self::from_raw_value(RawValue::store(T::TAG, ptr))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn make_int(val: i64, mc: &Mutation<'gc>) -> Self {
|
|
||||||
if val >= i32::MIN as i64 && val <= i32::MAX as i64 {
|
|
||||||
Value::new_inline(val as i32)
|
|
||||||
} else {
|
|
||||||
Value::new_gc(Gc::new(mc, val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Value<'gc> {
|
|
||||||
#[inline]
|
|
||||||
pub fn is_float(self) -> bool {
|
|
||||||
self.raw.is_float()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn is<T: Storable>(self) -> bool {
|
|
||||||
self.tag() == Some(T::TAG)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Value<'gc> {
|
|
||||||
#[inline]
|
|
||||||
pub fn as_float(self) -> Option<f64> {
|
|
||||||
self.raw.float().copied()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
|
|
||||||
if self.is::<T>() {
|
|
||||||
Some(unsafe {
|
|
||||||
let rv = self.raw.value().unwrap_unchecked();
|
|
||||||
T::from_val(rv)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn as_gc<T: GcStorable>(self) -> Option<Gc<'gc, T>> {
|
|
||||||
if self.is::<T>() {
|
|
||||||
Some(unsafe {
|
|
||||||
let rv = self.raw.value().unwrap_unchecked();
|
|
||||||
let ptr: *const T = <*const T as RawStore>::from_val(rv);
|
|
||||||
Gc::from_ptr(ptr)
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn to_bits(self) -> u64 {
|
|
||||||
self.raw.to_bits()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn as_num(self) -> Option<NixNum> {
|
|
||||||
if let Some(i) = self.as_inline::<i32>() {
|
|
||||||
Some(NixNum::Int(i as i64))
|
|
||||||
} else if let Some(gc_i) = self.as_gc::<i64>() {
|
|
||||||
Some(NixNum::Int(*gc_i))
|
|
||||||
} else {
|
|
||||||
self.as_float().map(NixNum::Float)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn restrict(self) -> Result<StrictValue<'gc>, Gc<'gc, Thunk<'gc>>> {
|
|
||||||
if let Some(thunk) = self.as_gc::<Thunk<'gc>>() {
|
|
||||||
Err(thunk)
|
|
||||||
} else {
|
|
||||||
Ok(StrictValue(self))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn ty(self) -> NixType {
|
|
||||||
if self.is_float() {
|
|
||||||
NixType::Float
|
|
||||||
} else if self.is::<i32>() || self.is::<i64>() {
|
|
||||||
NixType::Int
|
|
||||||
} else if self.is::<bool>() {
|
|
||||||
NixType::Bool
|
|
||||||
} else if self.is::<Null>() {
|
|
||||||
NixType::Null
|
|
||||||
} else if self.is::<StringId>() {
|
|
||||||
NixType::String
|
|
||||||
} else if self.is::<PrimOp>() {
|
|
||||||
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>() {
|
|
||||||
NixType::List
|
|
||||||
} else if self.is::<Thunk>() {
|
|
||||||
NixType::Thunk
|
|
||||||
} else if self.is::<Closure>() {
|
|
||||||
NixType::Closure
|
|
||||||
} else if self.is::<PrimOpApp>() {
|
|
||||||
NixType::PrimOpApp
|
|
||||||
} else {
|
|
||||||
unreachable!("value has no recognized type tag")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn expect_inline<T: InlineStorable>(self) -> Result<T, NixType> {
|
|
||||||
self.as_inline::<T>().ok_or_else(|| self.ty())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn expect_gc<T: GcStorable>(self) -> Result<Gc<'gc, T>, NixType> {
|
|
||||||
self.as_gc::<T>().ok_or_else(|| self.ty())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn expect_num(self) -> Result<NixNum, NixType> {
|
|
||||||
self.as_num().ok_or_else(|| self.ty())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn expect_bool(self) -> Result<bool, NixType> {
|
|
||||||
self.as_inline::<bool>().ok_or_else(|| self.ty())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
pub fn expect_float(self) -> Result<f64, NixType> {
|
|
||||||
self.as_float().ok_or_else(|| self.ty())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Copy, Clone, Default)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct StaticValue(Value<'static>);
|
|
||||||
|
|
||||||
impl<'gc> From<StaticValue> for Value<'gc> {
|
|
||||||
#[inline]
|
|
||||||
fn from(value: StaticValue) -> Self {
|
|
||||||
// SAFETY: StaticValue is guaranteed to not contain any `Gc`.
|
|
||||||
unsafe { std::mem::transmute::<Value<'static>, Value<'gc>>(value.0) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl StaticValue {
|
|
||||||
#[inline]
|
|
||||||
pub fn new_float(val: f64) -> Self {
|
|
||||||
Self(Value::new_float(val))
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn new_inline<T: InlineStorable>(val: T) -> Self {
|
|
||||||
Self(Value::new_inline(val))
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn new_primop(id: BuiltinId, arity: u8, dispatch_ip: u32) -> Self {
|
|
||||||
Self(Value::new_inline(PrimOp {
|
|
||||||
id,
|
|
||||||
arity,
|
|
||||||
dispatch_ip,
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn is_float(self) -> bool {
|
|
||||||
self.0.is_float()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn is<T: InlineStorable>(self) -> bool {
|
|
||||||
self.0.is::<T>()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn as_float(self) -> Option<f64> {
|
|
||||||
self.0.as_float()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn as_inline<T: InlineStorable>(self) -> Option<T> {
|
|
||||||
self.0.as_inline::<T>()
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
pub fn to_bits(self) -> u64 {
|
|
||||||
self.0.raw.to_bits()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug)]
|
|
||||||
pub struct Null;
|
|
||||||
impl RawStore for Null {
|
|
||||||
fn to_val(self, value: &mut RawValue) {
|
|
||||||
value.set_data([0; 6]);
|
|
||||||
}
|
|
||||||
fn from_val(_: &RawValue) -> Self {
|
|
||||||
Self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawStore for StringId {
|
|
||||||
fn to_val(self, value: &mut RawValue) {
|
|
||||||
(self.0.to_usize() as u32).to_val(value);
|
|
||||||
}
|
|
||||||
fn from_val(value: &RawValue) -> Self {
|
|
||||||
Self(
|
|
||||||
SymbolU32::try_from_usize(u32::from_val(value) as usize)
|
|
||||||
.expect("failed to read StringId from Value"),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 struct NixString {
|
|
||||||
data: Box<str>,
|
|
||||||
context: StringContext,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NixString {
|
|
||||||
pub fn new(s: impl Into<Box<str>>) -> Self {
|
|
||||||
Self {
|
|
||||||
data: s.into(),
|
|
||||||
context: StringContext::new(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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 {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt::Debug::fmt(&self.data, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Collect, Debug, Default)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
pub struct AttrSet<'gc> {
|
|
||||||
pub entries: SmallVec<[(StringId, Value<'gc>); 4]>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> AttrSet<'gc> {
|
|
||||||
pub fn from_sorted_unchecked(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
|
|
||||||
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
|
|
||||||
Self { entries }
|
|
||||||
}
|
|
||||||
|
|
||||||
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 fn has(&self, key: StringId) -> bool {
|
|
||||||
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok()
|
|
||||||
}
|
|
||||||
|
|
||||||
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));
|
|
||||||
debug_assert!(other.entries.is_sorted_by_key(|(key, _)| *key));
|
|
||||||
|
|
||||||
let mut entries = SmallVec::new();
|
|
||||||
let mut i = 0;
|
|
||||||
let mut j = 0;
|
|
||||||
while i < self.entries.len() && j < other.entries.len() {
|
|
||||||
match self.entries[i].0.cmp(&other.entries[j].0) {
|
|
||||||
Less => {
|
|
||||||
entries.push(self.entries[i]);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
Greater => {
|
|
||||||
entries.push(other.entries[j]);
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
Equal => {
|
|
||||||
entries.push(other.entries[j]);
|
|
||||||
i += 1;
|
|
||||||
j += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
entries.extend(other.entries[j..].iter().cloned());
|
|
||||||
entries.extend(self.entries[i..].iter().cloned());
|
|
||||||
|
|
||||||
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
|
|
||||||
|
|
||||||
Gc::new(mc, AttrSet { entries })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Collect, Debug, Default)]
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
pub struct List<'gc> {
|
|
||||||
pub inner: RefLock<SmallVec<[Value<'gc>; 4]>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> List<'gc> {
|
|
||||||
pub fn new(mc: &Mutation<'gc>, data: SmallVec<[Value<'gc>; 4]>) -> Gc<'gc, Self> {
|
|
||||||
Gc::new(
|
|
||||||
mc,
|
|
||||||
Self {
|
|
||||||
inner: RefLock::new(data),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_gc(mc: &Mutation<'gc>) -> Gc<'gc, Self> {
|
|
||||||
Gc::new(mc, Self::default())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Unlock for List<'gc> {
|
|
||||||
type Unlocked = RefCell<SmallVec<[Value<'gc>; 4]>>;
|
|
||||||
unsafe fn unlock_unchecked(&self) -> &Self::Unlocked {
|
|
||||||
unsafe { self.inner.unlock_unchecked() }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub type Thunk<'gc> = RefLock<ThunkState<'gc>>;
|
|
||||||
|
|
||||||
#[derive(Collect, Debug)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
pub enum ThunkState<'gc> {
|
|
||||||
Pending { ip: usize, env: GcEnv<'gc> },
|
|
||||||
Apply { func: Value<'gc>, arg: Value<'gc> },
|
|
||||||
Blackhole,
|
|
||||||
Evaluated(StrictValue<'gc>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Collect, Debug)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
pub struct Env<'gc> {
|
|
||||||
pub locals: SmallVec<[Value<'gc>; 4]>,
|
|
||||||
pub prev: Option<GcEnv<'gc>>,
|
|
||||||
}
|
|
||||||
pub type GcEnv<'gc> = GcRefLock<'gc, Env<'gc>>;
|
|
||||||
|
|
||||||
#[derive(Collect, Debug)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
pub struct WithEnv<'gc> {
|
|
||||||
pub env: Value<'gc>,
|
|
||||||
pub prev: Option<GcWithEnv<'gc>>,
|
|
||||||
}
|
|
||||||
pub type GcWithEnv<'gc> = Gc<'gc, WithEnv<'gc>>;
|
|
||||||
|
|
||||||
impl<'gc> Env<'gc> {
|
|
||||||
pub fn empty() -> Self {
|
|
||||||
Env {
|
|
||||||
locals: SmallVec::new(),
|
|
||||||
prev: None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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 {
|
|
||||||
locals,
|
|
||||||
prev: Some(prev),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Collect, Debug)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
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 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 struct PrimOp {
|
|
||||||
pub id: BuiltinId,
|
|
||||||
pub arity: u8,
|
|
||||||
pub dispatch_ip: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl RawStore for PrimOp {
|
|
||||||
fn to_val(self, value: &mut RawValue) {
|
|
||||||
let bytes = self.dispatch_ip.to_le_bytes();
|
|
||||||
value.set_data([
|
|
||||||
self.id as u8,
|
|
||||||
self.arity,
|
|
||||||
bytes[0],
|
|
||||||
bytes[1],
|
|
||||||
bytes[2],
|
|
||||||
bytes[3],
|
|
||||||
]);
|
|
||||||
}
|
|
||||||
fn from_val(value: &RawValue) -> Self {
|
|
||||||
let [id, arity, bytes @ ..] = *value.data();
|
|
||||||
Self {
|
|
||||||
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"),
|
|
||||||
arity,
|
|
||||||
dispatch_ip: u32::from_le_bytes(bytes),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Collect, Debug)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
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 struct StrictValue<'gc>(Value<'gc>);
|
|
||||||
|
|
||||||
impl<'gc> StrictValue<'gc> {
|
|
||||||
#[inline]
|
|
||||||
pub fn relax(self) -> Value<'gc> {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Deref for StrictValue<'gc> {
|
|
||||||
type Target = Value<'gc>;
|
|
||||||
#[inline]
|
|
||||||
fn deref(&self) -> &Value<'gc> {
|
|
||||||
&self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl fmt::Debug for StrictValue<'_> {
|
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
|
||||||
fmt::Debug::fmt(&self.0, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Collect)]
|
|
||||||
#[collect(require_static)]
|
|
||||||
pub enum NixType {
|
|
||||||
Int,
|
|
||||||
Float,
|
|
||||||
Bool,
|
|
||||||
Null,
|
|
||||||
String,
|
|
||||||
Path,
|
|
||||||
AttrSet,
|
|
||||||
List,
|
|
||||||
Thunk,
|
|
||||||
Closure,
|
|
||||||
PrimOp,
|
|
||||||
PrimOpApp,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl NixType {
|
|
||||||
pub fn display(self) -> &'static str {
|
|
||||||
use NixType::*;
|
|
||||||
match self {
|
|
||||||
Int => "an integer",
|
|
||||||
Float => "a float",
|
|
||||||
Bool => "a boolean",
|
|
||||||
Null => "null",
|
|
||||||
String => "a string",
|
|
||||||
Path => "a path",
|
|
||||||
AttrSet => "a set",
|
|
||||||
List => "a list",
|
|
||||||
Thunk => "a thunk",
|
|
||||||
Closure => "a function",
|
|
||||||
PrimOp => "a built-in function",
|
|
||||||
PrimOpApp => "a partially applied built-in function",
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl std::fmt::Display for NixType {
|
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
||||||
write!(f, "{}", self.display())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum NixNum {
|
|
||||||
Int(i64),
|
|
||||||
Float(f64),
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix-builtins"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
num_enum = { workspace = true }
|
|
||||||
gc-arena = { workspace = true }
|
|
||||||
@@ -1,399 +0,0 @@
|
|||||||
use gc_arena::Collect;
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
|
|
||||||
macro_rules! define_builtins {
|
|
||||||
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
|
|
||||||
/// Builtin function registry.
|
|
||||||
/// Array index IS the PrimOp id. (name, arity) pairs.
|
|
||||||
pub const BUILTINS: &[(&str, u8)] = &[
|
|
||||||
$(($name, $arity),)*
|
|
||||||
];
|
|
||||||
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, TryFromPrimitive, Collect)]
|
|
||||||
#[repr(u8)]
|
|
||||||
#[collect(require_static)]
|
|
||||||
pub enum BuiltinId {
|
|
||||||
$($variant,)*
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
define_builtins! {
|
|
||||||
("abort", Abort, 1),
|
|
||||||
("__add", Add, 2),
|
|
||||||
("__addErrorContext", AddErrorContext, 2),
|
|
||||||
("__all", All, 2),
|
|
||||||
("__any", Any, 2),
|
|
||||||
("__appendContext", AppendContext, 2),
|
|
||||||
("__attrNames", AttrNames, 1),
|
|
||||||
("__attrValues", AttrValues, 1),
|
|
||||||
("baseNameOf", BaseNameOf, 1),
|
|
||||||
("__bitAnd", BitAnd, 2),
|
|
||||||
("__bitOr", BitOr, 2),
|
|
||||||
("__bitXor", BitXor, 2),
|
|
||||||
("break", Break, 1),
|
|
||||||
("__catAttrs", CatAttrs, 2),
|
|
||||||
("__ceil", Ceil, 1),
|
|
||||||
("__compareVersions", CompareVersions, 2),
|
|
||||||
("__concatLists", ConcatLists, 1),
|
|
||||||
("__concatMap", ConcatMap, 2),
|
|
||||||
("__concatStringsSep", ConcatStringsSep, 2),
|
|
||||||
("__convertHash", ConvertHash, 1),
|
|
||||||
("__deepSeq", DeepSeq, 2),
|
|
||||||
("derivation", Derivation, 1),
|
|
||||||
("derivationStrict", DerivationStrict, 1),
|
|
||||||
("dirOf", DirOf, 1),
|
|
||||||
("__div", Div, 2),
|
|
||||||
("__elem", Elem, 2),
|
|
||||||
("__elemAt", ElemAt, 2),
|
|
||||||
("fetchGit", FetchGit, 1),
|
|
||||||
("fetchMercurial", FetchMercurial, 1),
|
|
||||||
("fetchTarball", FetchTarball, 1),
|
|
||||||
("fetchTree", FetchTree, 1),
|
|
||||||
("__fetchurl", FetchUrl, 1),
|
|
||||||
("__filter", Filter, 2),
|
|
||||||
("__filterSource", FilterSource, 2),
|
|
||||||
("__findFile", FindFile, 2),
|
|
||||||
("__floor", Floor, 1),
|
|
||||||
("__foldl'", FoldlStrict, 3),
|
|
||||||
("__fromJSON", FromJSON, 1),
|
|
||||||
("fromTOML", FromTOML, 1),
|
|
||||||
("__functionArgs", FunctionArgs, 1),
|
|
||||||
("__genList", GenList, 2),
|
|
||||||
("__genericClosure", GenericClosure, 1),
|
|
||||||
("__getAttr", GetAttr, 2),
|
|
||||||
("__getContext", GetContext, 1),
|
|
||||||
("__getEnv", GetEnv, 1),
|
|
||||||
("__groupBy", GroupBy, 2),
|
|
||||||
("__hasAttr", HasAttr, 2),
|
|
||||||
("__hasContext", HasContext, 1),
|
|
||||||
("__hashFile", HashFile, 2),
|
|
||||||
("__hashString", HashString, 2),
|
|
||||||
("__head", Head, 1),
|
|
||||||
("import", Import, 1),
|
|
||||||
("__intersectAttrs", IntersectAttrs, 2),
|
|
||||||
("__isAttrs", IsAttrs, 1),
|
|
||||||
("__isBool", IsBool, 1),
|
|
||||||
("__isFloat", IsFloat, 1),
|
|
||||||
("__isFunction", IsFunction, 1),
|
|
||||||
("__isInt", IsInt, 1),
|
|
||||||
("__isList", IsList, 1),
|
|
||||||
("isNull", IsNull, 1),
|
|
||||||
("__isPath", IsPath, 1),
|
|
||||||
("__isString", IsString, 1),
|
|
||||||
("__length", Length, 1),
|
|
||||||
("__lessThan", LessThan, 2),
|
|
||||||
("__listToAttrs", ListToAttrs, 1),
|
|
||||||
("map", Map, 2),
|
|
||||||
("__mapAttrs", MapAttrs, 2),
|
|
||||||
("__match", Match, 2),
|
|
||||||
("__mul", Mul, 2),
|
|
||||||
("__parseDrvName", ParseDrvName, 1),
|
|
||||||
("__partition", Partition, 2),
|
|
||||||
("__path", Path, 1),
|
|
||||||
("__pathExists", PathExists, 1),
|
|
||||||
("placeholder", Placeholder, 1),
|
|
||||||
("__readDir", ReadDir, 1),
|
|
||||||
("__readFile", ReadFile, 1),
|
|
||||||
("__readFileType", ReadFileType, 1),
|
|
||||||
("removeAttrs", RemoveAttrs, 2),
|
|
||||||
("__replaceStrings", ReplaceStrings, 3),
|
|
||||||
("scopedImport", ScopedImport, 2),
|
|
||||||
("__seq", Seq, 2),
|
|
||||||
("__sort", Sort, 2),
|
|
||||||
("__split", Split, 2),
|
|
||||||
("__splitVersion", SplitVersion, 1),
|
|
||||||
("__storePath", StorePath, 1),
|
|
||||||
("__stringLength", StringLength, 1),
|
|
||||||
("__sub", Sub, 2),
|
|
||||||
("__substring", Substring, 3),
|
|
||||||
("__tail", Tail, 1),
|
|
||||||
("throw", Throw, 1),
|
|
||||||
("__toFile", ToFile, 2),
|
|
||||||
("__toJSON", ToJSON, 1),
|
|
||||||
("__toPath", ToPath, 1),
|
|
||||||
("toString", ToString, 1),
|
|
||||||
("__toXML", ToXML, 1),
|
|
||||||
("__trace", Trace, 2),
|
|
||||||
("__tryEval", TryEval, 1),
|
|
||||||
("__typeOf", TypeOf, 1),
|
|
||||||
("__unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
|
|
||||||
("__unsafeDiscardOutputDependency", UnsafeDiscardOutputDependency, 1),
|
|
||||||
("__unsafeGetAttrPos", UnsafeGetAttrPos, 2),
|
|
||||||
("__warn", Warn, 2),
|
|
||||||
("__zipAttrsWith", ZipAttrsWith, 2),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
|
|
||||||
pub enum PrimOpPhase {
|
|
||||||
Abort,
|
|
||||||
Add,
|
|
||||||
AddErrorContext,
|
|
||||||
All,
|
|
||||||
Any,
|
|
||||||
AppendContext,
|
|
||||||
AttrNames,
|
|
||||||
AttrValues,
|
|
||||||
BaseNameOf,
|
|
||||||
BitAnd,
|
|
||||||
BitOr,
|
|
||||||
BitXor,
|
|
||||||
Break,
|
|
||||||
CatAttrs,
|
|
||||||
Ceil,
|
|
||||||
CompareVersions,
|
|
||||||
ConcatLists,
|
|
||||||
ConcatMap,
|
|
||||||
ConcatStringsSep,
|
|
||||||
ConvertHash,
|
|
||||||
|
|
||||||
DeepSeq,
|
|
||||||
DeepSeqPush,
|
|
||||||
DeepSeqLoop,
|
|
||||||
|
|
||||||
Derivation,
|
|
||||||
DerivationStrict,
|
|
||||||
DirOf,
|
|
||||||
Div,
|
|
||||||
Elem,
|
|
||||||
ElemAt,
|
|
||||||
FetchGit,
|
|
||||||
FetchMercurial,
|
|
||||||
FetchTarball,
|
|
||||||
FetchTree,
|
|
||||||
FetchUrl,
|
|
||||||
|
|
||||||
FilterForceList,
|
|
||||||
FilterCallPred,
|
|
||||||
FilterCheck,
|
|
||||||
|
|
||||||
FilterSource,
|
|
||||||
FindFile,
|
|
||||||
Floor,
|
|
||||||
FoldlStrict,
|
|
||||||
FoldlStrictEmpty,
|
|
||||||
FoldlStrictCall1,
|
|
||||||
FoldlStrictCall2,
|
|
||||||
FoldlStrictUpdate,
|
|
||||||
FromJSON,
|
|
||||||
FromTOML,
|
|
||||||
FunctionArgs,
|
|
||||||
GenList,
|
|
||||||
GenericClosure,
|
|
||||||
GetAttr,
|
|
||||||
GetContext,
|
|
||||||
GetEnv,
|
|
||||||
GroupBy,
|
|
||||||
HasAttr,
|
|
||||||
HasContext,
|
|
||||||
HashFile,
|
|
||||||
HashString,
|
|
||||||
Head,
|
|
||||||
Import,
|
|
||||||
IntersectAttrs,
|
|
||||||
IsAttrs,
|
|
||||||
IsBool,
|
|
||||||
IsFloat,
|
|
||||||
IsFunction,
|
|
||||||
IsInt,
|
|
||||||
IsList,
|
|
||||||
IsNull,
|
|
||||||
IsPath,
|
|
||||||
IsString,
|
|
||||||
Length,
|
|
||||||
LessThan,
|
|
||||||
ListToAttrs,
|
|
||||||
Map,
|
|
||||||
MapAttrs,
|
|
||||||
Match,
|
|
||||||
Mul,
|
|
||||||
ParseDrvName,
|
|
||||||
Partition,
|
|
||||||
Path,
|
|
||||||
PathExists,
|
|
||||||
Placeholder,
|
|
||||||
ReadDir,
|
|
||||||
ReadFile,
|
|
||||||
ReadFileType,
|
|
||||||
RemoveAttrs,
|
|
||||||
ReplaceStrings,
|
|
||||||
ScopedImport,
|
|
||||||
Seq,
|
|
||||||
Sort,
|
|
||||||
Split,
|
|
||||||
SplitVersion,
|
|
||||||
StorePath,
|
|
||||||
StringLength,
|
|
||||||
Sub,
|
|
||||||
Substring,
|
|
||||||
Tail,
|
|
||||||
Throw,
|
|
||||||
ToFile,
|
|
||||||
ToJSON,
|
|
||||||
ToPath,
|
|
||||||
ToString,
|
|
||||||
ToXML,
|
|
||||||
Trace,
|
|
||||||
TryEval,
|
|
||||||
TypeOf,
|
|
||||||
UnsafeDiscardStringContext,
|
|
||||||
UnsafeGetAttrPos,
|
|
||||||
Warn,
|
|
||||||
ZipAttrsWith,
|
|
||||||
|
|
||||||
ForceResultShallow,
|
|
||||||
ForceResultShallowPush,
|
|
||||||
ForceResultShallowLoop,
|
|
||||||
ForceResultDeepFinish,
|
|
||||||
|
|
||||||
EqStep,
|
|
||||||
EqForce,
|
|
||||||
|
|
||||||
// TODO: split into separate enums
|
|
||||||
CallPattern,
|
|
||||||
CallFunctor1,
|
|
||||||
CallFunctor2,
|
|
||||||
|
|
||||||
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 {
|
|
||||||
use BuiltinId::*;
|
|
||||||
match self {
|
|
||||||
Abort => PrimOpPhase::Abort,
|
|
||||||
Add => PrimOpPhase::Add,
|
|
||||||
AddErrorContext => PrimOpPhase::AddErrorContext,
|
|
||||||
All => PrimOpPhase::All,
|
|
||||||
Any => PrimOpPhase::Any,
|
|
||||||
AppendContext => PrimOpPhase::AppendContext,
|
|
||||||
AttrNames => PrimOpPhase::AttrNames,
|
|
||||||
AttrValues => PrimOpPhase::AttrValues,
|
|
||||||
BaseNameOf => PrimOpPhase::BaseNameOf,
|
|
||||||
BitAnd => PrimOpPhase::BitAnd,
|
|
||||||
BitOr => PrimOpPhase::BitOr,
|
|
||||||
BitXor => PrimOpPhase::BitXor,
|
|
||||||
Break => PrimOpPhase::Break,
|
|
||||||
CatAttrs => PrimOpPhase::CatAttrs,
|
|
||||||
Ceil => PrimOpPhase::Ceil,
|
|
||||||
CompareVersions => PrimOpPhase::CompareVersions,
|
|
||||||
ConcatLists => PrimOpPhase::ConcatLists,
|
|
||||||
ConcatMap => PrimOpPhase::ConcatMap,
|
|
||||||
ConcatStringsSep => PrimOpPhase::ConcatStringsSep,
|
|
||||||
ConvertHash => PrimOpPhase::ConvertHash,
|
|
||||||
DeepSeq => PrimOpPhase::DeepSeq,
|
|
||||||
Derivation => PrimOpPhase::Derivation,
|
|
||||||
DerivationStrict => PrimOpPhase::DerivationStrict,
|
|
||||||
DirOf => PrimOpPhase::DirOf,
|
|
||||||
Div => PrimOpPhase::Div,
|
|
||||||
Elem => PrimOpPhase::Elem,
|
|
||||||
ElemAt => PrimOpPhase::ElemAt,
|
|
||||||
FetchGit => PrimOpPhase::FetchGit,
|
|
||||||
FetchMercurial => PrimOpPhase::FetchMercurial,
|
|
||||||
FetchTarball => PrimOpPhase::FetchTarball,
|
|
||||||
FetchTree => PrimOpPhase::FetchTree,
|
|
||||||
FetchUrl => PrimOpPhase::FetchUrl,
|
|
||||||
Filter => PrimOpPhase::FilterForceList,
|
|
||||||
FilterSource => PrimOpPhase::FilterSource,
|
|
||||||
FindFile => PrimOpPhase::FindFile,
|
|
||||||
Floor => PrimOpPhase::Floor,
|
|
||||||
FoldlStrict => PrimOpPhase::FoldlStrict,
|
|
||||||
FromJSON => PrimOpPhase::FromJSON,
|
|
||||||
FromTOML => PrimOpPhase::FromTOML,
|
|
||||||
FunctionArgs => PrimOpPhase::FunctionArgs,
|
|
||||||
GenList => PrimOpPhase::GenList,
|
|
||||||
GenericClosure => PrimOpPhase::GenericClosure,
|
|
||||||
GetAttr => PrimOpPhase::GetAttr,
|
|
||||||
GetContext => PrimOpPhase::GetContext,
|
|
||||||
GetEnv => PrimOpPhase::GetEnv,
|
|
||||||
GroupBy => PrimOpPhase::GroupBy,
|
|
||||||
HasAttr => PrimOpPhase::HasAttr,
|
|
||||||
HasContext => PrimOpPhase::HasContext,
|
|
||||||
HashFile => PrimOpPhase::HashFile,
|
|
||||||
HashString => PrimOpPhase::HashString,
|
|
||||||
Head => PrimOpPhase::Head,
|
|
||||||
Import => PrimOpPhase::Import,
|
|
||||||
IntersectAttrs => PrimOpPhase::IntersectAttrs,
|
|
||||||
IsAttrs => PrimOpPhase::IsAttrs,
|
|
||||||
IsBool => PrimOpPhase::IsBool,
|
|
||||||
IsFloat => PrimOpPhase::IsFloat,
|
|
||||||
IsFunction => PrimOpPhase::IsFunction,
|
|
||||||
IsInt => PrimOpPhase::IsInt,
|
|
||||||
IsList => PrimOpPhase::IsList,
|
|
||||||
IsNull => PrimOpPhase::IsNull,
|
|
||||||
IsPath => PrimOpPhase::IsPath,
|
|
||||||
IsString => PrimOpPhase::IsString,
|
|
||||||
Length => PrimOpPhase::Length,
|
|
||||||
LessThan => PrimOpPhase::LessThan,
|
|
||||||
ListToAttrs => PrimOpPhase::ListToAttrs,
|
|
||||||
Map => PrimOpPhase::Map,
|
|
||||||
MapAttrs => PrimOpPhase::MapAttrs,
|
|
||||||
Match => PrimOpPhase::Match,
|
|
||||||
Mul => PrimOpPhase::Mul,
|
|
||||||
ParseDrvName => PrimOpPhase::ParseDrvName,
|
|
||||||
Partition => PrimOpPhase::Partition,
|
|
||||||
Path => PrimOpPhase::Path,
|
|
||||||
PathExists => PrimOpPhase::PathExists,
|
|
||||||
Placeholder => PrimOpPhase::Placeholder,
|
|
||||||
ReadDir => PrimOpPhase::ReadDir,
|
|
||||||
ReadFile => PrimOpPhase::ReadFile,
|
|
||||||
ReadFileType => PrimOpPhase::ReadFileType,
|
|
||||||
RemoveAttrs => PrimOpPhase::RemoveAttrs,
|
|
||||||
ReplaceStrings => PrimOpPhase::ReplaceStrings,
|
|
||||||
ScopedImport => PrimOpPhase::ScopedImport,
|
|
||||||
Seq => PrimOpPhase::Seq,
|
|
||||||
Sort => PrimOpPhase::Sort,
|
|
||||||
Split => PrimOpPhase::Split,
|
|
||||||
SplitVersion => PrimOpPhase::SplitVersion,
|
|
||||||
StorePath => PrimOpPhase::StorePath,
|
|
||||||
StringLength => PrimOpPhase::StringLength,
|
|
||||||
Sub => PrimOpPhase::Sub,
|
|
||||||
Substring => PrimOpPhase::Substring,
|
|
||||||
Tail => PrimOpPhase::Tail,
|
|
||||||
Throw => PrimOpPhase::Throw,
|
|
||||||
ToFile => PrimOpPhase::ToFile,
|
|
||||||
ToJSON => PrimOpPhase::ToJSON,
|
|
||||||
ToPath => PrimOpPhase::ToPath,
|
|
||||||
ToString => PrimOpPhase::ToString,
|
|
||||||
ToXML => PrimOpPhase::ToXML,
|
|
||||||
Trace => PrimOpPhase::Trace,
|
|
||||||
TryEval => PrimOpPhase::TryEval,
|
|
||||||
TypeOf => PrimOpPhase::TypeOf,
|
|
||||||
UnsafeDiscardStringContext => PrimOpPhase::UnsafeDiscardStringContext,
|
|
||||||
UnsafeDiscardOutputDependency => PrimOpPhase::UnsafeDiscardOutputDependency,
|
|
||||||
UnsafeGetAttrPos => PrimOpPhase::UnsafeGetAttrPos,
|
|
||||||
Warn => PrimOpPhase::Warn,
|
|
||||||
ZipAttrsWith => PrimOpPhase::ZipAttrsWith,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrimOpPhase {
|
|
||||||
pub fn ip(self) -> u32 {
|
|
||||||
self as u32 * 2
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix-codegen"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
hashbrown = { workspace = true }
|
|
||||||
num_enum = { workspace = true }
|
|
||||||
rnix = { workspace = true }
|
|
||||||
string-interner = { workspace = true }
|
|
||||||
colored = "3.1.1"
|
|
||||||
|
|
||||||
fix-builtins = { path = "../fix-builtins" }
|
|
||||||
fix-common = { path = "../fix-common" }
|
|
||||||
fix-ir = { path = "../fix-ir" }
|
|
||||||
@@ -1,467 +0,0 @@
|
|||||||
use std::fmt::Write;
|
|
||||||
|
|
||||||
use colored::Colorize;
|
|
||||||
use fix_builtins::BuiltinId;
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
|
|
||||||
use crate::{InstructionPtr, Op, OperandType};
|
|
||||||
|
|
||||||
pub trait DisassemblerContext {
|
|
||||||
fn resolve_string(&self, id: u32) -> &str;
|
|
||||||
fn get_code(&self) -> &[u8];
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Disassembler<'a, Ctx> {
|
|
||||||
code: &'a [u8],
|
|
||||||
ctx: &'a Ctx,
|
|
||||||
pc: usize,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
|
|
||||||
pub fn new(ip: InstructionPtr, ctx: &'a Ctx) -> Self {
|
|
||||||
Self {
|
|
||||||
code: ctx.get_code(),
|
|
||||||
ctx,
|
|
||||||
pc: ip.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_u8(&mut self) -> u8 {
|
|
||||||
let b = self.code[self.pc];
|
|
||||||
self.pc += 1;
|
|
||||||
b
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_u16(&mut self) -> u16 {
|
|
||||||
let bytes = self.code[self.pc..self.pc + 2]
|
|
||||||
.try_into()
|
|
||||||
.expect("no enough bytes");
|
|
||||||
self.pc += 2;
|
|
||||||
u16::from_le_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_u32(&mut self) -> u32 {
|
|
||||||
let bytes = self.code[self.pc..self.pc + 4]
|
|
||||||
.try_into()
|
|
||||||
.expect("no enough bytes");
|
|
||||||
self.pc += 4;
|
|
||||||
u32::from_le_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_i32(&mut self) -> i32 {
|
|
||||||
let bytes = self.code[self.pc..self.pc + 4]
|
|
||||||
.try_into()
|
|
||||||
.expect("no enough bytes");
|
|
||||||
self.pc += 4;
|
|
||||||
i32::from_le_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_i64(&mut self) -> i64 {
|
|
||||||
let bytes = self.code[self.pc..self.pc + 8]
|
|
||||||
.try_into()
|
|
||||||
.expect("no enough bytes");
|
|
||||||
self.pc += 8;
|
|
||||||
i64::from_le_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_f64(&mut self) -> f64 {
|
|
||||||
let bytes = self.code[self.pc..self.pc + 8]
|
|
||||||
.try_into()
|
|
||||||
.expect("no enough bytes");
|
|
||||||
self.pc += 8;
|
|
||||||
f64::from_le_bytes(bytes)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn read_operand_data(&mut self) {
|
|
||||||
use OperandType::*;
|
|
||||||
let tag = self.read_u8();
|
|
||||||
let ty = OperandType::try_from_primitive(tag).expect("invalid operand type");
|
|
||||||
match ty {
|
|
||||||
Const => {
|
|
||||||
self.read_u32();
|
|
||||||
}
|
|
||||||
BigInt => {
|
|
||||||
self.read_i64();
|
|
||||||
}
|
|
||||||
Local => {
|
|
||||||
self.read_u8();
|
|
||||||
self.read_u32();
|
|
||||||
}
|
|
||||||
BuiltinConst => {
|
|
||||||
self.read_u32();
|
|
||||||
}
|
|
||||||
Builtins => {}
|
|
||||||
ReplBinding => {
|
|
||||||
self.read_u32();
|
|
||||||
}
|
|
||||||
ScopedImportBinding => {
|
|
||||||
self.read_u32();
|
|
||||||
self.read_u32();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disassemble(&mut self) -> String {
|
|
||||||
self.disassemble_impl(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disassemble_colored(&mut self) -> String {
|
|
||||||
self.disassemble_impl(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn disassemble_impl(&mut self, color: bool) -> String {
|
|
||||||
let mut out = String::new();
|
|
||||||
if color {
|
|
||||||
let _ = writeln!(out, "{}", "=== Bytecode Disassembly ===".bold().white());
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
"{} {}",
|
|
||||||
"Length:".white(),
|
|
||||||
format!("{} bytes", self.code.len()).cyan()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let _ = writeln!(out, "=== Bytecode Disassembly ===");
|
|
||||||
let _ = writeln!(out, "Length: {} bytes", self.code.len());
|
|
||||||
}
|
|
||||||
|
|
||||||
while self.pc < self.code.len() {
|
|
||||||
let start_pos = self.pc;
|
|
||||||
let op_byte = self.read_u8();
|
|
||||||
let (mnemonic, args) = self.decode_instruction(op_byte, start_pos);
|
|
||||||
|
|
||||||
let bytes_slice = &self.code[start_pos + 1..self.pc];
|
|
||||||
let mut chunks = bytes_slice.chunks(4);
|
|
||||||
|
|
||||||
let first_chunk = chunks.next().unwrap_or(&[]);
|
|
||||||
let bytes_str = {
|
|
||||||
let mut temp = format!("{:02x}", self.code[start_pos]);
|
|
||||||
for b in first_chunk {
|
|
||||||
let _ = write!(&mut temp, " {:02x}", b);
|
|
||||||
}
|
|
||||||
temp
|
|
||||||
};
|
|
||||||
|
|
||||||
if color {
|
|
||||||
let sep = if args.is_empty() { "" } else { " " };
|
|
||||||
let _ = writeln!(
|
|
||||||
out,
|
|
||||||
"{} {:<14} | {}{}{}",
|
|
||||||
format!("{:04x}", start_pos).dimmed(),
|
|
||||||
bytes_str.green(),
|
|
||||||
mnemonic.yellow().bold(),
|
|
||||||
sep,
|
|
||||||
args.cyan()
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
let op_str = if args.is_empty() {
|
|
||||||
mnemonic.to_string()
|
|
||||||
} else {
|
|
||||||
format!("{} {}", mnemonic, args)
|
|
||||||
};
|
|
||||||
let _ = writeln!(out, "{:04x} {:<14} | {}", start_pos, bytes_str, op_str);
|
|
||||||
}
|
|
||||||
|
|
||||||
for chunk in chunks {
|
|
||||||
let bytes_str = {
|
|
||||||
let mut temp = String::from(" ");
|
|
||||||
for b in chunk {
|
|
||||||
let _ = write!(&mut temp, " {:02x}", b);
|
|
||||||
}
|
|
||||||
temp
|
|
||||||
};
|
|
||||||
|
|
||||||
let extra_width = if start_pos > 0 {
|
|
||||||
start_pos.ilog2() >> 4
|
|
||||||
} else {
|
|
||||||
0
|
|
||||||
};
|
|
||||||
|
|
||||||
if color {
|
|
||||||
let _ = write!(out, " ");
|
|
||||||
for _ in 0..extra_width {
|
|
||||||
let _ = write!(out, " ");
|
|
||||||
}
|
|
||||||
let _ = writeln!(out, " {:<14} |", bytes_str.green());
|
|
||||||
} else {
|
|
||||||
let _ = write!(out, " ");
|
|
||||||
for _ in 0..extra_width {
|
|
||||||
let _ = write!(out, " ");
|
|
||||||
}
|
|
||||||
let _ = writeln!(out, " {:<14} |", bytes_str);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
out
|
|
||||||
}
|
|
||||||
|
|
||||||
fn decode_instruction(&mut self, op_byte: u8, current_pc: usize) -> (&'static str, String) {
|
|
||||||
let op = Op::try_from_primitive(op_byte).expect("invalid op code");
|
|
||||||
|
|
||||||
match op {
|
|
||||||
Op::PushSmi => {
|
|
||||||
let val = self.read_i32();
|
|
||||||
("PushSmi", format!("{}", val))
|
|
||||||
}
|
|
||||||
Op::PushBigInt => {
|
|
||||||
let val = self.read_i64();
|
|
||||||
("PushBigInt", format!("{}", val))
|
|
||||||
}
|
|
||||||
Op::PushFloat => {
|
|
||||||
let val = self.read_f64();
|
|
||||||
("PushFloat", format!("{}", val))
|
|
||||||
}
|
|
||||||
Op::PushString => {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
let s = self.ctx.resolve_string(idx);
|
|
||||||
let len = s.len();
|
|
||||||
let mut s_fmt = format!("{:?}", s);
|
|
||||||
if s_fmt.len() > 60 {
|
|
||||||
s_fmt.truncate(57);
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
write!(s_fmt, "...\" (total {len} bytes)").unwrap();
|
|
||||||
}
|
|
||||||
("PushString", format!("@{} {}", idx, s_fmt))
|
|
||||||
}
|
|
||||||
Op::PushNull => ("PushNull", String::new()),
|
|
||||||
Op::PushTrue => ("PushTrue", String::new()),
|
|
||||||
Op::PushFalse => ("PushFalse", String::new()),
|
|
||||||
|
|
||||||
Op::LoadLocal => {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
("LoadLocal", format!("[{}]", idx))
|
|
||||||
}
|
|
||||||
Op::LoadOuter => {
|
|
||||||
let depth = self.read_u8();
|
|
||||||
let idx = self.read_u32();
|
|
||||||
("LoadOuter", format!("depth={} [{}]", depth, idx))
|
|
||||||
}
|
|
||||||
Op::StoreLocal => {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
("StoreLocal", format!("[{}]", idx))
|
|
||||||
}
|
|
||||||
Op::AllocLocals => {
|
|
||||||
let count = self.read_u32();
|
|
||||||
("AllocLocals", format!("count={}", count))
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::MakeThunk => {
|
|
||||||
let offset = self.read_u32();
|
|
||||||
("MakeThunk", format!("-> {:04x}", offset))
|
|
||||||
}
|
|
||||||
Op::MakeClosure => {
|
|
||||||
let offset = self.read_u32();
|
|
||||||
let slots = self.read_u32();
|
|
||||||
("MakeClosure", format!("-> {:04x} slots={}", offset, slots))
|
|
||||||
}
|
|
||||||
Op::MakePatternClosure => {
|
|
||||||
let offset = self.read_u32();
|
|
||||||
let slots = self.read_u32();
|
|
||||||
let req_count = self.read_u16();
|
|
||||||
let opt_count = self.read_u16();
|
|
||||||
let ellipsis = self.read_u8() != 0;
|
|
||||||
|
|
||||||
let mut arg_str = format!(
|
|
||||||
"-> {:04x} slots={} req={} opt={} ...={})",
|
|
||||||
offset, slots, req_count, opt_count, ellipsis
|
|
||||||
);
|
|
||||||
|
|
||||||
arg_str.push_str(" Args=[");
|
|
||||||
for _ in 0..req_count {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
arg_str.push_str(&format!("Req({}) ", self.ctx.resolve_string(idx)));
|
|
||||||
}
|
|
||||||
for _ in 0..opt_count {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
arg_str.push_str(&format!("Opt({}) ", self.ctx.resolve_string(idx)));
|
|
||||||
}
|
|
||||||
|
|
||||||
let total_args = req_count + opt_count;
|
|
||||||
for _ in 0..total_args {
|
|
||||||
let _name_idx = self.read_u32();
|
|
||||||
let _span_id = self.read_u32();
|
|
||||||
}
|
|
||||||
arg_str.push(']');
|
|
||||||
|
|
||||||
("MakePatternClosure", arg_str)
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::Call => {
|
|
||||||
self.read_operand_data();
|
|
||||||
("Call", "arg=?".into())
|
|
||||||
}
|
|
||||||
Op::DispatchPrimOp => {
|
|
||||||
let id = BuiltinId::try_from_primitive(self.read_u8()).expect("invalid builtin id");
|
|
||||||
("DispatchPrimOp", format!("id={id:?}"))
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::MakeAttrs => {
|
|
||||||
let static_count = self.read_u32();
|
|
||||||
let dynamic_count = self.read_u32();
|
|
||||||
let mut args = format!("static={} dynamic={}", static_count, dynamic_count);
|
|
||||||
|
|
||||||
for _ in 0..static_count {
|
|
||||||
let key_id = self.read_u32();
|
|
||||||
let _ = write!(args, " [{}={}", self.ctx.resolve_string(key_id), key_id);
|
|
||||||
self.read_operand_data();
|
|
||||||
let _span_id = self.read_u32();
|
|
||||||
args.push(']');
|
|
||||||
}
|
|
||||||
|
|
||||||
for _ in 0..dynamic_count {
|
|
||||||
let _ = write!(args, " [dyn");
|
|
||||||
self.read_operand_data();
|
|
||||||
let _span_id = self.read_u32();
|
|
||||||
args.push(']');
|
|
||||||
}
|
|
||||||
|
|
||||||
("MakeAttrs", args)
|
|
||||||
}
|
|
||||||
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
|
|
||||||
|
|
||||||
Op::SelectStatic => {
|
|
||||||
let span_id = self.read_u32();
|
|
||||||
let key_id = self.read_u32();
|
|
||||||
(
|
|
||||||
"SelectStatic",
|
|
||||||
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Op::SelectDynamic => {
|
|
||||||
let span_id = self.read_u32();
|
|
||||||
("SelectDynamic", format!("span={}", span_id))
|
|
||||||
}
|
|
||||||
Op::HasAttrPathStatic => {
|
|
||||||
let span_id = self.read_u32();
|
|
||||||
let key_id = self.read_u32();
|
|
||||||
(
|
|
||||||
"HasAttrPathStatic",
|
|
||||||
format!("key={} span={}", self.ctx.resolve_string(key_id), span_id),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Op::HasAttrPathDynamic => {
|
|
||||||
let span_id = self.read_u32();
|
|
||||||
("HasAttrPathDynamic", format!("span={}", span_id))
|
|
||||||
}
|
|
||||||
Op::HasAttrStatic => {
|
|
||||||
let key_id = self.read_u32();
|
|
||||||
(
|
|
||||||
"HasAttrStatic",
|
|
||||||
format!("key={}", self.ctx.resolve_string(key_id)),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Op::HasAttrDynamic => ("HasAttrDynamic", String::new()),
|
|
||||||
Op::HasAttrResolve => ("HasAttrResolve", String::new()),
|
|
||||||
Op::JumpIfSelectSucceeded => {
|
|
||||||
let offset = self.read_i32();
|
|
||||||
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
|
|
||||||
(
|
|
||||||
"JumpIfSelectSucceeded",
|
|
||||||
format!("-> {:04x} offset={}", target, offset),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Op::JumpIfSelectFailed => {
|
|
||||||
let offset = self.read_i32();
|
|
||||||
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
|
|
||||||
(
|
|
||||||
"JumpIfSelectFailed",
|
|
||||||
format!("-> {:04x} offset={}", target, offset),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::MakeList => {
|
|
||||||
let count = self.read_u32();
|
|
||||||
for _ in 0..count {
|
|
||||||
self.read_operand_data();
|
|
||||||
}
|
|
||||||
("MakeList", format!("size={}", count))
|
|
||||||
}
|
|
||||||
Op::MakeEmptyList => ("MakeEmptyList", String::new()),
|
|
||||||
|
|
||||||
Op::OpAdd => ("OpAdd", String::new()),
|
|
||||||
Op::OpSub => ("OpSub", String::new()),
|
|
||||||
Op::OpMul => ("OpMul", String::new()),
|
|
||||||
Op::OpDiv => ("OpDiv", String::new()),
|
|
||||||
Op::OpEq => ("OpEq", String::new()),
|
|
||||||
Op::OpNeq => ("OpNeq", String::new()),
|
|
||||||
Op::OpLt => ("OpLt", String::new()),
|
|
||||||
Op::OpGt => ("OpGt", String::new()),
|
|
||||||
Op::OpLeq => ("OpLeq", String::new()),
|
|
||||||
Op::OpGeq => ("OpGeq", String::new()),
|
|
||||||
Op::OpConcat => ("OpConcat", String::new()),
|
|
||||||
Op::OpUpdate => ("OpUpdate", String::new()),
|
|
||||||
Op::OpNeg => ("OpNeg", String::new()),
|
|
||||||
Op::OpNot => ("OpNot", String::new()),
|
|
||||||
|
|
||||||
Op::JumpIfFalse => {
|
|
||||||
let offset = self.read_i32();
|
|
||||||
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
|
|
||||||
(
|
|
||||||
"JumpIfFalse",
|
|
||||||
format!("-> {:04x} offset={}", target, offset),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Op::JumpIfTrue => {
|
|
||||||
let offset = self.read_i32();
|
|
||||||
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
|
|
||||||
("JumpIfTrue", format!("-> {:04x} offset={}", target, offset))
|
|
||||||
}
|
|
||||||
Op::Jump => {
|
|
||||||
let offset = self.read_i32();
|
|
||||||
let target = (current_pc as isize + 1 + 4 + offset as isize) as usize;
|
|
||||||
("Jump", format!("-> {:04x} offset={}", target, offset))
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::ConcatStrings => {
|
|
||||||
let count = self.read_u16();
|
|
||||||
let force = self.read_u8();
|
|
||||||
("ConcatStrings", format!("count={} force={}", count, force))
|
|
||||||
}
|
|
||||||
Op::CoerceToString => ("CoerceToString", String::new()),
|
|
||||||
Op::ResolvePath => {
|
|
||||||
let dir_id = self.read_u32();
|
|
||||||
let dir = self.ctx.resolve_string(dir_id);
|
|
||||||
("ResolvePath", format!("dir={:?}", dir))
|
|
||||||
}
|
|
||||||
Op::Assert => {
|
|
||||||
let raw_idx = self.read_u32();
|
|
||||||
let span_id = self.read_u32();
|
|
||||||
("Assert", format!("text_id={} span={}", raw_idx, span_id))
|
|
||||||
}
|
|
||||||
Op::LookupWith => {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
let name = self.ctx.resolve_string(idx);
|
|
||||||
let n = self.read_u8();
|
|
||||||
for _ in 0..n {
|
|
||||||
self.read_operand_data();
|
|
||||||
}
|
|
||||||
("LookupWith", format!("sym={:?} n={}", name, n))
|
|
||||||
}
|
|
||||||
|
|
||||||
Op::LoadBuiltins => ("LoadBuiltins", String::new()),
|
|
||||||
Op::LoadBuiltin => {
|
|
||||||
let id = self.read_u8();
|
|
||||||
("LoadBuiltin", format!("id={}", id))
|
|
||||||
}
|
|
||||||
Op::LoadReplBinding => {
|
|
||||||
let idx = self.read_u32();
|
|
||||||
let name = self.ctx.resolve_string(idx);
|
|
||||||
("LoadReplBinding", format!("{:?}", name))
|
|
||||||
}
|
|
||||||
Op::LoadScopedBinding => {
|
|
||||||
let slot = self.read_u32();
|
|
||||||
let idx = self.read_u32();
|
|
||||||
let name = self.ctx.resolve_string(idx);
|
|
||||||
("LoadScopedBinding", format!("slot={} {:?}", slot, name))
|
|
||||||
}
|
|
||||||
Op::Return => ("Return", String::new()),
|
|
||||||
Op::Illegal => ("Illegal", String::new()),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,903 +0,0 @@
|
|||||||
use fix_builtins::{BUILTINS, BuiltinId};
|
|
||||||
use fix_common::StringId;
|
|
||||||
use fix_ir::{Attr, BinOpKind, Ir, MaybeThunk, Param, RawIrRef, ThunkId, UnOpKind};
|
|
||||||
use hashbrown::HashMap;
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use rnix::TextRange;
|
|
||||||
use string_interner::Symbol as _;
|
|
||||||
|
|
||||||
pub mod disassembler;
|
|
||||||
|
|
||||||
pub struct InstructionPtr(pub usize);
|
|
||||||
|
|
||||||
pub trait BytecodeContext {
|
|
||||||
fn intern_string(&mut self, s: &str) -> StringId;
|
|
||||||
fn register_span(&mut self, range: TextRange) -> u32;
|
|
||||||
fn get_code(&self) -> &[u8];
|
|
||||||
fn get_code_mut(&mut self) -> &mut Vec<u8>;
|
|
||||||
fn add_constant(&mut self, val: Const) -> u32;
|
|
||||||
fn current_source_dir(&mut self) -> StringId;
|
|
||||||
fn current_scope_slot(&self) -> Option<u32>;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
|
||||||
pub enum Op {
|
|
||||||
PushSmi,
|
|
||||||
PushBigInt,
|
|
||||||
PushFloat,
|
|
||||||
PushString,
|
|
||||||
PushNull,
|
|
||||||
PushTrue,
|
|
||||||
PushFalse,
|
|
||||||
|
|
||||||
LoadLocal,
|
|
||||||
LoadOuter,
|
|
||||||
StoreLocal,
|
|
||||||
AllocLocals,
|
|
||||||
|
|
||||||
MakeThunk,
|
|
||||||
MakeClosure,
|
|
||||||
MakePatternClosure,
|
|
||||||
|
|
||||||
Call,
|
|
||||||
DispatchPrimOp,
|
|
||||||
|
|
||||||
MakeAttrs,
|
|
||||||
MakeEmptyAttrs,
|
|
||||||
SelectStatic,
|
|
||||||
SelectDynamic,
|
|
||||||
HasAttrPathStatic,
|
|
||||||
HasAttrPathDynamic,
|
|
||||||
HasAttrStatic,
|
|
||||||
HasAttrDynamic,
|
|
||||||
HasAttrResolve,
|
|
||||||
JumpIfSelectSucceeded,
|
|
||||||
JumpIfSelectFailed,
|
|
||||||
|
|
||||||
MakeList,
|
|
||||||
MakeEmptyList,
|
|
||||||
|
|
||||||
OpAdd,
|
|
||||||
OpSub,
|
|
||||||
OpMul,
|
|
||||||
OpDiv,
|
|
||||||
OpEq,
|
|
||||||
OpNeq,
|
|
||||||
OpLt,
|
|
||||||
OpGt,
|
|
||||||
OpLeq,
|
|
||||||
OpGeq,
|
|
||||||
OpConcat,
|
|
||||||
OpUpdate,
|
|
||||||
|
|
||||||
OpNeg,
|
|
||||||
OpNot,
|
|
||||||
|
|
||||||
JumpIfFalse,
|
|
||||||
JumpIfTrue,
|
|
||||||
Jump,
|
|
||||||
|
|
||||||
CoerceToString,
|
|
||||||
|
|
||||||
ConcatStrings,
|
|
||||||
ResolvePath,
|
|
||||||
|
|
||||||
Assert,
|
|
||||||
|
|
||||||
LookupWith,
|
|
||||||
|
|
||||||
LoadBuiltins,
|
|
||||||
LoadBuiltin,
|
|
||||||
|
|
||||||
LoadReplBinding,
|
|
||||||
LoadScopedBinding,
|
|
||||||
|
|
||||||
Return,
|
|
||||||
|
|
||||||
Illegal,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ScopeInfo {
|
|
||||||
depth: u8,
|
|
||||||
thunk_map: HashMap<ThunkId, u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
|
|
||||||
ctx: &'a mut Ctx,
|
|
||||||
scope_stack: Vec<ScopeInfo>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
|
|
||||||
pub enum OperandType {
|
|
||||||
Const,
|
|
||||||
BigInt,
|
|
||||||
Local,
|
|
||||||
BuiltinConst,
|
|
||||||
Builtins,
|
|
||||||
ReplBinding,
|
|
||||||
ScopedImportBinding,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum Const {
|
|
||||||
Smi(i32),
|
|
||||||
Float(f64),
|
|
||||||
Bool(bool),
|
|
||||||
String(StringId),
|
|
||||||
Path(StringId),
|
|
||||||
PrimOp {
|
|
||||||
id: BuiltinId,
|
|
||||||
arity: u8,
|
|
||||||
dispatch_ip: u32,
|
|
||||||
},
|
|
||||||
Null,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(u8)]
|
|
||||||
#[derive(Debug, Clone, Copy, TryFromPrimitive)]
|
|
||||||
pub enum AttrKeyType {
|
|
||||||
Static,
|
|
||||||
Dynamic,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum InlineOperand {
|
|
||||||
Const(Const),
|
|
||||||
BigInt(i64),
|
|
||||||
Local { layer: u8, local: u32 },
|
|
||||||
BuiltinConst(StringId),
|
|
||||||
Builtins,
|
|
||||||
ReplBinding(StringId),
|
|
||||||
ScopedImportBinding(StringId),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
|
|
||||||
let ip = ctx.get_code().len();
|
|
||||||
let mut emitter = BytecodeEmitter::new(ctx);
|
|
||||||
emitter.emit_toplevel(ir);
|
|
||||||
InstructionPtr(ip)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
|
||||||
fn new(ctx: &'a mut Ctx) -> Self {
|
|
||||||
Self {
|
|
||||||
ctx,
|
|
||||||
scope_stack: Vec::with_capacity(32),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[must_use]
|
|
||||||
fn inline_maybe_thunk(&self, val: &MaybeThunk) -> InlineOperand {
|
|
||||||
use MaybeThunk::*;
|
|
||||||
match *val {
|
|
||||||
Int(x) => {
|
|
||||||
if let Ok(x) = x.try_into() {
|
|
||||||
InlineOperand::Const(Const::Smi(x))
|
|
||||||
} else {
|
|
||||||
InlineOperand::BigInt(x)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Float(x) => InlineOperand::Const(Const::Float(x)),
|
|
||||||
Bool(b) => InlineOperand::Const(Const::Bool(b)),
|
|
||||||
Null => InlineOperand::Const(Const::Null),
|
|
||||||
Str(id) => InlineOperand::Const(Const::String(id)),
|
|
||||||
Path(id) => InlineOperand::Const(Const::String(id)),
|
|
||||||
Thunk(id) => {
|
|
||||||
let (layer, local) = self.resolve_thunk(id);
|
|
||||||
InlineOperand::Local { layer, local }
|
|
||||||
}
|
|
||||||
Arg { layer } => InlineOperand::Local { layer, local: 0 },
|
|
||||||
Builtin(id) => {
|
|
||||||
let (_, arity) = BUILTINS[id as usize];
|
|
||||||
InlineOperand::Const(Const::PrimOp {
|
|
||||||
id,
|
|
||||||
arity,
|
|
||||||
dispatch_ip: id.entry_phase().ip(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
BuiltinConst(id) => InlineOperand::BuiltinConst(id),
|
|
||||||
Builtins => InlineOperand::Builtins,
|
|
||||||
ReplBinding(id) => InlineOperand::ReplBinding(id),
|
|
||||||
ScopedImportBinding(id) => InlineOperand::ScopedImportBinding(id),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_maybe_thunk(&mut self, val: &MaybeThunk) {
|
|
||||||
use InlineOperand::*;
|
|
||||||
let operand = self.inline_maybe_thunk(val);
|
|
||||||
match operand {
|
|
||||||
Const(val) => {
|
|
||||||
let idx = self.ctx.add_constant(val);
|
|
||||||
self.emit_u8(OperandType::Const as u8);
|
|
||||||
self.emit_u32(idx);
|
|
||||||
}
|
|
||||||
BigInt(val) => {
|
|
||||||
self.emit_u8(OperandType::BigInt as u8);
|
|
||||||
self.emit_i64(val);
|
|
||||||
}
|
|
||||||
Local { layer, local } => {
|
|
||||||
self.emit_u8(OperandType::Local as u8);
|
|
||||||
self.emit_u8(layer);
|
|
||||||
self.emit_u32(local);
|
|
||||||
}
|
|
||||||
BuiltinConst(id) => {
|
|
||||||
self.emit_u8(OperandType::BuiltinConst as u8);
|
|
||||||
self.emit_str_id(id);
|
|
||||||
}
|
|
||||||
Builtins => {
|
|
||||||
self.emit_u8(OperandType::Builtins as u8);
|
|
||||||
}
|
|
||||||
ReplBinding(id) => {
|
|
||||||
self.emit_u8(OperandType::ReplBinding as u8);
|
|
||||||
self.emit_str_id(id);
|
|
||||||
}
|
|
||||||
ScopedImportBinding(id) => {
|
|
||||||
self.emit_u8(OperandType::ScopedImportBinding as u8);
|
|
||||||
let slot = self
|
|
||||||
.ctx
|
|
||||||
.current_scope_slot()
|
|
||||||
.expect("ScopedImportBinding outside scoped compilation");
|
|
||||||
self.emit_u32(slot);
|
|
||||||
self.emit_str_id(id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_op(&mut self, op: Op) {
|
|
||||||
self.ctx.get_code_mut().push(op as u8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_bool(&mut self, val: bool) {
|
|
||||||
self.emit_u8(u8::from(val));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_u8(&mut self, val: u8) {
|
|
||||||
self.ctx.get_code_mut().push(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_u16(&mut self, val: u16) {
|
|
||||||
self.ctx
|
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_u32(&mut self, val: u32) {
|
|
||||||
self.ctx
|
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_i32(&mut self, val: i32) {
|
|
||||||
self.ctx
|
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_i64(&mut self, val: i64) {
|
|
||||||
self.ctx
|
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_f64(&mut self, val: f64) {
|
|
||||||
self.ctx
|
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_i32_placeholder(&mut self) -> usize {
|
|
||||||
let offset = self.ctx.get_code_mut().len();
|
|
||||||
self.ctx.get_code_mut().extend_from_slice(&[0u8; 4]);
|
|
||||||
offset
|
|
||||||
}
|
|
||||||
#[inline]
|
|
||||||
fn patch_i32(&mut self, offset: usize, val: i32) {
|
|
||||||
self.ctx.get_code_mut()[offset..offset + 4].copy_from_slice(&val.to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_jump_placeholder(&mut self) -> usize {
|
|
||||||
self.emit_op(Op::Jump);
|
|
||||||
self.emit_i32_placeholder()
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn patch_jump_target(&mut self, placeholder_offset: usize) {
|
|
||||||
let current_pos = self.ctx.get_code_mut().len();
|
|
||||||
let relative_offset = (current_pos as i32) - (placeholder_offset as i32) - 4;
|
|
||||||
self.patch_i32(placeholder_offset, relative_offset);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn emit_str_id(&mut self, id: StringId) {
|
|
||||||
self.ctx
|
|
||||||
.get_code_mut()
|
|
||||||
.extend_from_slice(&(id.0.to_usize() as u32).to_le_bytes());
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_depth(&self) -> u8 {
|
|
||||||
self.scope_stack.last().map_or(0, |s| s.depth)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_thunk(&self, id: ThunkId) -> (u8, u32) {
|
|
||||||
for scope in self.scope_stack.iter().rev() {
|
|
||||||
if let Some(&local_idx) = scope.thunk_map.get(&id) {
|
|
||||||
let layer = self.current_depth() - scope.depth;
|
|
||||||
return (layer, local_idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
panic!("ThunkId {:?} not found in any scope", id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_load(&mut self, layer: u8, local: u32) {
|
|
||||||
if layer == 0 {
|
|
||||||
self.emit_op(Op::LoadLocal);
|
|
||||||
self.emit_u32(local);
|
|
||||||
} else {
|
|
||||||
self.emit_op(Op::LoadOuter);
|
|
||||||
self.emit_u8(layer);
|
|
||||||
self.emit_u32(local);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn push_scope(&mut self, has_arg: bool, thunk_ids: &[ThunkId]) {
|
|
||||||
let depth = self.scope_stack.len().try_into().expect("scope too deep!");
|
|
||||||
let thunk_base = if has_arg { 1u32 } else { 0u32 };
|
|
||||||
let thunk_map = thunk_ids
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.map(|(i, &id)| (id, thunk_base + i as u32))
|
|
||||||
.collect();
|
|
||||||
self.scope_stack.push(ScopeInfo { depth, thunk_map });
|
|
||||||
}
|
|
||||||
|
|
||||||
fn pop_scope(&mut self) {
|
|
||||||
self.scope_stack.pop();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
|
|
||||||
match ir {
|
|
||||||
&Ir::TopLevel { body, ref thunks } => {
|
|
||||||
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
|
|
||||||
self.push_scope(false, &thunk_ids);
|
|
||||||
if !thunks.is_empty() {
|
|
||||||
self.emit_op(Op::AllocLocals);
|
|
||||||
self.emit_u32(thunks.len().try_into().expect("too many thunks"));
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit_scope_thunks(thunks);
|
|
||||||
self.emit_expr(body);
|
|
||||||
self.emit_op(Op::Return);
|
|
||||||
|
|
||||||
self.pop_scope();
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.push_scope(false, &[]);
|
|
||||||
self.emit_expr(ir);
|
|
||||||
self.emit_op(Op::Return);
|
|
||||||
self.pop_scope();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_scope_thunks(&mut self, thunks: &[(ThunkId, RawIrRef<'_>)]) {
|
|
||||||
for &(id, inner) in thunks {
|
|
||||||
let skip_patch = self.emit_jump_placeholder();
|
|
||||||
let entry_point = self.ctx.get_code_mut().len() as u32;
|
|
||||||
self.emit_expr(inner);
|
|
||||||
self.emit_op(Op::Return);
|
|
||||||
self.patch_jump_target(skip_patch);
|
|
||||||
self.emit_op(Op::MakeThunk);
|
|
||||||
self.emit_u32(entry_point);
|
|
||||||
let (_, local_idx) = self.resolve_thunk(id);
|
|
||||||
self.emit_op(Op::StoreLocal);
|
|
||||||
self.emit_u32(local_idx);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_expr(&mut self, ir: RawIrRef<'_>) {
|
|
||||||
match ir {
|
|
||||||
&Ir::Int(x) => {
|
|
||||||
if let Ok(x) = x.try_into() {
|
|
||||||
self.emit_op(Op::PushSmi);
|
|
||||||
self.emit_i32(x);
|
|
||||||
} else {
|
|
||||||
self.emit_op(Op::PushBigInt);
|
|
||||||
self.emit_i64(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&Ir::Float(x) => {
|
|
||||||
self.emit_op(Op::PushFloat);
|
|
||||||
self.emit_f64(x);
|
|
||||||
}
|
|
||||||
&Ir::Bool(true) => self.emit_op(Op::PushTrue),
|
|
||||||
&Ir::Bool(false) => self.emit_op(Op::PushFalse),
|
|
||||||
Ir::Null => self.emit_op(Op::PushNull),
|
|
||||||
&Ir::Str(id) => {
|
|
||||||
self.emit_op(Op::PushString);
|
|
||||||
self.emit_str_id(id);
|
|
||||||
}
|
|
||||||
&Ir::Path(p) => {
|
|
||||||
self.emit_expr(p);
|
|
||||||
self.emit_op(Op::ResolvePath);
|
|
||||||
let dir_id = self.ctx.current_source_dir();
|
|
||||||
self.emit_str_id(dir_id);
|
|
||||||
}
|
|
||||||
&Ir::If { cond, consq, alter } => {
|
|
||||||
self.emit_expr(cond);
|
|
||||||
|
|
||||||
self.emit_op(Op::JumpIfFalse);
|
|
||||||
let else_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jif = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
self.emit_expr(consq);
|
|
||||||
|
|
||||||
self.emit_op(Op::Jump);
|
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jump = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
let else_offset = (after_jump as i32) - (after_jif as i32);
|
|
||||||
self.patch_i32(else_placeholder, else_offset);
|
|
||||||
|
|
||||||
self.emit_expr(alter);
|
|
||||||
|
|
||||||
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
|
|
||||||
self.patch_i32(end_placeholder, end_offset);
|
|
||||||
}
|
|
||||||
&Ir::BinOp { lhs, rhs, kind } => {
|
|
||||||
self.emit_binop(lhs, rhs, kind);
|
|
||||||
}
|
|
||||||
&Ir::UnOp { rhs, kind } => match kind {
|
|
||||||
UnOpKind::Neg => {
|
|
||||||
self.emit_expr(rhs);
|
|
||||||
self.emit_op(Op::OpNeg);
|
|
||||||
}
|
|
||||||
UnOpKind::Not => {
|
|
||||||
self.emit_expr(rhs);
|
|
||||||
self.emit_op(Op::OpNot);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
&Ir::Func {
|
|
||||||
body,
|
|
||||||
ref param,
|
|
||||||
ref thunks,
|
|
||||||
} => {
|
|
||||||
self.emit_func(thunks, param, body);
|
|
||||||
}
|
|
||||||
Ir::AttrSet { stcs, dyns } => {
|
|
||||||
self.emit_attrset(stcs, dyns);
|
|
||||||
}
|
|
||||||
Ir::List { items } => {
|
|
||||||
if items.is_empty() {
|
|
||||||
self.emit_op(Op::MakeEmptyList);
|
|
||||||
} else {
|
|
||||||
self.emit_op(Op::MakeList);
|
|
||||||
self.emit_u32(items.len() as u32);
|
|
||||||
for &item in items.iter() {
|
|
||||||
self.emit_maybe_thunk(item);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&Ir::Call { func, arg, .. } => {
|
|
||||||
self.emit_expr(func);
|
|
||||||
self.emit_op(Op::Call);
|
|
||||||
self.emit_maybe_thunk(arg);
|
|
||||||
}
|
|
||||||
&Ir::Arg { layer } => {
|
|
||||||
self.emit_load(layer, 0);
|
|
||||||
}
|
|
||||||
&Ir::TopLevel { body, ref thunks } => {
|
|
||||||
self.emit_toplevel_inner(body, thunks);
|
|
||||||
}
|
|
||||||
&Ir::Select {
|
|
||||||
expr,
|
|
||||||
ref attrpath,
|
|
||||||
default,
|
|
||||||
span,
|
|
||||||
} => {
|
|
||||||
self.emit_select(expr, attrpath, default, span);
|
|
||||||
}
|
|
||||||
Ir::Builtins => {
|
|
||||||
self.emit_op(Op::LoadBuiltins);
|
|
||||||
}
|
|
||||||
&Ir::Builtin(id) => {
|
|
||||||
self.emit_op(Op::LoadBuiltin);
|
|
||||||
self.emit_u8(id as u8);
|
|
||||||
}
|
|
||||||
&Ir::BuiltinConst(id) => {
|
|
||||||
self.emit_select(
|
|
||||||
&Ir::Builtins,
|
|
||||||
&[Attr::Str(id, TextRange::default())],
|
|
||||||
None,
|
|
||||||
TextRange::default(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
&Ir::ConcatStrings {
|
|
||||||
ref parts,
|
|
||||||
force_string,
|
|
||||||
} => {
|
|
||||||
for &part in parts.iter() {
|
|
||||||
self.emit_expr(part);
|
|
||||||
self.emit_op(Op::CoerceToString);
|
|
||||||
}
|
|
||||||
self.emit_op(Op::ConcatStrings);
|
|
||||||
self.emit_u16(parts.len() as u16);
|
|
||||||
self.emit_bool(force_string);
|
|
||||||
}
|
|
||||||
&Ir::HasAttr { lhs, ref rhs } => {
|
|
||||||
self.emit_has_attr(lhs, rhs);
|
|
||||||
}
|
|
||||||
Ir::Assert {
|
|
||||||
assertion,
|
|
||||||
expr,
|
|
||||||
assertion_raw,
|
|
||||||
span,
|
|
||||||
} => {
|
|
||||||
let raw_idx = self.ctx.intern_string(assertion_raw);
|
|
||||||
let span_id = self.ctx.register_span(*span);
|
|
||||||
self.emit_expr(*assertion);
|
|
||||||
self.emit_op(Op::Assert);
|
|
||||||
self.emit_str_id(raw_idx);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
self.emit_expr(*expr);
|
|
||||||
}
|
|
||||||
&Ir::ReplBinding(name) => {
|
|
||||||
self.emit_op(Op::LoadReplBinding);
|
|
||||||
self.emit_str_id(name);
|
|
||||||
}
|
|
||||||
&Ir::ScopedImportBinding(name) => {
|
|
||||||
self.emit_op(Op::LoadScopedBinding);
|
|
||||||
let slot = self
|
|
||||||
.ctx
|
|
||||||
.current_scope_slot()
|
|
||||||
.expect("ScopedImportBinding outside scoped compilation");
|
|
||||||
self.emit_u32(slot);
|
|
||||||
self.emit_str_id(name);
|
|
||||||
}
|
|
||||||
Ir::WithLookup { sym, namespaces } => {
|
|
||||||
// counter
|
|
||||||
self.emit_expr(&Ir::Int(0));
|
|
||||||
self.emit_op(Op::LookupWith);
|
|
||||||
self.emit_str_id(*sym);
|
|
||||||
self.emit_u8(
|
|
||||||
namespaces
|
|
||||||
.len()
|
|
||||||
.try_into()
|
|
||||||
.expect("too many `with` namespaces"),
|
|
||||||
);
|
|
||||||
for namespace in namespaces {
|
|
||||||
self.emit_maybe_thunk(namespace);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&Ir::MaybeThunk(thunk) => {
|
|
||||||
use MaybeThunk::*;
|
|
||||||
match *thunk {
|
|
||||||
Int(x) => {
|
|
||||||
if let Ok(x) = x.try_into() {
|
|
||||||
self.emit_op(Op::PushSmi);
|
|
||||||
self.emit_i32(x);
|
|
||||||
} else {
|
|
||||||
self.emit_op(Op::PushBigInt);
|
|
||||||
self.emit_i64(x);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Float(x) => {
|
|
||||||
self.emit_op(Op::PushFloat);
|
|
||||||
self.emit_f64(x);
|
|
||||||
}
|
|
||||||
Bool(true) => self.emit_op(Op::PushTrue),
|
|
||||||
Bool(false) => self.emit_op(Op::PushFalse),
|
|
||||||
Null => self.emit_op(Op::PushNull),
|
|
||||||
Str(id) => {
|
|
||||||
self.emit_op(Op::PushString);
|
|
||||||
self.emit_str_id(id);
|
|
||||||
}
|
|
||||||
Path(id) => {
|
|
||||||
self.emit_op(Op::PushString);
|
|
||||||
self.emit_str_id(id);
|
|
||||||
self.emit_op(Op::ResolvePath);
|
|
||||||
let dir_id = self.ctx.current_source_dir();
|
|
||||||
self.emit_str_id(dir_id);
|
|
||||||
}
|
|
||||||
Thunk(id) => {
|
|
||||||
let (layer, local) = self.resolve_thunk(id);
|
|
||||||
self.emit_load(layer, local);
|
|
||||||
}
|
|
||||||
Arg { layer } => self.emit_load(layer, 0),
|
|
||||||
Builtin(id) => {
|
|
||||||
self.emit_op(Op::LoadBuiltin);
|
|
||||||
self.emit_u8(id as u8);
|
|
||||||
}
|
|
||||||
BuiltinConst(id) => self.emit_select(
|
|
||||||
&Ir::Builtins,
|
|
||||||
&[Attr::Str(id, TextRange::default())],
|
|
||||||
None,
|
|
||||||
TextRange::default(),
|
|
||||||
),
|
|
||||||
Builtins => self.emit_op(Op::LoadBuiltins),
|
|
||||||
ReplBinding(name) => {
|
|
||||||
self.emit_op(Op::LoadReplBinding);
|
|
||||||
self.emit_str_id(name);
|
|
||||||
}
|
|
||||||
ScopedImportBinding(name) => {
|
|
||||||
self.emit_op(Op::LoadScopedBinding);
|
|
||||||
let slot = self
|
|
||||||
.ctx
|
|
||||||
.current_scope_slot()
|
|
||||||
.expect("ScopedImportBinding outside scoped compilation");
|
|
||||||
self.emit_u32(slot);
|
|
||||||
self.emit_str_id(name);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_binop(&mut self, lhs: RawIrRef<'_>, rhs: RawIrRef<'_>, kind: BinOpKind) {
|
|
||||||
use BinOpKind::*;
|
|
||||||
match kind {
|
|
||||||
And => {
|
|
||||||
self.emit_expr(lhs);
|
|
||||||
self.emit_op(Op::JumpIfFalse);
|
|
||||||
let skip_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jif = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
self.emit_expr(rhs);
|
|
||||||
self.emit_op(Op::Jump);
|
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jump = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
let false_offset = (after_jump as i32) - (after_jif as i32);
|
|
||||||
self.patch_i32(skip_placeholder, false_offset);
|
|
||||||
|
|
||||||
self.emit_op(Op::PushFalse);
|
|
||||||
|
|
||||||
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
|
|
||||||
self.patch_i32(end_placeholder, end_offset);
|
|
||||||
}
|
|
||||||
Or => {
|
|
||||||
self.emit_expr(lhs);
|
|
||||||
self.emit_op(Op::JumpIfTrue);
|
|
||||||
let skip_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jit = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
self.emit_expr(rhs);
|
|
||||||
self.emit_op(Op::Jump);
|
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jump = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
let true_offset = (after_jump as i32) - (after_jit as i32);
|
|
||||||
self.patch_i32(skip_placeholder, true_offset);
|
|
||||||
|
|
||||||
self.emit_op(Op::PushTrue);
|
|
||||||
|
|
||||||
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
|
|
||||||
self.patch_i32(end_placeholder, end_offset);
|
|
||||||
}
|
|
||||||
Impl => {
|
|
||||||
self.emit_expr(lhs);
|
|
||||||
self.emit_op(Op::JumpIfFalse);
|
|
||||||
let skip_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jif = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
self.emit_expr(rhs);
|
|
||||||
self.emit_op(Op::Jump);
|
|
||||||
let end_placeholder = self.emit_i32_placeholder();
|
|
||||||
let after_jump = self.ctx.get_code_mut().len();
|
|
||||||
|
|
||||||
let true_offset = (after_jump as i32) - (after_jif as i32);
|
|
||||||
self.patch_i32(skip_placeholder, true_offset);
|
|
||||||
|
|
||||||
self.emit_op(Op::PushTrue);
|
|
||||||
|
|
||||||
let end_offset = (self.ctx.get_code_mut().len() as i32) - (after_jump as i32);
|
|
||||||
self.patch_i32(end_placeholder, end_offset);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
self.emit_expr(lhs);
|
|
||||||
self.emit_expr(rhs);
|
|
||||||
self.emit_op(match kind {
|
|
||||||
Add => Op::OpAdd,
|
|
||||||
Sub => Op::OpSub,
|
|
||||||
Mul => Op::OpMul,
|
|
||||||
Div => Op::OpDiv,
|
|
||||||
Eq => Op::OpEq,
|
|
||||||
Neq => Op::OpNeq,
|
|
||||||
Lt => Op::OpLt,
|
|
||||||
Gt => Op::OpGt,
|
|
||||||
Leq => Op::OpLeq,
|
|
||||||
Geq => Op::OpGeq,
|
|
||||||
Con => Op::OpConcat,
|
|
||||||
Upd => Op::OpUpdate,
|
|
||||||
_ => unreachable!(),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_func<'ir>(
|
|
||||||
&mut self,
|
|
||||||
thunks: &[(ThunkId, RawIrRef<'ir>)],
|
|
||||||
param: &Option<Param<'ir>>,
|
|
||||||
body: RawIrRef<'ir>,
|
|
||||||
) {
|
|
||||||
let thunk_ids: Vec<ThunkId> = thunks.iter().map(|&(id, _)| id).collect();
|
|
||||||
|
|
||||||
let skip_patch = self.emit_jump_placeholder();
|
|
||||||
let entry_point = self.ctx.get_code().len() as u32;
|
|
||||||
self.push_scope(true, &thunk_ids);
|
|
||||||
self.emit_scope_thunks(thunks);
|
|
||||||
self.emit_expr(body);
|
|
||||||
self.emit_op(Op::Return);
|
|
||||||
self.pop_scope();
|
|
||||||
self.patch_jump_target(skip_patch);
|
|
||||||
|
|
||||||
if let Some(Param {
|
|
||||||
required,
|
|
||||||
optional,
|
|
||||||
ellipsis,
|
|
||||||
}) = param
|
|
||||||
{
|
|
||||||
self.emit_op(Op::MakePatternClosure);
|
|
||||||
self.emit_u32(entry_point);
|
|
||||||
self.emit_u32(thunks.len().try_into().expect("too many thunks"));
|
|
||||||
self.emit_u16(required.len() as u16);
|
|
||||||
self.emit_u16(optional.len() as u16);
|
|
||||||
self.emit_bool(*ellipsis);
|
|
||||||
|
|
||||||
for &(sym, _) in required.iter() {
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
}
|
|
||||||
for &(sym, _) in optional.iter() {
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
}
|
|
||||||
for &(sym, span) in required.iter().chain(optional.iter()) {
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
self.emit_op(Op::MakeClosure);
|
|
||||||
self.emit_u32(entry_point);
|
|
||||||
self.emit_u32(thunks.len().try_into().expect("too many thunks"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_attrset(
|
|
||||||
&mut self,
|
|
||||||
stcs: &fix_ir::HashMap<'_, StringId, (&MaybeThunk, TextRange)>,
|
|
||||||
dyns: &[(RawIrRef<'_>, &MaybeThunk, TextRange)],
|
|
||||||
) {
|
|
||||||
if stcs.is_empty() && dyns.is_empty() {
|
|
||||||
self.emit_op(Op::MakeEmptyAttrs);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
for &(key_expr, _val, _span) in dyns.iter() {
|
|
||||||
self.emit_expr(key_expr);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.emit_op(Op::MakeAttrs);
|
|
||||||
self.emit_u32(stcs.len() as u32);
|
|
||||||
self.emit_u32(dyns.len() as u32);
|
|
||||||
|
|
||||||
for (&sym, &(val, span)) in stcs.iter() {
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
self.emit_maybe_thunk(val);
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
}
|
|
||||||
|
|
||||||
for &(_key, val, span) in dyns.iter() {
|
|
||||||
self.emit_maybe_thunk(val);
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_select(
|
|
||||||
&mut self,
|
|
||||||
expr: RawIrRef<'_>,
|
|
||||||
attrpath: &[Attr<RawIrRef<'_>>],
|
|
||||||
default: Option<RawIrRef<'_>>,
|
|
||||||
span: TextRange,
|
|
||||||
) {
|
|
||||||
self.emit_expr(expr);
|
|
||||||
|
|
||||||
let mut dynamic_patches = Vec::new();
|
|
||||||
for attr in attrpath.iter() {
|
|
||||||
match *attr {
|
|
||||||
Attr::Str(sym, _) => {
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_op(Op::SelectStatic);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
}
|
|
||||||
Attr::Dynamic(key_expr, _) => {
|
|
||||||
self.emit_op(Op::JumpIfSelectFailed);
|
|
||||||
dynamic_patches.push(self.emit_i32_placeholder());
|
|
||||||
self.emit_expr(key_expr);
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_op(Op::SelectDynamic);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(default) = default {
|
|
||||||
let before: i32 = self.ctx.get_code().len().try_into().unwrap();
|
|
||||||
for patch in dynamic_patches {
|
|
||||||
self.patch_jump_target(patch);
|
|
||||||
}
|
|
||||||
self.emit_op(Op::JumpIfSelectSucceeded);
|
|
||||||
let placeholder = self.emit_i32_placeholder();
|
|
||||||
self.emit_expr(default);
|
|
||||||
let after: i32 = self.ctx.get_code().len().try_into().unwrap();
|
|
||||||
// Offset is relative to after the placeholder, so subtract the
|
|
||||||
// size of JumpIfSelectSucceeded (1) + placeholder (4).
|
|
||||||
self.patch_i32(placeholder, after - before - 5);
|
|
||||||
} else {
|
|
||||||
for patch in dynamic_patches {
|
|
||||||
self.patch_jump_target(patch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
|
|
||||||
self.emit_expr(lhs);
|
|
||||||
|
|
||||||
let mut dynamic_patches = Vec::new();
|
|
||||||
let [attrs @ .., last] = rhs else {
|
|
||||||
panic!("attrpath is empty");
|
|
||||||
};
|
|
||||||
for attr in attrs {
|
|
||||||
match *attr {
|
|
||||||
Attr::Str(sym, span) => {
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_op(Op::HasAttrPathStatic);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
}
|
|
||||||
Attr::Dynamic(key_expr, span) => {
|
|
||||||
self.emit_op(Op::JumpIfSelectFailed);
|
|
||||||
dynamic_patches.push(self.emit_i32_placeholder());
|
|
||||||
self.emit_expr(key_expr);
|
|
||||||
let span_id = self.ctx.register_span(span);
|
|
||||||
self.emit_op(Op::HasAttrPathDynamic);
|
|
||||||
self.emit_u32(span_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
match *last {
|
|
||||||
Attr::Str(sym, _) => {
|
|
||||||
self.emit_op(Op::HasAttrStatic);
|
|
||||||
self.emit_str_id(sym);
|
|
||||||
}
|
|
||||||
Attr::Dynamic(key_expr, _) => {
|
|
||||||
self.emit_op(Op::JumpIfSelectFailed);
|
|
||||||
dynamic_patches.push(self.emit_i32_placeholder());
|
|
||||||
self.emit_expr(key_expr);
|
|
||||||
self.emit_op(Op::HasAttrDynamic);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for patch in dynamic_patches {
|
|
||||||
self.patch_jump_target(patch);
|
|
||||||
}
|
|
||||||
self.emit_op(Op::HasAttrResolve);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
|
|
||||||
self.emit_scope_thunks(thunks);
|
|
||||||
self.emit_expr(body);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix-common"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
gc-arena = { workspace = true }
|
|
||||||
string-interner = { workspace = true }
|
|
||||||
ere = { workspace = true }
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix-error"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
miette = { version = "7.6", features = ["fancy"] }
|
|
||||||
thiserror = "2.0"
|
|
||||||
rnix = { workspace = true }
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
use std::path::{Path, PathBuf};
|
|
||||||
use std::sync::Arc;
|
|
||||||
|
|
||||||
use miette::{Diagnostic, NamedSource, SourceSpan};
|
|
||||||
use thiserror::Error;
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Box<Error>>;
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub enum SourceType {
|
|
||||||
/// dir
|
|
||||||
Eval(Arc<PathBuf>),
|
|
||||||
/// dir
|
|
||||||
Repl(Arc<PathBuf>),
|
|
||||||
/// file
|
|
||||||
File(Arc<PathBuf>),
|
|
||||||
/// virtual (name, no path)
|
|
||||||
Virtual(Arc<str>),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
|
||||||
pub struct Source {
|
|
||||||
pub ty: SourceType,
|
|
||||||
pub src: Arc<str>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl TryFrom<&str> for Source {
|
|
||||||
type Error = Box<Error>;
|
|
||||||
fn try_from(value: &str) -> Result<Self> {
|
|
||||||
Source::new_eval(value.into())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<Source> for NamedSource<Arc<str>> {
|
|
||||||
fn from(value: Source) -> Self {
|
|
||||||
let name = value.get_name();
|
|
||||||
NamedSource::new(name, value.src.clone())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Source {
|
|
||||||
pub fn new_file(path: PathBuf) -> std::io::Result<Self> {
|
|
||||||
Ok(Source {
|
|
||||||
src: std::fs::read_to_string(&path)?.into(),
|
|
||||||
ty: SourceType::File(Arc::new(path)),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_eval(src: String) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
ty: std::env::current_dir()
|
|
||||||
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
|
|
||||||
.map(Arc::new)
|
|
||||||
.map(SourceType::Eval)?,
|
|
||||||
src: src.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_repl(src: String) -> Result<Self> {
|
|
||||||
Ok(Self {
|
|
||||||
ty: std::env::current_dir()
|
|
||||||
.map_err(|err| Error::internal(format!("Failed to get current working dir: {err}")))
|
|
||||||
.map(Arc::new)
|
|
||||||
.map(SourceType::Repl)?,
|
|
||||||
src: src.into(),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_virtual(name: Arc<str>, src: String) -> Self {
|
|
||||||
Self {
|
|
||||||
ty: SourceType::Virtual(name),
|
|
||||||
src: src.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_dir(&self) -> &Path {
|
|
||||||
use SourceType::*;
|
|
||||||
match &self.ty {
|
|
||||||
Eval(dir) | Repl(dir) => dir.as_ref(),
|
|
||||||
File(file) => file
|
|
||||||
.as_path()
|
|
||||||
.parent()
|
|
||||||
.expect("source file must have a parent dir"),
|
|
||||||
Virtual(_) => Path::new("/"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_name(&self) -> String {
|
|
||||||
match &self.ty {
|
|
||||||
SourceType::Eval(_) => "«eval»".into(),
|
|
||||||
SourceType::Repl(_) => "«repl»".into(),
|
|
||||||
SourceType::File(path) => path.as_os_str().to_string_lossy().to_string(),
|
|
||||||
SourceType::Virtual(name) => name.to_string(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Error, Debug, Diagnostic)]
|
|
||||||
pub enum Error {
|
|
||||||
#[error("Parse error: {message}")]
|
|
||||||
#[diagnostic(code(nix::parse))]
|
|
||||||
ParseError {
|
|
||||||
#[source_code]
|
|
||||||
src: Option<NamedSource<Arc<str>>>,
|
|
||||||
#[label("error occurred here")]
|
|
||||||
span: Option<SourceSpan>,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("Downgrade error: {message}")]
|
|
||||||
#[diagnostic(code(nix::downgrade))]
|
|
||||||
DowngradeError {
|
|
||||||
#[source_code]
|
|
||||||
src: Option<NamedSource<Arc<str>>>,
|
|
||||||
#[label("{message}")]
|
|
||||||
span: Option<SourceSpan>,
|
|
||||||
message: String,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("Evaluation error: {message}")]
|
|
||||||
#[diagnostic(code(nix::eval))]
|
|
||||||
EvalError {
|
|
||||||
#[source_code]
|
|
||||||
src: Option<NamedSource<Arc<str>>>,
|
|
||||||
#[label("error occurred here")]
|
|
||||||
span: Option<SourceSpan>,
|
|
||||||
message: String,
|
|
||||||
#[related]
|
|
||||||
stack_trace: Vec<StackFrame>,
|
|
||||||
},
|
|
||||||
|
|
||||||
#[error("Internal error: {message}")]
|
|
||||||
#[diagnostic(code(nix::internal))]
|
|
||||||
InternalError { message: String },
|
|
||||||
|
|
||||||
#[error("{message}")]
|
|
||||||
#[diagnostic(code(nix::catchable))]
|
|
||||||
Catchable { message: String },
|
|
||||||
|
|
||||||
#[error("Unknown error")]
|
|
||||||
#[diagnostic(code(nix::unknown))]
|
|
||||||
Unknown,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Error {
|
|
||||||
pub fn parse_error(msg: String) -> Box<Self> {
|
|
||||||
Error::ParseError {
|
|
||||||
src: None,
|
|
||||||
span: None,
|
|
||||||
message: msg,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn downgrade_error(msg: String, src: Source, span: rnix::TextRange) -> Box<Self> {
|
|
||||||
Error::DowngradeError {
|
|
||||||
src: Some(src.into()),
|
|
||||||
span: Some(text_range_to_source_span(span)),
|
|
||||||
message: msg,
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_error(msg: impl Into<String>) -> Box<Self> {
|
|
||||||
Error::EvalError {
|
|
||||||
src: None,
|
|
||||||
span: None,
|
|
||||||
message: msg.into(),
|
|
||||||
stack_trace: Vec::new(),
|
|
||||||
}
|
|
||||||
.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn internal(msg: String) -> Box<Self> {
|
|
||||||
Error::InternalError { message: msg }.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn catchable(msg: String) -> Box<Self> {
|
|
||||||
Error::Catchable { message: msg }.into()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_span(mut self: Box<Self>, span: rnix::TextRange) -> Box<Self> {
|
|
||||||
use Error::*;
|
|
||||||
let source_span = Some(text_range_to_source_span(span));
|
|
||||||
let (ParseError { span, .. } | DowngradeError { span, .. } | EvalError { span, .. }) =
|
|
||||||
self.as_mut()
|
|
||||||
else {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
*span = source_span;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn with_source(mut self: Box<Self>, source: Source) -> Box<Self> {
|
|
||||||
use Error::*;
|
|
||||||
let new_src = Some(source.into());
|
|
||||||
let (ParseError { src, .. } | DowngradeError { src, .. } | EvalError { src, .. }) =
|
|
||||||
self.as_mut()
|
|
||||||
else {
|
|
||||||
return self;
|
|
||||||
};
|
|
||||||
*src = new_src;
|
|
||||||
self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn text_range_to_source_span(range: rnix::TextRange) -> SourceSpan {
|
|
||||||
let start = usize::from(range.start());
|
|
||||||
let len = usize::from(range.end()) - start;
|
|
||||||
SourceSpan::new(start.into(), len)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Stack frame types from Nix evaluation
|
|
||||||
#[derive(Debug, Clone, Error, Diagnostic)]
|
|
||||||
#[error("{message}")]
|
|
||||||
pub struct StackFrame {
|
|
||||||
#[label]
|
|
||||||
pub span: SourceSpan,
|
|
||||||
#[help]
|
|
||||||
pub message: String,
|
|
||||||
#[source_code]
|
|
||||||
pub src: NamedSource<Arc<str>>,
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix-ir"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
bumpalo = { workspace = true }
|
|
||||||
ghost-cell = { workspace = true }
|
|
||||||
rnix = { workspace = true }
|
|
||||||
rowan = { workspace = true }
|
|
||||||
string-interner = { workspace = true }
|
|
||||||
hashbrown = { workspace = true }
|
|
||||||
num_enum = { workspace = true }
|
|
||||||
|
|
||||||
fix-builtins = { path = "../fix-builtins" }
|
|
||||||
fix-common = { path = "../fix-common" }
|
|
||||||
fix-error = { path = "../fix-error" }
|
|
||||||
@@ -1,332 +0,0 @@
|
|||||||
use std::hash::Hash;
|
|
||||||
use std::marker::PhantomData;
|
|
||||||
|
|
||||||
use bumpalo::Bump;
|
|
||||||
use bumpalo::collections::Vec;
|
|
||||||
use fix_builtins::{BUILTINS, BuiltinId};
|
|
||||||
use fix_common::StringId;
|
|
||||||
use ghost_cell::{GhostCell, GhostToken};
|
|
||||||
use num_enum::TryFromPrimitive as _;
|
|
||||||
use rnix::{TextRange, ast};
|
|
||||||
use string_interner::DefaultStringInterner;
|
|
||||||
|
|
||||||
pub mod downgrade;
|
|
||||||
|
|
||||||
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
|
|
||||||
|
|
||||||
pub type GhostIrRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::IrRef;
|
|
||||||
pub type GhostRoIrRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::IrRef;
|
|
||||||
pub type RawIrRef<'ir> = <RawRef<'ir> as RefExt<'ir>>::IrRef;
|
|
||||||
pub type GhostMaybeThunkRef<'id, 'ir> = <GhostRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
|
|
||||||
pub type GhostRoMaybeThunkRef<'id, 'ir> = <GhostRoRef<'id, 'ir> as RefExt<'ir>>::MaybeThunkRef;
|
|
||||||
|
|
||||||
impl<'id, 'ir> Ir<'ir, GhostRoRef<'id, 'ir>> {
|
|
||||||
/// Freeze a mutable IR reference into a read-only one, consuming the
|
|
||||||
/// `GhostToken` to prevent any further mutation.
|
|
||||||
pub fn freeze(this: GhostRoIrRef<'id, 'ir>, _: GhostToken<'id>) -> RawIrRef<'ir> {
|
|
||||||
// SAFETY: The transmute is sound because:
|
|
||||||
// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`, so
|
|
||||||
// `&'ir GhostCell<'id, T>` and `&'ir T` have identical layout.
|
|
||||||
// - `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::MaybeThunkRef` becomes `&'ir GhostCell<'id, MaybeThunk>`
|
|
||||||
// vs `&'ir MaybeThunk`
|
|
||||||
// - `R::Ref<Ir<'ir, R>>` (used in `ConcatStrings::parts`) reduces
|
|
||||||
// to the same case as `R::IrRef`
|
|
||||||
// - Therefore `IrRef<'id, 'ir>` and `RawIrRef<'ir>` are both
|
|
||||||
// pointer-sized references with the same layout.
|
|
||||||
//
|
|
||||||
// Consuming the `GhostToken` guarantees no `borrow_mut` calls can
|
|
||||||
// occur afterwards, so the shared `&Ir` references reachable from a
|
|
||||||
// `RawIrRef<'ir>` can never alias with mutable references.
|
|
||||||
unsafe { std::mem::transmute::<GhostRoIrRef<'id, 'ir>, RawIrRef<'ir>>(this) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(transparent)]
|
|
||||||
pub struct GhostRoCell<'id, T: ?Sized>(GhostCell<'id, T>);
|
|
||||||
|
|
||||||
impl<'id, T> From<GhostCell<'id, T>> for GhostRoCell<'id, T> {
|
|
||||||
fn from(value: GhostCell<'id, T>) -> Self {
|
|
||||||
Self(value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'id, T: ?Sized> From<&GhostCell<'id, T>> for &GhostRoCell<'id, T> {
|
|
||||||
fn from(value: &GhostCell<'id, T>) -> Self {
|
|
||||||
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`
|
|
||||||
// TODO: document mutability
|
|
||||||
unsafe { std::mem::transmute(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'id, T: ?Sized> From<&T> for &GhostRoCell<'id, T> {
|
|
||||||
fn from(value: &T) -> Self {
|
|
||||||
// SAFETY: `GhostRoCell` is `#[repr(transparent)]` over `GhostCell`,
|
|
||||||
// which is `#[repr(transparent)]` over `T`
|
|
||||||
// TODO: document mutability
|
|
||||||
unsafe { std::mem::transmute(value) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'id, T: ?Sized> GhostRoCell<'id, T> {
|
|
||||||
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a T {
|
|
||||||
self.0.borrow(token)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug, Clone, Copy)]
|
|
||||||
pub enum MaybeThunk {
|
|
||||||
Int(i64),
|
|
||||||
Float(f64),
|
|
||||||
Bool(bool),
|
|
||||||
Null,
|
|
||||||
Str(StringId),
|
|
||||||
Path(StringId),
|
|
||||||
Thunk(ThunkId),
|
|
||||||
Arg { layer: u8 },
|
|
||||||
Builtin(BuiltinId),
|
|
||||||
BuiltinConst(StringId),
|
|
||||||
Builtins,
|
|
||||||
ReplBinding(StringId),
|
|
||||||
ScopedImportBinding(StringId),
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait Ref<'ir> {
|
|
||||||
type Ref<T>
|
|
||||||
where
|
|
||||||
T: 'ir;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub trait RefExt<'ir>: Ref<'ir> {
|
|
||||||
type Ir;
|
|
||||||
type IrRef;
|
|
||||||
type MaybeThunkRef;
|
|
||||||
}
|
|
||||||
impl<'ir, T: Ref<'ir> + 'ir> RefExt<'ir> for T {
|
|
||||||
type Ir = Ir<'ir, Self>;
|
|
||||||
type IrRef = Self::Ref<Self::Ir>;
|
|
||||||
type MaybeThunkRef = Self::Ref<MaybeThunk>;
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct GhostRef<'id, 'ir>(PhantomData<&'ir GhostCell<'id, ()>>);
|
|
||||||
pub struct GhostRoRef<'id, 'ir>(PhantomData<&'ir GhostRoCell<'id, ()>>);
|
|
||||||
pub struct RawRef<'ir>(PhantomData<&'ir ()>);
|
|
||||||
|
|
||||||
impl<'id, 'ir> Ref<'ir> for GhostRef<'id, 'ir> {
|
|
||||||
type Ref<T: 'ir> = &'ir GhostCell<'id, T>;
|
|
||||||
}
|
|
||||||
impl<'id, 'ir> Ref<'ir> for GhostRoRef<'id, 'ir> {
|
|
||||||
type Ref<T: 'ir> = &'ir GhostRoCell<'id, T>;
|
|
||||||
}
|
|
||||||
impl<'ir> Ref<'ir> for RawRef<'ir> {
|
|
||||||
type Ref<T: 'ir> = &'ir T;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(C)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Ir<'ir, R: RefExt<'ir> + ?Sized + 'ir> {
|
|
||||||
Int(i64),
|
|
||||||
Float(f64),
|
|
||||||
Bool(bool),
|
|
||||||
Null,
|
|
||||||
Str(StringId),
|
|
||||||
Path(R::IrRef),
|
|
||||||
AttrSet {
|
|
||||||
stcs: HashMap<'ir, StringId, (R::MaybeThunkRef, TextRange)>,
|
|
||||||
dyns: Vec<'ir, (R::IrRef, R::MaybeThunkRef, TextRange)>,
|
|
||||||
},
|
|
||||||
List {
|
|
||||||
items: Vec<'ir, R::MaybeThunkRef>,
|
|
||||||
},
|
|
||||||
ConcatStrings {
|
|
||||||
parts: Vec<'ir, R::Ref<Ir<'ir, R>>>,
|
|
||||||
force_string: bool,
|
|
||||||
},
|
|
||||||
|
|
||||||
// OPs
|
|
||||||
UnOp {
|
|
||||||
rhs: R::IrRef,
|
|
||||||
kind: UnOpKind,
|
|
||||||
},
|
|
||||||
BinOp {
|
|
||||||
lhs: R::IrRef,
|
|
||||||
rhs: R::IrRef,
|
|
||||||
kind: BinOpKind,
|
|
||||||
},
|
|
||||||
HasAttr {
|
|
||||||
lhs: R::IrRef,
|
|
||||||
rhs: Vec<'ir, Attr<R::IrRef>>,
|
|
||||||
},
|
|
||||||
Select {
|
|
||||||
expr: R::IrRef,
|
|
||||||
attrpath: Vec<'ir, Attr<R::IrRef>>,
|
|
||||||
default: Option<R::IrRef>,
|
|
||||||
span: TextRange,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Conditionals
|
|
||||||
If {
|
|
||||||
cond: R::IrRef,
|
|
||||||
consq: R::IrRef,
|
|
||||||
alter: R::IrRef,
|
|
||||||
},
|
|
||||||
Assert {
|
|
||||||
assertion: R::IrRef,
|
|
||||||
expr: R::IrRef,
|
|
||||||
assertion_raw: String,
|
|
||||||
span: TextRange,
|
|
||||||
},
|
|
||||||
|
|
||||||
WithLookup {
|
|
||||||
sym: StringId,
|
|
||||||
namespaces: Vec<'ir, R::MaybeThunkRef>,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Function related
|
|
||||||
Func {
|
|
||||||
body: R::IrRef,
|
|
||||||
param: Option<Param<'ir>>,
|
|
||||||
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
|
|
||||||
},
|
|
||||||
Arg {
|
|
||||||
layer: u8,
|
|
||||||
},
|
|
||||||
Call {
|
|
||||||
func: R::IrRef,
|
|
||||||
arg: R::MaybeThunkRef,
|
|
||||||
span: TextRange,
|
|
||||||
},
|
|
||||||
|
|
||||||
// Builtins
|
|
||||||
Builtins,
|
|
||||||
Builtin(BuiltinId),
|
|
||||||
BuiltinConst(StringId),
|
|
||||||
|
|
||||||
// Misc
|
|
||||||
TopLevel {
|
|
||||||
body: R::IrRef,
|
|
||||||
thunks: Vec<'ir, (ThunkId, R::IrRef)>,
|
|
||||||
},
|
|
||||||
MaybeThunk(R::MaybeThunkRef),
|
|
||||||
ReplBinding(StringId),
|
|
||||||
ScopedImportBinding(StringId),
|
|
||||||
}
|
|
||||||
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct ThunkId(pub usize);
|
|
||||||
|
|
||||||
#[repr(transparent)]
|
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
|
||||||
pub struct SpanId(pub u32);
|
|
||||||
|
|
||||||
/// Represents a key in an attribute path.
|
|
||||||
#[allow(unused)]
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub enum Attr<Ref> {
|
|
||||||
/// A dynamic attribute key, which is an expression that must evaluate to a string.
|
|
||||||
/// Example: `attrs.${key}`
|
|
||||||
Dynamic(Ref, TextRange),
|
|
||||||
/// A static attribute key.
|
|
||||||
/// Example: `attrs.key`
|
|
||||||
Str(StringId, TextRange),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The kinds of binary operations supported in Nix.
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
|
||||||
pub enum BinOpKind {
|
|
||||||
// Arithmetic
|
|
||||||
Add,
|
|
||||||
Sub,
|
|
||||||
Div,
|
|
||||||
Mul,
|
|
||||||
|
|
||||||
// Comparison
|
|
||||||
Eq,
|
|
||||||
Neq,
|
|
||||||
Lt,
|
|
||||||
Gt,
|
|
||||||
Leq,
|
|
||||||
Geq,
|
|
||||||
|
|
||||||
// Logical
|
|
||||||
And,
|
|
||||||
Or,
|
|
||||||
Impl,
|
|
||||||
|
|
||||||
// Set/String/Path operations
|
|
||||||
Con, // List concatenation (`++`)
|
|
||||||
Upd, // AttrSet update (`//`)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// The kinds of unary operations.
|
|
||||||
#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)]
|
|
||||||
pub enum UnOpKind {
|
|
||||||
Neg, // Negation (`-`)
|
|
||||||
Not, // Logical not (`!`)
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<ast::UnaryOpKind> for UnOpKind {
|
|
||||||
fn from(value: ast::UnaryOpKind) -> Self {
|
|
||||||
match value {
|
|
||||||
ast::UnaryOpKind::Invert => UnOpKind::Not,
|
|
||||||
ast::UnaryOpKind::Negate => UnOpKind::Neg,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Describes the parameters of a function.
|
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Param<'ir> {
|
|
||||||
pub required: Vec<'ir, (StringId, TextRange)>,
|
|
||||||
pub optional: Vec<'ir, (StringId, TextRange)>,
|
|
||||||
pub ellipsis: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn new_global_env(
|
|
||||||
strings: &mut DefaultStringInterner,
|
|
||||||
) -> hashbrown::HashMap<StringId, MaybeThunk> {
|
|
||||||
let mut global_env = hashbrown::HashMap::new();
|
|
||||||
let builtins_sym = StringId(strings.get_or_intern("builtins"));
|
|
||||||
global_env.insert(builtins_sym, MaybeThunk::Builtins);
|
|
||||||
|
|
||||||
for (idx, &(name, _)) in BUILTINS.iter().enumerate() {
|
|
||||||
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
|
|
||||||
let name = StringId(strings.get_or_intern(name));
|
|
||||||
global_env.insert(name, MaybeThunk::Builtin(id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let consts = [
|
|
||||||
(
|
|
||||||
"__currentSystem",
|
|
||||||
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("currentSystem"))),
|
|
||||||
),
|
|
||||||
("__langVersion", MaybeThunk::Int(6)),
|
|
||||||
(
|
|
||||||
"__nixVersion",
|
|
||||||
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixVersion"))),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"__storeDir",
|
|
||||||
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("storeDir"))),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"__nixPath",
|
|
||||||
MaybeThunk::BuiltinConst(StringId(strings.get_or_intern("nixPath"))),
|
|
||||||
),
|
|
||||||
("null", MaybeThunk::Null),
|
|
||||||
("true", MaybeThunk::Bool(true)),
|
|
||||||
("false", MaybeThunk::Bool(false)),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (name, ir) in consts {
|
|
||||||
let name = StringId(strings.get_or_intern(name));
|
|
||||||
global_env.insert(name, ir);
|
|
||||||
}
|
|
||||||
|
|
||||||
global_env
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
[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" }
|
|
||||||
@@ -1,447 +0,0 @@
|
|||||||
//! `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)
|
|
||||||
}
|
|
||||||
@@ -1,362 +0,0 @@
|
|||||||
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<'_>>()
|
|
||||||
}
|
|
||||||
@@ -1,51 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
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:?}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
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(())
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,24 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix-vm"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[features]
|
|
||||||
tailcall = []
|
|
||||||
|
|
||||||
[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"
|
|
||||||
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" }
|
|
||||||
@@ -1,304 +0,0 @@
|
|||||||
#![cfg(feature = "tailcall")]
|
|
||||||
|
|
||||||
use gc_arena::Mutation;
|
|
||||||
|
|
||||||
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(
|
|
||||||
&mut Vm<'gc>,
|
|
||||||
&Mutation<'gc>,
|
|
||||||
&mut C,
|
|
||||||
&[u8],
|
|
||||||
&DispatchTable<'gc, C>,
|
|
||||||
u32,
|
|
||||||
u32,
|
|
||||||
) -> TailResult;
|
|
||||||
|
|
||||||
pub(crate) struct DispatchTable<'gc, C: VmRuntimeCtx>(pub(crate) [OpFn<'gc, C>; 256]);
|
|
||||||
|
|
||||||
extern "rust-preserve-none" fn op_illegal<'gc, C: VmRuntimeCtx>(
|
|
||||||
_vm: &mut Vm<'gc>,
|
|
||||||
_mc: &Mutation<'gc>,
|
|
||||||
_ctx: &mut C,
|
|
||||||
_bc: &[u8],
|
|
||||||
_table: &DispatchTable<'gc, C>,
|
|
||||||
pc: u32,
|
|
||||||
_fuel: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
panic!("illegal opcode at pc = {pc}");
|
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
if $fuel == 0 {
|
|
||||||
return TailResult::YieldFuel(new_pc);
|
|
||||||
}
|
|
||||||
let next_op = $bc[new_pc as usize] as usize;
|
|
||||||
become $table.0[next_op]($vm, $mc, $ctx, $bc, $table, new_pc, $fuel - 1)
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! tail_fn {
|
|
||||||
($name:ident, ()) => {
|
|
||||||
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
|
|
||||||
vm: &mut Vm<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
ctx: &mut C,
|
|
||||||
bc: &[u8],
|
|
||||||
table: &DispatchTable<'gc, C>,
|
|
||||||
pc: u32,
|
|
||||||
fuel: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
let result = vm.$name();
|
|
||||||
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($name:ident, (reader)) => {
|
|
||||||
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
|
|
||||||
vm: &mut Vm<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
ctx: &mut C,
|
|
||||||
bc: &[u8],
|
|
||||||
table: &DispatchTable<'gc, C>,
|
|
||||||
pc: u32,
|
|
||||||
fuel: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
|
|
||||||
let result = vm.$name(&mut reader);
|
|
||||||
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($name:ident, (reader, mc)) => {
|
|
||||||
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
|
|
||||||
vm: &mut Vm<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
ctx: &mut C,
|
|
||||||
bc: &[u8],
|
|
||||||
table: &DispatchTable<'gc, C>,
|
|
||||||
pc: u32,
|
|
||||||
fuel: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
|
|
||||||
let result = vm.$name(&mut reader, mc);
|
|
||||||
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($name:ident, (ctx, reader, mc)) => {
|
|
||||||
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
|
|
||||||
vm: &mut Vm<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
ctx: &mut C,
|
|
||||||
bc: &[u8],
|
|
||||||
table: &DispatchTable<'gc, C>,
|
|
||||||
pc: u32,
|
|
||||||
fuel: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
let mut reader = BytecodeReader::from_after_op(bc, pc as usize);
|
|
||||||
let result = vm.$name(ctx, &mut reader, mc);
|
|
||||||
tail_dispatch_after!(result, reader.pc() as u32, vm, mc, ctx, bc, table, fuel)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
($name:ident, (ctx)) => {
|
|
||||||
extern "rust-preserve-none" fn $name<'gc, C: VmRuntimeCtx>(
|
|
||||||
vm: &mut Vm<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
ctx: &mut C,
|
|
||||||
bc: &[u8],
|
|
||||||
table: &DispatchTable<'gc, C>,
|
|
||||||
pc: u32,
|
|
||||||
fuel: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
let result = vm.$name(ctx);
|
|
||||||
tail_dispatch_after!(result, pc + 1, vm, mc, ctx, bc, table, fuel)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
tail_fn!(op_push_smi, (reader));
|
|
||||||
tail_fn!(op_push_bigint, (reader, mc));
|
|
||||||
tail_fn!(op_push_float, (reader));
|
|
||||||
tail_fn!(op_push_string, (reader));
|
|
||||||
tail_fn!(op_push_null, ());
|
|
||||||
tail_fn!(op_push_true, ());
|
|
||||||
tail_fn!(op_push_false, ());
|
|
||||||
|
|
||||||
tail_fn!(op_load_local, (reader));
|
|
||||||
tail_fn!(op_load_outer, (reader));
|
|
||||||
tail_fn!(op_store_local, (reader, mc));
|
|
||||||
tail_fn!(op_alloc_locals, (reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_make_thunk, (reader, mc));
|
|
||||||
tail_fn!(op_make_closure, (reader, mc));
|
|
||||||
tail_fn!(op_make_pattern_closure, (reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_call, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_dispatch_primop, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_return, (ctx, reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_make_attrs, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_make_empty_attrs, ());
|
|
||||||
tail_fn!(op_select_static, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_select_dynamic, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_has_attr_path_static, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_has_attr_path_dynamic, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_jump_if_select_failed, (reader));
|
|
||||||
tail_fn!(op_jump_if_select_succeeded, (reader));
|
|
||||||
tail_fn!(op_has_attr_static, (reader, mc));
|
|
||||||
tail_fn!(op_has_attr_dynamic, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_has_attr_resolve, ());
|
|
||||||
|
|
||||||
tail_fn!(op_make_list, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_make_empty_list, ());
|
|
||||||
|
|
||||||
tail_fn!(op_add, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_sub, (reader, mc));
|
|
||||||
tail_fn!(op_mul, (reader, mc));
|
|
||||||
tail_fn!(op_div, (reader, mc));
|
|
||||||
tail_fn!(op_eq, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_neq, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_lt, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_gt, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_leq, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_geq, (ctx, reader, mc));
|
|
||||||
tail_fn!(op_concat, (reader, mc));
|
|
||||||
tail_fn!(op_update, (reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_neg, (reader, mc));
|
|
||||||
tail_fn!(op_not, (reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_jump_if_false, (reader, mc));
|
|
||||||
tail_fn!(op_jump_if_true, (reader, mc));
|
|
||||||
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, reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_assert, (ctx, reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_lookup_with, (ctx, reader, mc));
|
|
||||||
|
|
||||||
tail_fn!(op_load_builtins, ());
|
|
||||||
tail_fn!(op_load_builtin, (reader));
|
|
||||||
|
|
||||||
tail_fn!(op_load_repl_binding, (reader));
|
|
||||||
tail_fn!(op_load_scoped_binding, (ctx, reader, mc));
|
|
||||||
|
|
||||||
macro_rules! table {
|
|
||||||
($($variant:ident => $fn:ident),* $(,)?) => {
|
|
||||||
impl<'gc, C: VmRuntimeCtx> DispatchTable<'gc, C> {
|
|
||||||
pub(crate) const NEW: Self = {
|
|
||||||
let mut arr: [OpFn<'gc, C>; 256] = [op_illegal; 256];
|
|
||||||
$( arr[fix_codegen::Op::$variant as usize] = $fn; )*
|
|
||||||
DispatchTable(arr)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Exhaustiveness check: fails to compile if `fix_codegen::Op` gains,
|
|
||||||
// loses, or renames a variant that isn't wired up above.
|
|
||||||
#[allow(dead_code)]
|
|
||||||
const _: fn(fix_codegen::Op) = |op| match op {
|
|
||||||
$( fix_codegen::Op::$variant => (), )*
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
table! {
|
|
||||||
PushSmi => op_push_smi,
|
|
||||||
PushBigInt => op_push_bigint,
|
|
||||||
PushFloat => op_push_float,
|
|
||||||
PushString => op_push_string,
|
|
||||||
PushNull => op_push_null,
|
|
||||||
PushTrue => op_push_true,
|
|
||||||
PushFalse => op_push_false,
|
|
||||||
|
|
||||||
LoadLocal => op_load_local,
|
|
||||||
LoadOuter => op_load_outer,
|
|
||||||
StoreLocal => op_store_local,
|
|
||||||
AllocLocals => op_alloc_locals,
|
|
||||||
|
|
||||||
MakeThunk => op_make_thunk,
|
|
||||||
MakeClosure => op_make_closure,
|
|
||||||
MakePatternClosure => op_make_pattern_closure,
|
|
||||||
|
|
||||||
Call => op_call,
|
|
||||||
DispatchPrimOp => op_dispatch_primop,
|
|
||||||
Return => op_return,
|
|
||||||
|
|
||||||
MakeAttrs => op_make_attrs,
|
|
||||||
MakeEmptyAttrs => op_make_empty_attrs,
|
|
||||||
SelectStatic => op_select_static,
|
|
||||||
SelectDynamic => op_select_dynamic,
|
|
||||||
HasAttrPathStatic => op_has_attr_path_static,
|
|
||||||
HasAttrPathDynamic => op_has_attr_path_dynamic,
|
|
||||||
HasAttrStatic => op_has_attr_static,
|
|
||||||
HasAttrDynamic => op_has_attr_dynamic,
|
|
||||||
HasAttrResolve => op_has_attr_resolve,
|
|
||||||
JumpIfSelectSucceeded => op_jump_if_select_succeeded,
|
|
||||||
JumpIfSelectFailed => op_jump_if_select_failed,
|
|
||||||
|
|
||||||
MakeList => op_make_list,
|
|
||||||
MakeEmptyList => op_make_empty_list,
|
|
||||||
|
|
||||||
OpAdd => op_add,
|
|
||||||
OpSub => op_sub,
|
|
||||||
OpMul => op_mul,
|
|
||||||
OpDiv => op_div,
|
|
||||||
OpEq => op_eq,
|
|
||||||
OpNeq => op_neq,
|
|
||||||
OpLt => op_lt,
|
|
||||||
OpGt => op_gt,
|
|
||||||
OpLeq => op_leq,
|
|
||||||
OpGeq => op_geq,
|
|
||||||
OpConcat => op_concat,
|
|
||||||
OpUpdate => op_update,
|
|
||||||
|
|
||||||
OpNeg => op_neg,
|
|
||||||
OpNot => op_not,
|
|
||||||
|
|
||||||
JumpIfFalse => op_jump_if_false,
|
|
||||||
JumpIfTrue => op_jump_if_true,
|
|
||||||
Jump => op_jump,
|
|
||||||
|
|
||||||
CoerceToString => op_coerce_to_string,
|
|
||||||
|
|
||||||
ConcatStrings => op_concat_strings,
|
|
||||||
ResolvePath => op_resolve_path,
|
|
||||||
|
|
||||||
Assert => op_assert,
|
|
||||||
|
|
||||||
LookupWith => op_lookup_with,
|
|
||||||
|
|
||||||
LoadBuiltins => op_load_builtins,
|
|
||||||
LoadBuiltin => op_load_builtin,
|
|
||||||
|
|
||||||
LoadReplBinding => op_load_repl_binding,
|
|
||||||
LoadScopedBinding => op_load_scoped_binding,
|
|
||||||
|
|
||||||
Illegal => op_illegal,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn run_tailcall<'gc, C: VmRuntimeCtx>(
|
|
||||||
vm: &mut Vm<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
ctx: &mut C,
|
|
||||||
bc: &[u8],
|
|
||||||
pc: u32,
|
|
||||||
) -> TailResult {
|
|
||||||
let table = &DispatchTable::<'gc, C>::NEW;
|
|
||||||
let op = bc[pc as usize] as usize;
|
|
||||||
table.0[op](vm, mc, ctx, bc, table, pc, Vm::DEFAULT_FUEL_AMOUNT)
|
|
||||||
}
|
|
||||||
@@ -1,297 +0,0 @@
|
|||||||
use std::cmp::Ordering;
|
|
||||||
|
|
||||||
use fix_abstract_vm::*;
|
|
||||||
use gc_arena::{Gc, Mutation, RefLock};
|
|
||||||
|
|
||||||
use crate::{BytecodeReader, NixNum, Step, VmError, VmRuntimeCtx};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_add(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
// 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(());
|
|
||||||
}
|
|
||||||
let res = numeric_binop(lhs, rhs, mc, i64::wrapping_add, |a, b| a + b);
|
|
||||||
match res {
|
|
||||||
Ok(val) => {
|
|
||||||
self.push(val);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
Err(e) => self.finish_vm_err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_sub(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
|
|
||||||
self.op_arith(reader, mc, i64::wrapping_sub, |a, b| a - b)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_mul(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
|
|
||||||
self.op_arith(reader, mc, i64::wrapping_mul, |a, b| a * b)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn op_arith(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
int_op: fn(i64, i64) -> i64,
|
|
||||||
float_op: fn(f64, f64) -> f64,
|
|
||||||
) -> Step {
|
|
||||||
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
let res = numeric_binop(lhs, rhs, mc, int_op, float_op);
|
|
||||||
match res {
|
|
||||||
Ok(val) => {
|
|
||||||
self.push(val);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
Err(e) => self.finish_vm_err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_div(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
|
|
||||||
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
match (get_num(lhs), get_num(rhs)) {
|
|
||||||
(_, Some(NixNum::Int(0))) | (_, Some(NixNum::Float(0.))) => {
|
|
||||||
return self.finish_vm_err(VmError::Uncatchable(fix_error::Error::eval_error(
|
|
||||||
"division by zero",
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
let res = numeric_binop(lhs, rhs, mc, |a, b| a / b, |a, b| a / b);
|
|
||||||
match res {
|
|
||||||
Ok(val) => {
|
|
||||||
self.push(val);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
Err(e) => self.finish_vm_err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_eq(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
fix_primops::start_eq(self, ctx, reader, mc, lhs, rhs, false)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_neq(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
fix_primops::start_eq(self, ctx, reader, mc, lhs, rhs, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_lt(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
self.compare_values(ctx, reader, mc, Ordering::is_lt)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_gt(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
self.compare_values(ctx, reader, mc, Ordering::is_gt)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_leq(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
self.compare_values(ctx, reader, mc, Ordering::is_le)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_geq(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
self.compare_values(ctx, reader, mc, Ordering::is_ge)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_values(
|
|
||||||
&mut self,
|
|
||||||
ctx: &impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
pred: fn(Ordering) -> bool,
|
|
||||||
) -> Step {
|
|
||||||
let (lhs, rhs) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
match self.compare_values_inner(ctx, pred, lhs, rhs) {
|
|
||||||
Ok(()) => Step::Continue(()),
|
|
||||||
Err(e) => self.finish_vm_err(e),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_concat(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let (l, r) = self.force_and_retry::<(Gc<List>, Gc<List>)>(reader, mc)?;
|
|
||||||
let mut items = smallvec::SmallVec::new();
|
|
||||||
items.extend_from_slice(&l.inner.borrow());
|
|
||||||
items.extend_from_slice(&r.inner.borrow());
|
|
||||||
self.push(Value::new_gc(Gc::new(
|
|
||||||
mc,
|
|
||||||
crate::List {
|
|
||||||
inner: RefLock::new(items),
|
|
||||||
},
|
|
||||||
)));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_update(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let (l, r) = self.force_and_retry::<(Gc<AttrSet>, Gc<AttrSet>)>(reader, mc)?;
|
|
||||||
self.push(Value::new_gc(l.merge(&r, mc)));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_neg(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
|
|
||||||
let rhs = self.force_and_retry::<NixNum>(reader, mc)?;
|
|
||||||
match rhs {
|
|
||||||
NixNum::Int(int) => self.push(Value::make_int(-int, mc)),
|
|
||||||
NixNum::Float(float) => self.push(Value::new_float(-float)),
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_not(&mut self, reader: &mut BytecodeReader<'_>, mc: &Mutation<'gc>) -> Step {
|
|
||||||
let rhs = self.force_and_retry::<bool>(reader, mc)?;
|
|
||||||
self.push(Value::new_inline(!rhs));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn compare_values_inner(
|
|
||||||
&mut self,
|
|
||||||
ctx: &impl VmRuntimeCtx,
|
|
||||||
pred: fn(Ordering) -> bool,
|
|
||||||
lhs: StrictValue<'gc>,
|
|
||||||
rhs: StrictValue<'gc>,
|
|
||||||
) -> crate::VmResult<()> {
|
|
||||||
if let (Some(a), Some(b)) = (get_num(lhs), get_num(rhs)) {
|
|
||||||
let ord = match (a, b) {
|
|
||||||
(NixNum::Int(a), NixNum::Int(b)) => a.cmp(&b),
|
|
||||||
(NixNum::Float(a), NixNum::Float(b)) => a.partial_cmp(&b).unwrap_or(Ordering::Less),
|
|
||||||
(NixNum::Int(a), NixNum::Float(b)) => {
|
|
||||||
(a as f64).partial_cmp(&b).unwrap_or(Ordering::Less)
|
|
||||||
}
|
|
||||||
(NixNum::Float(a), NixNum::Int(b)) => {
|
|
||||||
a.partial_cmp(&(b as f64)).unwrap_or(Ordering::Less)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
self.push(Value::new_inline(pred(ord)));
|
|
||||||
return Ok(());
|
|
||||||
}
|
|
||||||
if let (Some(a), Some(b)) = (ctx.get_string(lhs), ctx.get_string(rhs)) {
|
|
||||||
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"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn get_num(val: StrictValue<'_>) -> Option<NixNum> {
|
|
||||||
if let Some(i) = val.as_inline::<i32>() {
|
|
||||||
Some(NixNum::Int(i as i64))
|
|
||||||
} else if let Some(gc_i) = val.as_gc::<i64>() {
|
|
||||||
Some(NixNum::Int(*gc_i))
|
|
||||||
} else {
|
|
||||||
val.as_float().map(NixNum::Float)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn numeric_binop<'gc>(
|
|
||||||
lhs: StrictValue<'gc>,
|
|
||||||
rhs: StrictValue<'gc>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
int_op: fn(i64, i64) -> i64,
|
|
||||||
float_op: fn(f64, f64) -> f64,
|
|
||||||
) -> crate::VmResult<Value<'gc>> {
|
|
||||||
match (get_num(lhs), get_num(rhs)) {
|
|
||||||
(Some(NixNum::Int(a)), Some(NixNum::Int(b))) => Ok(Value::make_int(int_op(a, b), mc)),
|
|
||||||
(Some(NixNum::Float(a)), Some(NixNum::Float(b))) => Ok(Value::new_float(float_op(a, b))),
|
|
||||||
(Some(NixNum::Int(a)), Some(NixNum::Float(b))) => {
|
|
||||||
Ok(Value::new_float(float_op(a as f64, b)))
|
|
||||||
}
|
|
||||||
(Some(NixNum::Float(a)), Some(NixNum::Int(b))) => {
|
|
||||||
Ok(Value::new_float(float_op(a, b as f64)))
|
|
||||||
}
|
|
||||||
_ => Err(crate::vm_err(format!(
|
|
||||||
"cannot perform arithmetic on non-numbers: {:?}",
|
|
||||||
(lhs.ty(), rhs.ty())
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
use fix_abstract_vm::{resolve_operand, *};
|
|
||||||
use fix_builtins::PrimOpPhase;
|
|
||||||
use fix_error::Error;
|
|
||||||
use gc_arena::{Gc, Mutation, RefLock};
|
|
||||||
|
|
||||||
use crate::{
|
|
||||||
BytecodeReader, CallFrame, Closure, Env, ForceMode, Step, ThunkState, VmRuntimeCtx,
|
|
||||||
VmRuntimeCtxExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn call(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
arg: Value<'gc>,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
let func = self.force_and_retry::<StrictValue>(reader, mc)?;
|
|
||||||
if self.call_depth > 10000 {
|
|
||||||
return self.finish_err(Error::eval_error("stack overflow; max-call-depth exceeded"));
|
|
||||||
}
|
|
||||||
self.call_depth += 1;
|
|
||||||
if let Some(closure) = func.as_gc::<Closure>() {
|
|
||||||
if closure.pattern.is_some() {
|
|
||||||
// FIXME: better DX...
|
|
||||||
self.push(func.relax());
|
|
||||||
self.push(arg);
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
pc: resume_pc,
|
|
||||||
thunk: None,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
reader.set_pc(PrimOpPhase::CallPattern.ip() as usize);
|
|
||||||
return Step::Continue(());
|
|
||||||
}
|
|
||||||
|
|
||||||
let ip = closure.ip;
|
|
||||||
let n_locals = closure.n_locals;
|
|
||||||
let env = closure.env;
|
|
||||||
let new_env = Gc::new(mc, RefLock::new(Env::with_arg(arg, n_locals, env)));
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
pc: resume_pc,
|
|
||||||
thunk: None,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
reader.set_pc(ip as usize);
|
|
||||||
self.env = new_env;
|
|
||||||
} else if let Some(primop) = func.as_inline::<PrimOp>() {
|
|
||||||
if primop.arity == 1 {
|
|
||||||
self.push(arg);
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
pc: resume_pc,
|
|
||||||
thunk: None,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
reader.set_pc(primop.dispatch_ip as usize)
|
|
||||||
} else {
|
|
||||||
let app = PrimOpApp {
|
|
||||||
primop,
|
|
||||||
arity: primop.arity - 1,
|
|
||||||
args: [arg, Value::default(), Value::default()],
|
|
||||||
};
|
|
||||||
self.push(Value::new_gc(Gc::new(mc, app)));
|
|
||||||
}
|
|
||||||
} else if let Some(app) = func.as_gc::<PrimOpApp>() {
|
|
||||||
if app.arity == 1 {
|
|
||||||
for i in 0..app.primop.arity - 1 {
|
|
||||||
self.push(app.args[i as usize]);
|
|
||||||
}
|
|
||||||
self.push(arg);
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
pc: resume_pc,
|
|
||||||
thunk: None,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
reader.set_pc(app.primop.dispatch_ip as usize)
|
|
||||||
} else {
|
|
||||||
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>()
|
|
||||||
&& let Some(functor) = attrs.lookup(self.functor_sym)
|
|
||||||
{
|
|
||||||
// f arg => (f.__functor f) arg
|
|
||||||
//
|
|
||||||
// Stage the work for `CallFunctor1` so retries during force are
|
|
||||||
// safe: the stack invariant `[..., orig_arg, self, functor]`
|
|
||||||
// holds every time control re-enters phase 1.
|
|
||||||
self.call_depth -= 1;
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
pc: resume_pc,
|
|
||||||
thunk: None,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
self.push(arg);
|
|
||||||
self.push(func.relax());
|
|
||||||
self.push(functor);
|
|
||||||
reader.set_pc(PrimOpPhase::CallFunctor1.ip() as usize);
|
|
||||||
return Step::Continue(());
|
|
||||||
} else {
|
|
||||||
return self.finish_err(Error::eval_error(format!(
|
|
||||||
"attempt to call something which is not a function but {}",
|
|
||||||
func.ty()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_call(
|
|
||||||
&mut self,
|
|
||||||
ctx: &impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let arg = resolve_operand(&reader.read_operand_data(ctx), mc, self);
|
|
||||||
let pc = reader.pc();
|
|
||||||
self.call(reader, mc, arg, pc)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_return(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let val = self.force_and_retry::<StrictValue>(reader, mc)?;
|
|
||||||
let Some(CallFrame {
|
|
||||||
pc: ret_pc,
|
|
||||||
thunk,
|
|
||||||
env,
|
|
||||||
}) = self.call_stack.pop()
|
|
||||||
else {
|
|
||||||
match self.force_mode {
|
|
||||||
ForceMode::AsIs => return self.finish_ok(ctx.convert_value(val.relax())),
|
|
||||||
ForceMode::Shallow => {
|
|
||||||
self.push(val.relax());
|
|
||||||
reader.set_pc(PrimOpPhase::ForceResultShallow.ip() as usize);
|
|
||||||
return Step::Continue(());
|
|
||||||
}
|
|
||||||
ForceMode::Deep => {
|
|
||||||
self.push(val.relax());
|
|
||||||
self.push(val.relax());
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
pc: PrimOpPhase::ForceResultDeepFinish.ip() as usize,
|
|
||||||
thunk: None,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
self.call_depth += 1;
|
|
||||||
reader.set_pc(PrimOpPhase::DeepSeq.ip() as usize);
|
|
||||||
return Step::Continue(());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
reader.set_pc(ret_pc);
|
|
||||||
if let Some(outer_thunk) = thunk {
|
|
||||||
*outer_thunk.borrow_mut(mc) = ThunkState::Evaluated(val);
|
|
||||||
} else {
|
|
||||||
self.call_depth -= 1;
|
|
||||||
self.push(val.relax())
|
|
||||||
}
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
use gc_arena::{Gc, Mutation, RefLock};
|
|
||||||
|
|
||||||
use crate::{BytecodeReader, Step, ThunkState, Value};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_thunk(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let entry_point = reader.read_u32();
|
|
||||||
let thunk = Gc::new(
|
|
||||||
mc,
|
|
||||||
RefLock::new(ThunkState::Pending {
|
|
||||||
ip: entry_point as usize,
|
|
||||||
env: self.env,
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
self.push(Value::new_gc(thunk));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_closure(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let entry_point = reader.read_u32();
|
|
||||||
let n_locals = reader.read_u32();
|
|
||||||
let closure = Gc::new(
|
|
||||||
mc,
|
|
||||||
crate::Closure {
|
|
||||||
ip: entry_point,
|
|
||||||
n_locals,
|
|
||||||
env: self.env,
|
|
||||||
pattern: None,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.push(Value::new_gc(closure));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_pattern_closure(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let entry_point = reader.read_u32();
|
|
||||||
let n_locals = reader.read_u32();
|
|
||||||
let req_count = reader.read_u16() as usize;
|
|
||||||
let opt_count = reader.read_u16() as usize;
|
|
||||||
let has_ellipsis = reader.read_u8() != 0;
|
|
||||||
|
|
||||||
let mut required = smallvec::SmallVec::new();
|
|
||||||
for _ in 0..req_count {
|
|
||||||
required.push(reader.read_string_id());
|
|
||||||
}
|
|
||||||
let mut optional = smallvec::SmallVec::new();
|
|
||||||
for _ in 0..opt_count {
|
|
||||||
optional.push(reader.read_string_id());
|
|
||||||
}
|
|
||||||
let total = req_count + opt_count;
|
|
||||||
let mut param_spans = Vec::with_capacity(total);
|
|
||||||
for _ in 0..total {
|
|
||||||
let name = reader.read_string_id();
|
|
||||||
let span_id = reader.read_u32();
|
|
||||||
param_spans.push((name, span_id));
|
|
||||||
}
|
|
||||||
|
|
||||||
let pattern = Gc::new(
|
|
||||||
mc,
|
|
||||||
crate::PatternInfo {
|
|
||||||
required,
|
|
||||||
optional,
|
|
||||||
ellipsis: has_ellipsis,
|
|
||||||
param_spans: param_spans.into_boxed_slice(),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let closure = Gc::new(
|
|
||||||
mc,
|
|
||||||
crate::Closure {
|
|
||||||
ip: entry_point,
|
|
||||||
n_locals,
|
|
||||||
env: self.env,
|
|
||||||
pattern: Some(pattern),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.push(Value::new_gc(closure));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,334 +0,0 @@
|
|||||||
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::{
|
|
||||||
AttrSet, BytecodeReader, List, Step, StrictValue, Value, VmRuntimeCtx, VmRuntimeCtxExt,
|
|
||||||
};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_attrs(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let static_count = reader.read_u32() as usize;
|
|
||||||
let dynamic_count = reader.read_u32() as usize;
|
|
||||||
|
|
||||||
for i in 0..dynamic_count {
|
|
||||||
let depth = dynamic_count - 1 - i;
|
|
||||||
self.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())?;
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut dyn_keys: SmallVec<[_; 2]> = SmallVec::with_capacity(dynamic_count);
|
|
||||||
for i in 0..dynamic_count {
|
|
||||||
let depth = dynamic_count - 1 - i;
|
|
||||||
let key_val = self.peek_forced(depth);
|
|
||||||
let key_sid = match ctx.get_string_id(key_val) {
|
|
||||||
Ok(id) => Some(id),
|
|
||||||
Err(NixType::Null) => None,
|
|
||||||
Err(got) => return self.finish_type_err(NixType::String, got),
|
|
||||||
};
|
|
||||||
dyn_keys.push(key_sid);
|
|
||||||
}
|
|
||||||
|
|
||||||
self.stack.truncate(self.stack.len() - dynamic_count);
|
|
||||||
|
|
||||||
let mut kv: SmallVec<[(crate::StringId, Value); 4]> =
|
|
||||||
SmallVec::with_capacity(static_count + dynamic_count);
|
|
||||||
|
|
||||||
for _ in 0..static_count {
|
|
||||||
let key = reader.read_string_id();
|
|
||||||
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 = resolve_operand(&reader.read_operand_data(ctx), mc, self);
|
|
||||||
let _span_id = reader.read_u32();
|
|
||||||
if let Some(key) = key {
|
|
||||||
kv.push((key, val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
kv.sort_by_key(|(k, _)| *k);
|
|
||||||
let attrs = Gc::new(mc, AttrSet::from_sorted_unchecked(kv));
|
|
||||||
self.push(Value::new_gc(attrs));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_empty_attrs(&mut self) -> Step {
|
|
||||||
self.push(self.empty_attrs);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_select_static(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let _span_id = reader.read_u32();
|
|
||||||
let key = reader.read_string_id();
|
|
||||||
|
|
||||||
let attrset = self.force_and_retry::<Gc<AttrSet>>(reader, mc)?;
|
|
||||||
|
|
||||||
match attrset.lookup(key) {
|
|
||||||
Some(v) => {
|
|
||||||
self.push(v);
|
|
||||||
}
|
|
||||||
None => return self.select_skip(key, ctx, reader),
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_select_dynamic(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let _span_id = reader.read_u32();
|
|
||||||
|
|
||||||
let (attrset, key_val) = self.force_and_retry::<(Gc<AttrSet>, StrictValue)>(reader, mc)?;
|
|
||||||
|
|
||||||
let key_sid = match ctx.get_string_id(key_val) {
|
|
||||||
Ok(id) => id,
|
|
||||||
Err(got) => return self.finish_type_err(NixType::String, got),
|
|
||||||
};
|
|
||||||
|
|
||||||
match attrset.lookup(key_sid) {
|
|
||||||
Some(v) => {
|
|
||||||
self.push(v);
|
|
||||||
}
|
|
||||||
None => return self.select_skip(key_sid, ctx, reader),
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Skip the rest of a **Select** attrpath after a missing attribute.
|
|
||||||
/// Only recognises Select opcodes and jumps; encountering any other
|
|
||||||
/// opcode means we've reached the end of the select sequence and
|
|
||||||
/// should report the missing-attribute error.
|
|
||||||
fn select_skip(
|
|
||||||
&mut self,
|
|
||||||
key: StringId,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
) -> Step {
|
|
||||||
use fix_codegen::Op::*;
|
|
||||||
loop {
|
|
||||||
match reader.read_op() {
|
|
||||||
SelectStatic => {
|
|
||||||
reader.set_pc(reader.pc() + 4 + 4);
|
|
||||||
}
|
|
||||||
SelectDynamic => {
|
|
||||||
reader.set_pc(reader.pc() + 4);
|
|
||||||
}
|
|
||||||
JumpIfSelectSucceeded => {
|
|
||||||
reader.set_pc(reader.pc() + 4);
|
|
||||||
break Step::Continue(());
|
|
||||||
}
|
|
||||||
JumpIfSelectFailed => {
|
|
||||||
let offset = reader.read_i32();
|
|
||||||
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
let name = ctx.resolve_string(key);
|
|
||||||
return self
|
|
||||||
.finish_err(Error::eval_error(format!("attribute '{name}' missing")));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Skip the rest of a **HasAttr** attrpath after an intermediate
|
|
||||||
/// lookup failed. Only recognises HasAttr opcodes and jumps.
|
|
||||||
fn has_attr_skip(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
use fix_codegen::Op::*;
|
|
||||||
loop {
|
|
||||||
match reader.read_op() {
|
|
||||||
HasAttrPathStatic => {
|
|
||||||
reader.set_pc(reader.pc() + 4 + 4);
|
|
||||||
}
|
|
||||||
HasAttrPathDynamic => {
|
|
||||||
reader.set_pc(reader.pc() + 4);
|
|
||||||
}
|
|
||||||
HasAttrStatic => {
|
|
||||||
reader.set_pc(reader.pc() + 4);
|
|
||||||
break Step::Continue(());
|
|
||||||
}
|
|
||||||
JumpIfSelectFailed => {
|
|
||||||
let offset = reader.read_i32();
|
|
||||||
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
|
|
||||||
}
|
|
||||||
HasAttrDynamic => {
|
|
||||||
break Step::Continue(());
|
|
||||||
}
|
|
||||||
HasAttrResolve => {
|
|
||||||
reader.set_pc(reader.pc() - 1);
|
|
||||||
break Step::Continue(());
|
|
||||||
}
|
|
||||||
other => {
|
|
||||||
unreachable!("unexpected opcode {:?} in has_attr_skip", other as u8)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_has_attr_path_static(
|
|
||||||
&mut self,
|
|
||||||
_ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let _span_id = reader.read_u32();
|
|
||||||
let key = reader.read_string_id();
|
|
||||||
|
|
||||||
let current = self.force_and_retry::<StrictValue>(reader, mc)?;
|
|
||||||
|
|
||||||
match current
|
|
||||||
.as_gc::<AttrSet>()
|
|
||||||
.and_then(|attrs| attrs.lookup(key))
|
|
||||||
{
|
|
||||||
Some(v) => {
|
|
||||||
self.push(v);
|
|
||||||
}
|
|
||||||
None => return self.has_attr_skip(reader),
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_has_attr_path_dynamic(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let _span_id = reader.read_u32();
|
|
||||||
|
|
||||||
let (current, key_val) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
|
|
||||||
let key_sid = match ctx.get_string_id(key_val) {
|
|
||||||
Ok(id) => id,
|
|
||||||
Err(got) => return self.finish_type_err(NixType::String, got),
|
|
||||||
};
|
|
||||||
|
|
||||||
match current
|
|
||||||
.as_gc::<AttrSet>()
|
|
||||||
.and_then(|attrs| attrs.lookup(key_sid))
|
|
||||||
{
|
|
||||||
Some(v) => {
|
|
||||||
self.push(v);
|
|
||||||
}
|
|
||||||
None => return self.has_attr_skip(reader),
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_jump_if_select_failed(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
// No-op
|
|
||||||
let _offset = reader.read_i32();
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_jump_if_select_succeeded(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let offset = reader.read_i32();
|
|
||||||
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_has_attr_static(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let key = reader.read_string_id();
|
|
||||||
let current = self.force_and_retry::<StrictValue>(reader, mc)?;
|
|
||||||
|
|
||||||
self.push(Value::new_inline(
|
|
||||||
current
|
|
||||||
.as_gc::<AttrSet>()
|
|
||||||
.and_then(|attrs| attrs.lookup(key))
|
|
||||||
.is_some(),
|
|
||||||
));
|
|
||||||
// Skip HasAttrResolve
|
|
||||||
reader.set_pc(reader.pc() + 1);
|
|
||||||
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_has_attr_dynamic(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let (current, dyn_key) = self.force_and_retry::<(StrictValue, StrictValue)>(reader, mc)?;
|
|
||||||
|
|
||||||
let key_sid = match ctx.get_string_id(dyn_key) {
|
|
||||||
Ok(id) => id,
|
|
||||||
Err(got) => return self.finish_type_err(NixType::String, got),
|
|
||||||
};
|
|
||||||
|
|
||||||
self.push(Value::new_inline(
|
|
||||||
current
|
|
||||||
.as_gc::<AttrSet>()
|
|
||||||
.and_then(|attrs| attrs.lookup(key_sid))
|
|
||||||
.is_some(),
|
|
||||||
));
|
|
||||||
// Skip HasAttrResolve
|
|
||||||
reader.set_pc(reader.pc() + 1);
|
|
||||||
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_has_attr_resolve(&mut self) -> Step {
|
|
||||||
// If we reach here, has_attr check has failed, push false (AttrSet is already popped)
|
|
||||||
self.push(Value::new_inline(false));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_list(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let count = reader.read_u32() as usize;
|
|
||||||
let mut items: SmallVec<[Value; 4]> = SmallVec::with_capacity(count);
|
|
||||||
for _ in 0..count {
|
|
||||||
items.push(resolve_operand(&reader.read_operand_data(ctx), mc, self));
|
|
||||||
}
|
|
||||||
let list = Gc::new(
|
|
||||||
mc,
|
|
||||||
List {
|
|
||||||
inner: RefLock::new(items),
|
|
||||||
},
|
|
||||||
);
|
|
||||||
self.push(Value::new_gc(list));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_make_empty_list(&mut self) -> Step {
|
|
||||||
self.push(self.empty_list);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
use fix_abstract_vm::*;
|
|
||||||
use fix_error::Error;
|
|
||||||
use gc_arena::Mutation;
|
|
||||||
|
|
||||||
use crate::{BytecodeReader, Step, VmRuntimeCtx};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_jump_if_false(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let offset = reader.read_i32();
|
|
||||||
let cond = self.force_and_retry::<StrictValue>(reader, mc)?;
|
|
||||||
if cond.as_inline::<bool>() == Some(false) {
|
|
||||||
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_jump_if_true(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let offset = reader.read_i32();
|
|
||||||
let cond = self.force_and_retry::<StrictValue>(reader, mc)?;
|
|
||||||
if cond.as_inline::<bool>() == Some(true) {
|
|
||||||
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_jump(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let offset = reader.read_i32();
|
|
||||||
reader.set_pc(((reader.pc() as isize) + (offset as isize)) as usize);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_assert(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let raw_id = reader.read_string_id();
|
|
||||||
let raw = ctx.resolve_string(raw_id);
|
|
||||||
let _span_id = reader.read_u32();
|
|
||||||
let assertion = self.force_and_retry::<bool>(reader, mc)?;
|
|
||||||
if !assertion {
|
|
||||||
// FIXME: use catchable error
|
|
||||||
return self.finish_err(Error::eval_error(format!("assertion '{raw}' failed")));
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
use gc_arena::{Gc, Mutation};
|
|
||||||
|
|
||||||
use crate::{BytecodeReader, Step, Value};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_smi(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let val = reader.read_i32();
|
|
||||||
self.push(Value::new_inline(val));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_bigint(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let val = reader.read_i64();
|
|
||||||
self.push(Value::new_gc(Gc::new(mc, val)));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_float(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let val = reader.read_f64();
|
|
||||||
self.push(Value::new_float(val));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_string(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let sid = reader.read_string_id();
|
|
||||||
self.push(Value::new_inline(sid));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_null(&mut self) -> Step {
|
|
||||||
self.push(Value::new_inline(crate::Null));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_true(&mut self) -> Step {
|
|
||||||
self.push(Value::new_inline(true));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_push_false(&mut self) -> Step {
|
|
||||||
self.push(Value::new_inline(false));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,183 +0,0 @@
|
|||||||
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::{BytecodeReader, PrimOp, Step, Value, VmRuntimeCtx, VmRuntimeCtxExt};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_load_builtins(&mut self) -> Step {
|
|
||||||
self.push(self.builtins);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_load_builtin(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let Ok(id) = BuiltinId::try_from_primitive(reader.read_u8())
|
|
||||||
.map_err(|err| panic!("unknown builtin id: {}", err.number));
|
|
||||||
self.push(Value::new_inline(PrimOp {
|
|
||||||
id,
|
|
||||||
arity: fix_builtins::BUILTINS[id as usize].1,
|
|
||||||
dispatch_ip: id.entry_phase().ip(),
|
|
||||||
}));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_load_repl_binding(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let _name = reader.read_string_id();
|
|
||||||
todo!("LoadReplBinding");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_load_scoped_binding(
|
|
||||||
&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();
|
|
||||||
let scope = match self.scope_slots.get(slot_id as usize).copied() {
|
|
||||||
Some(s) => s,
|
|
||||||
None => {
|
|
||||||
return self.finish_err(Error::eval_error(format!(
|
|
||||||
"internal: invalid scope slot {slot_id}"
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let Some(attrs) = scope.as_gc::<AttrSet>() else {
|
|
||||||
return self.finish_err(Error::eval_error("internal: scope slot is not an attrset"));
|
|
||||||
};
|
|
||||||
match attrs.lookup(name) {
|
|
||||||
Some(val) => {
|
|
||||||
self.push(val);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
None => self.finish_err(Error::eval_error(format!(
|
|
||||||
"scoped binding '{}' not found",
|
|
||||||
ctx.resolve_string(name)
|
|
||||||
))),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_coerce_to_string(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
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());
|
|
||||||
}
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_concat_strings(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
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(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_resolve_path(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'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 => {
|
|
||||||
return self.finish_err(Error::eval_error(format!(
|
|
||||||
"expected a string for path, got {}",
|
|
||||||
path_val.ty()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
let resolved = match resolve_path_str(ctx.resolve_string(dir_id), &path) {
|
|
||||||
Ok(s) => s,
|
|
||||||
Err(e) => return self.finish_err(e),
|
|
||||||
};
|
|
||||||
let sid = ctx.intern_string(resolved);
|
|
||||||
self.push(Value::new_inline(Path(sid)));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_path_str(current_dir: &str, path: &str) -> Result<String, Box<Error>> {
|
|
||||||
let raw = if path.starts_with('/') {
|
|
||||||
return Ok(canon_path_str(path));
|
|
||||||
} else if let Some(rest) = path.strip_prefix("~/") {
|
|
||||||
let mut dir =
|
|
||||||
std::env::home_dir().ok_or_else(|| Error::eval_error("home dir not defined"))?;
|
|
||||||
dir.push(rest);
|
|
||||||
dir
|
|
||||||
} else {
|
|
||||||
let mut dir = PathBuf::from(current_dir);
|
|
||||||
dir.push(path);
|
|
||||||
dir
|
|
||||||
};
|
|
||||||
Ok(canon_path_str(&raw))
|
|
||||||
}
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
pub(crate) mod arithmetic;
|
|
||||||
pub(crate) mod calls;
|
|
||||||
pub(crate) mod closures;
|
|
||||||
pub(crate) mod collections;
|
|
||||||
pub(crate) mod control;
|
|
||||||
pub(crate) mod literals;
|
|
||||||
pub(crate) mod misc;
|
|
||||||
pub(crate) mod variables;
|
|
||||||
pub(crate) mod with_scope;
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use crate::{BytecodeReader, Mutation, Step, Value};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_load_local(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let idx = reader.read_u32() as usize;
|
|
||||||
self.push(self.env.borrow().locals[idx]);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_load_outer(&mut self, reader: &mut BytecodeReader<'_>) -> Step {
|
|
||||||
let layer = reader.read_u8();
|
|
||||||
let idx = reader.read_u32() as usize;
|
|
||||||
let mut cur = self.env;
|
|
||||||
for _ in 0..layer {
|
|
||||||
let prev = cur.borrow().prev.expect("LoadOuter: env chain too short");
|
|
||||||
cur = prev;
|
|
||||||
}
|
|
||||||
let val = cur.borrow().locals[idx];
|
|
||||||
self.push(val);
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_store_local(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let idx = reader.read_u32() as usize;
|
|
||||||
let val = self.pop();
|
|
||||||
self.env.borrow_mut(mc).locals[idx] = val;
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_alloc_locals(
|
|
||||||
&mut self,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
let count = reader.read_u32() as usize;
|
|
||||||
self.env
|
|
||||||
.borrow_mut(mc)
|
|
||||||
.locals
|
|
||||||
.extend(std::iter::repeat_n(Value::default(), count));
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
use fix_abstract_vm::{resolve_operand, *};
|
|
||||||
use fix_common::Symbol;
|
|
||||||
use fix_error::Error;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
use crate::{Break, BytecodeReader, CallFrame, Step, VmRuntimeCtx};
|
|
||||||
|
|
||||||
impl<'gc> crate::Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
pub(crate) fn op_lookup_with(
|
|
||||||
&mut self,
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &gc_arena::Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
let counter = self.peek_forced(0).as_inline::<i32>().unwrap();
|
|
||||||
|
|
||||||
let name = reader.read_string_id();
|
|
||||||
let n = reader.read_u8();
|
|
||||||
let mut namespaces = SmallVec::<[_; 2]>::new();
|
|
||||||
for _ in 0..n {
|
|
||||||
namespaces.push(resolve_operand(&reader.read_operand_data(ctx), mc, self));
|
|
||||||
}
|
|
||||||
|
|
||||||
let resume_pc = reader.inst_start_pc();
|
|
||||||
let namespace = match namespaces[counter as usize].restrict() {
|
|
||||||
Ok(val) => val,
|
|
||||||
Err(thunk) => {
|
|
||||||
let mut state = thunk.borrow_mut(mc);
|
|
||||||
match *state {
|
|
||||||
ThunkState::Pending { ip, env } => {
|
|
||||||
*state = ThunkState::Blackhole;
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
thunk: Some(thunk),
|
|
||||||
pc: resume_pc,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
self.env = env;
|
|
||||||
reader.set_pc(ip);
|
|
||||||
return Step::Break(Break::Force);
|
|
||||||
}
|
|
||||||
ThunkState::Evaluated(v) => v,
|
|
||||||
ThunkState::Apply { func, arg } => {
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
thunk: Some(thunk),
|
|
||||||
pc: resume_pc,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
self.push(func);
|
|
||||||
return self.call(reader, mc, arg, resume_pc);
|
|
||||||
}
|
|
||||||
ThunkState::Blackhole => {
|
|
||||||
return self
|
|
||||||
.finish_err(Error::eval_error("infinite recursion encountered"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(val) = namespace
|
|
||||||
.as_gc::<AttrSet>()
|
|
||||||
.and_then(|attrs| attrs.lookup(name))
|
|
||||||
{
|
|
||||||
self.replace(0, val);
|
|
||||||
} else if counter + 1 == n as i32 {
|
|
||||||
return self.finish_err(Error::eval_error(format!(
|
|
||||||
"undefined variable '{}'",
|
|
||||||
Symbol::from(ctx.resolve_string(name))
|
|
||||||
)));
|
|
||||||
} else {
|
|
||||||
self.replace(0, Value::new_inline(counter + 1));
|
|
||||||
reader.set_pc(resume_pc);
|
|
||||||
}
|
|
||||||
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,694 +0,0 @@
|
|||||||
#![warn(clippy::unwrap_used)]
|
|
||||||
#![cfg_attr(feature = "tailcall", expect(incomplete_features))]
|
|
||||||
#![cfg_attr(
|
|
||||||
feature = "tailcall",
|
|
||||||
feature(explicit_tail_calls, rust_preserve_none_cc)
|
|
||||||
)]
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use fix_builtins::{BUILTINS, BuiltinId};
|
|
||||||
use fix_codegen::InstructionPtr;
|
|
||||||
use fix_common::StringId;
|
|
||||||
use fix_error::{Error, Result, Source};
|
|
||||||
use gc_arena::metrics::Pacing;
|
|
||||||
use gc_arena::{Arena, Collect, Gc, Mutation, RefLock, Rootable};
|
|
||||||
use hashbrown::HashMap;
|
|
||||||
use num_enum::TryFromPrimitive;
|
|
||||||
use smallvec::SmallVec;
|
|
||||||
|
|
||||||
#[cfg(feature = "tailcall")]
|
|
||||||
mod dispatch_tailcall;
|
|
||||||
pub use fix_abstract_vm::*;
|
|
||||||
mod instructions;
|
|
||||||
|
|
||||||
type VmResult<T> = std::result::Result<T, VmError>;
|
|
||||||
|
|
||||||
#[derive(Collect)]
|
|
||||||
#[collect(no_drop)]
|
|
||||||
pub struct Vm<'gc> {
|
|
||||||
stack: Vec<Value<'gc>>,
|
|
||||||
call_stack: Vec<CallFrame<'gc>>,
|
|
||||||
call_depth: usize,
|
|
||||||
#[allow(dead_code)]
|
|
||||||
#[collect(require_static)]
|
|
||||||
error_context: Vec<ErrorFrame>,
|
|
||||||
|
|
||||||
env: GcEnv<'gc>,
|
|
||||||
|
|
||||||
import_cache: HashMap<PathBuf, Value<'gc>>,
|
|
||||||
scope_slots: Vec<Value<'gc>>,
|
|
||||||
|
|
||||||
builtins: Value<'gc>,
|
|
||||||
empty_list: Value<'gc>,
|
|
||||||
empty_attrs: Value<'gc>,
|
|
||||||
|
|
||||||
force_mode: ForceMode,
|
|
||||||
|
|
||||||
#[collect(require_static)]
|
|
||||||
result: Option<Result<fix_common::Value>>,
|
|
||||||
|
|
||||||
#[collect(require_static)]
|
|
||||||
pending_load: Option<PendingLoad>,
|
|
||||||
|
|
||||||
functor_sym: StringId,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn init_builtins<'gc>(mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Value<'gc> {
|
|
||||||
let mut entries = SmallVec::with_capacity(BUILTINS.len());
|
|
||||||
|
|
||||||
for (idx, &(name, arity)) in BUILTINS.iter().enumerate() {
|
|
||||||
let id = BuiltinId::try_from_primitive(idx as u8).expect("infallible");
|
|
||||||
let name = name.strip_prefix("__").unwrap_or(name);
|
|
||||||
let name = ctx.intern_string(name);
|
|
||||||
let dispatch_ip = id.entry_phase().ip();
|
|
||||||
entries.push((
|
|
||||||
name,
|
|
||||||
Value::new_inline(PrimOp {
|
|
||||||
id,
|
|
||||||
arity,
|
|
||||||
dispatch_ip,
|
|
||||||
}),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let consts = [
|
|
||||||
(
|
|
||||||
"__currentSystem",
|
|
||||||
Value::new_inline(ctx.intern_string("x86_64-linux")),
|
|
||||||
),
|
|
||||||
("__langVersion", Value::new_inline(6i32)),
|
|
||||||
(
|
|
||||||
"__nixVersion",
|
|
||||||
Value::new_inline(ctx.intern_string("2.24.0")),
|
|
||||||
),
|
|
||||||
(
|
|
||||||
"__storeDir",
|
|
||||||
Value::new_inline(ctx.intern_string("/nix/store")),
|
|
||||||
),
|
|
||||||
("__nixPath", Value::new_gc(Gc::new(mc, List::default()))),
|
|
||||||
("null", Value::new_inline(Null)),
|
|
||||||
("true", Value::new_inline(true)),
|
|
||||||
("false", Value::new_inline(false)),
|
|
||||||
];
|
|
||||||
|
|
||||||
for (name, val) in consts {
|
|
||||||
let name = name.strip_prefix("__").unwrap_or(name);
|
|
||||||
let name = ctx.intern_string(name);
|
|
||||||
entries.push((name, val));
|
|
||||||
}
|
|
||||||
|
|
||||||
let self_ref_thunk = Gc::new(mc, RefLock::new(ThunkState::Blackhole));
|
|
||||||
let sym = ctx.intern_string("builtins");
|
|
||||||
entries.push((sym, Value::new_gc(self_ref_thunk)));
|
|
||||||
|
|
||||||
entries.sort_by_key(|(k, _)| *k);
|
|
||||||
|
|
||||||
let builtins_set = Gc::new(mc, AttrSet::from_sorted_unchecked(entries));
|
|
||||||
let builtins_value = Value::new_gc(builtins_set);
|
|
||||||
*self_ref_thunk.borrow_mut(mc) =
|
|
||||||
ThunkState::Evaluated(builtins_value.restrict().expect("builtins is not a thunk"));
|
|
||||||
builtins_value
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Vm<'gc> {
|
|
||||||
fn new(force_mode: ForceMode, mc: &Mutation<'gc>, ctx: &mut impl VmRuntimeCtx) -> Self {
|
|
||||||
let builtins = init_builtins(mc, ctx);
|
|
||||||
Vm {
|
|
||||||
stack: Vec::with_capacity(8192),
|
|
||||||
call_stack: Vec::with_capacity(1024),
|
|
||||||
call_depth: 0,
|
|
||||||
error_context: Vec::with_capacity(1024),
|
|
||||||
|
|
||||||
env: Gc::new(mc, RefLock::new(Env::empty())),
|
|
||||||
|
|
||||||
import_cache: HashMap::new(),
|
|
||||||
scope_slots: Vec::new(),
|
|
||||||
|
|
||||||
builtins,
|
|
||||||
empty_list: Value::new_gc(Gc::new(mc, List::default())),
|
|
||||||
empty_attrs: Value::new_gc(Gc::new(mc, AttrSet::default())),
|
|
||||||
|
|
||||||
force_mode,
|
|
||||||
|
|
||||||
result: None,
|
|
||||||
pending_load: None,
|
|
||||||
|
|
||||||
functor_sym: ctx.intern_string("__functor"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn finish_ok(&mut self, val: fix_common::Value) -> Step {
|
|
||||||
self.result = Some(Ok(val));
|
|
||||||
Step::Break(Break::Done)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn finish_err(&mut self, err: Box<Error>) -> Step {
|
|
||||||
self.result = Some(Err(err));
|
|
||||||
Step::Break(Break::Done)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn finish_type_err(&mut self, expected: NixType, got: NixType) -> Step {
|
|
||||||
self.result = Some(Err(Error::eval_error(format!(
|
|
||||||
"expected {expected}, got {got}"
|
|
||||||
))));
|
|
||||||
Step::Break(Break::Done)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn finish_vm_err(&mut self, err: VmError) -> Step {
|
|
||||||
self.finish_err(err.into_error())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn push(&mut self, val: Value<'gc>) {
|
|
||||||
self.stack.push(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[must_use]
|
|
||||||
fn pop(&mut self) -> Value<'gc> {
|
|
||||||
self.stack.pop().expect("stack underflow")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[must_use]
|
|
||||||
fn peek(&self, depth: usize) -> Value<'gc> {
|
|
||||||
*self
|
|
||||||
.stack
|
|
||||||
.get(self.stack.len() - depth - 1)
|
|
||||||
.expect("stack underflow")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[must_use]
|
|
||||||
fn peek_forced(&self, depth: usize) -> StrictValue<'gc> {
|
|
||||||
self.stack
|
|
||||||
.get(self.stack.len() - depth - 1)
|
|
||||||
.expect("stack underflow")
|
|
||||||
.restrict()
|
|
||||||
.expect("forced")
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn replace(&mut self, depth: usize, val: Value<'gc>) {
|
|
||||||
let len = self.stack.len();
|
|
||||||
*self
|
|
||||||
.stack
|
|
||||||
.get_mut(len - depth - 1)
|
|
||||||
.expect("stack underflow") = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[cfg_attr(debug_assertions, track_caller)]
|
|
||||||
fn pop_forced(&mut self) -> StrictValue<'gc> {
|
|
||||||
self.stack
|
|
||||||
.pop()
|
|
||||||
.expect("stack underflow")
|
|
||||||
.restrict()
|
|
||||||
.expect("forced")
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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. Calling `force_and_retry` twice (or more)
|
|
||||||
/// means the handler will be re-run from the top after each retry;
|
|
||||||
/// any stack modifications between the two calls would be duplicated
|
|
||||||
/// and corrupt the stack layout.
|
|
||||||
///
|
|
||||||
/// * The caller must ensure that the stack layout at the point of
|
|
||||||
/// invocation is **identical** every time the handler is re-entered.
|
|
||||||
/// In practice this means no pushes, pops, or local mutations may
|
|
||||||
/// happen before the call, and the call must be the first thing
|
|
||||||
/// that consumes the instruction's operand values.
|
|
||||||
///
|
|
||||||
/// * The return value must be propagated with `?` so that
|
|
||||||
/// `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>,
|
|
||||||
) -> std::ops::ControlFlow<Break, T> {
|
|
||||||
self.force_and_retry_pc(reader, mc, reader.inst_start_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,
|
|
||||||
) -> std::ops::ControlFlow<Break, T> {
|
|
||||||
T::force_and_check(self, reader, mc, 0, resume_pc)?;
|
|
||||||
std::ops::ControlFlow::Continue(T::pop_converted(self))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[allow(unused)]
|
|
||||||
fn force_slot(
|
|
||||||
&mut self,
|
|
||||||
depth: usize,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Step {
|
|
||||||
self.force_slot_to_pc(depth, reader, mc, reader.inst_start_pc())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn force_slot_to_pc(
|
|
||||||
&mut self,
|
|
||||||
depth: usize,
|
|
||||||
reader: &mut BytecodeReader<'_>,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
resume_pc: usize,
|
|
||||||
) -> Step {
|
|
||||||
let Some(thunk) = self.peek(depth).as_gc::<Thunk>() else {
|
|
||||||
return Step::Continue(());
|
|
||||||
};
|
|
||||||
let mut state = thunk.borrow_mut(mc);
|
|
||||||
match *state {
|
|
||||||
ThunkState::Pending { ip, env } => {
|
|
||||||
*state = ThunkState::Blackhole;
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
thunk: Some(thunk),
|
|
||||||
pc: resume_pc,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
self.env = env;
|
|
||||||
reader.set_pc(ip);
|
|
||||||
Step::Break(Break::Force)
|
|
||||||
}
|
|
||||||
ThunkState::Evaluated(v) => {
|
|
||||||
self.replace(depth, v.relax());
|
|
||||||
Step::Continue(())
|
|
||||||
}
|
|
||||||
ThunkState::Apply { func, arg } => {
|
|
||||||
self.call_stack.push(CallFrame {
|
|
||||||
thunk: Some(thunk),
|
|
||||||
pc: resume_pc,
|
|
||||||
env: self.env,
|
|
||||||
});
|
|
||||||
self.push(func);
|
|
||||||
self.call(reader, mc, arg, resume_pc)
|
|
||||||
}
|
|
||||||
ThunkState::Blackhole => {
|
|
||||||
self.finish_err(Error::eval_error("infinite recursion encountered"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Machine<'gc> for Vm<'gc> {
|
|
||||||
#[inline(always)]
|
|
||||||
fn push(&mut self, val: Value<'gc>) {
|
|
||||||
self.push(val);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[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 {
|
|
||||||
Continue { pc: usize },
|
|
||||||
Done(Result<fix_common::Value>),
|
|
||||||
LoadFile(PendingLoad),
|
|
||||||
}
|
|
||||||
|
|
||||||
/// 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<'_> {
|
|
||||||
pub fn run<C: VmContext>(
|
|
||||||
ctx: &mut C,
|
|
||||||
ip: InstructionPtr,
|
|
||||||
force_mode: ForceMode,
|
|
||||||
) -> Result<fix_common::Value> {
|
|
||||||
let (code, runtime) = ctx.split();
|
|
||||||
let mut arena: Arena<Rootable![Vm<'_>]> = Arena::new(|mc| Vm::new(force_mode, mc, runtime));
|
|
||||||
arena.metrics().set_pacing(Pacing {
|
|
||||||
min_sleep: initial_heap_size(),
|
|
||||||
..Pacing::STOP_THE_WORLD
|
|
||||||
});
|
|
||||||
|
|
||||||
let mut pc = ip.0;
|
|
||||||
loop {
|
|
||||||
let bytecode = code.bytecode();
|
|
||||||
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() > 0.0 {
|
|
||||||
arena.finish_cycle();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Action::LoadFile(load) => {
|
|
||||||
let source = match Source::new_file(load.path) {
|
|
||||||
Ok(src) => src,
|
|
||||||
Err(err) => break Err(Error::eval_error(format!("import failed: {err}"))),
|
|
||||||
};
|
|
||||||
let extra_scope = load.scope.map(|s| ExtraScope::ScopedImport {
|
|
||||||
keys: s.keys,
|
|
||||||
slot_id: s.slot_id,
|
|
||||||
});
|
|
||||||
let new_ip = match code.compile_with_scope(source, extra_scope, runtime) {
|
|
||||||
Ok(ip) => ip,
|
|
||||||
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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'gc> Vm<'gc> {
|
|
||||||
const DEFAULT_FUEL_AMOUNT: u32 = 1024;
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
fn dispatch_batch<C: VmRuntimeCtx>(
|
|
||||||
&mut self,
|
|
||||||
bytecode: &[u8],
|
|
||||||
ctx: &mut C,
|
|
||||||
pc: usize,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Action {
|
|
||||||
#[cfg(not(feature = "tailcall"))]
|
|
||||||
{
|
|
||||||
self.execute_batch(bytecode, ctx, pc, mc)
|
|
||||||
}
|
|
||||||
#[cfg(feature = "tailcall")]
|
|
||||||
{
|
|
||||||
use crate::dispatch_tailcall::{TailResult, run_tailcall};
|
|
||||||
match run_tailcall(self, mc, ctx, bytecode, pc as u32) {
|
|
||||||
TailResult::YieldFuel(new_pc) => Action::Continue {
|
|
||||||
pc: new_pc as usize,
|
|
||||||
},
|
|
||||||
TailResult::Done => {
|
|
||||||
Action::Done(self.result.take().expect("TailResult::Done without result"))
|
|
||||||
}
|
|
||||||
TailResult::LoadFile => Action::LoadFile(
|
|
||||||
self.pending_load
|
|
||||||
.take()
|
|
||||||
.expect("TailResult::LoadFile without pending_load"),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline(always)]
|
|
||||||
#[cfg(not(feature = "tailcall"))]
|
|
||||||
fn execute_batch(
|
|
||||||
&mut self,
|
|
||||||
bytecode: &[u8],
|
|
||||||
ctx: &mut impl VmRuntimeCtx,
|
|
||||||
pc: usize,
|
|
||||||
mc: &Mutation<'gc>,
|
|
||||||
) -> Action {
|
|
||||||
use fix_codegen::Op::*;
|
|
||||||
|
|
||||||
let mut reader = BytecodeReader::new(bytecode, pc);
|
|
||||||
let mut fuel = Self::DEFAULT_FUEL_AMOUNT;
|
|
||||||
|
|
||||||
loop {
|
|
||||||
if fuel == 0 {
|
|
||||||
return Action::Continue { pc: reader.pc() };
|
|
||||||
}
|
|
||||||
fuel -= 1;
|
|
||||||
|
|
||||||
let op = reader.read_op();
|
|
||||||
|
|
||||||
let result = match op {
|
|
||||||
PushSmi => self.op_push_smi(&mut reader),
|
|
||||||
PushBigInt => self.op_push_bigint(&mut reader, mc),
|
|
||||||
PushFloat => self.op_push_float(&mut reader),
|
|
||||||
PushString => self.op_push_string(&mut reader),
|
|
||||||
PushNull => self.op_push_null(),
|
|
||||||
PushTrue => self.op_push_true(),
|
|
||||||
PushFalse => self.op_push_false(),
|
|
||||||
|
|
||||||
LoadLocal => self.op_load_local(&mut reader),
|
|
||||||
LoadOuter => self.op_load_outer(&mut reader),
|
|
||||||
StoreLocal => self.op_store_local(&mut reader, mc),
|
|
||||||
AllocLocals => self.op_alloc_locals(&mut reader, mc),
|
|
||||||
|
|
||||||
MakeThunk => self.op_make_thunk(&mut reader, mc),
|
|
||||||
MakeClosure => self.op_make_closure(&mut reader, mc),
|
|
||||||
MakePatternClosure => self.op_make_pattern_closure(&mut reader, mc),
|
|
||||||
|
|
||||||
Call => self.op_call(ctx, &mut reader, mc),
|
|
||||||
DispatchPrimOp => self.op_dispatch_primop(ctx, &mut reader, mc),
|
|
||||||
Return => self.op_return(ctx, &mut reader, mc),
|
|
||||||
|
|
||||||
MakeAttrs => self.op_make_attrs(ctx, &mut reader, mc),
|
|
||||||
MakeEmptyAttrs => self.op_make_empty_attrs(),
|
|
||||||
SelectStatic => self.op_select_static(ctx, &mut reader, mc),
|
|
||||||
SelectDynamic => self.op_select_dynamic(ctx, &mut reader, mc),
|
|
||||||
HasAttrPathStatic => self.op_has_attr_path_static(ctx, &mut reader, mc),
|
|
||||||
HasAttrPathDynamic => self.op_has_attr_path_dynamic(ctx, &mut reader, mc),
|
|
||||||
HasAttrStatic => self.op_has_attr_static(&mut reader, mc),
|
|
||||||
HasAttrDynamic => self.op_has_attr_dynamic(ctx, &mut reader, mc),
|
|
||||||
HasAttrResolve => self.op_has_attr_resolve(),
|
|
||||||
JumpIfSelectFailed => self.op_jump_if_select_failed(&mut reader),
|
|
||||||
JumpIfSelectSucceeded => self.op_jump_if_select_succeeded(&mut reader),
|
|
||||||
|
|
||||||
MakeList => self.op_make_list(ctx, &mut reader, mc),
|
|
||||||
MakeEmptyList => self.op_make_empty_list(),
|
|
||||||
|
|
||||||
OpAdd => self.op_add(ctx, &mut reader, mc),
|
|
||||||
OpSub => self.op_sub(&mut reader, mc),
|
|
||||||
OpMul => self.op_mul(&mut reader, mc),
|
|
||||||
OpDiv => self.op_div(&mut reader, mc),
|
|
||||||
OpEq => self.op_eq(ctx, &mut reader, mc),
|
|
||||||
OpNeq => self.op_neq(ctx, &mut reader, mc),
|
|
||||||
OpLt => self.op_lt(ctx, &mut reader, mc),
|
|
||||||
OpGt => self.op_gt(ctx, &mut reader, mc),
|
|
||||||
OpLeq => self.op_leq(ctx, &mut reader, mc),
|
|
||||||
OpGeq => self.op_geq(ctx, &mut reader, mc),
|
|
||||||
OpConcat => self.op_concat(&mut reader, mc),
|
|
||||||
OpUpdate => self.op_update(&mut reader, mc),
|
|
||||||
|
|
||||||
OpNeg => self.op_neg(&mut reader, mc),
|
|
||||||
OpNot => self.op_not(&mut reader, mc),
|
|
||||||
|
|
||||||
JumpIfFalse => self.op_jump_if_false(&mut reader, mc),
|
|
||||||
JumpIfTrue => self.op_jump_if_true(&mut reader, mc),
|
|
||||||
Jump => self.op_jump(&mut reader),
|
|
||||||
|
|
||||||
ConcatStrings => self.op_concat_strings(ctx, &mut reader, mc),
|
|
||||||
CoerceToString => self.op_coerce_to_string(&mut reader, mc),
|
|
||||||
ResolvePath => self.op_resolve_path(ctx, &mut reader, mc),
|
|
||||||
|
|
||||||
Assert => self.op_assert(ctx, &mut reader, mc),
|
|
||||||
|
|
||||||
LookupWith => self.op_lookup_with(ctx, &mut reader, mc),
|
|
||||||
|
|
||||||
LoadBuiltins => self.op_load_builtins(),
|
|
||||||
LoadBuiltin => self.op_load_builtin(&mut reader),
|
|
||||||
|
|
||||||
LoadReplBinding => self.op_load_repl_binding(&mut reader),
|
|
||||||
LoadScopedBinding => self.op_load_scoped_binding(ctx, &mut reader, mc),
|
|
||||||
|
|
||||||
Illegal => unreachable!(),
|
|
||||||
};
|
|
||||||
|
|
||||||
match result {
|
|
||||||
Step::Continue(()) | Step::Break(Break::Force) => {}
|
|
||||||
Step::Break(Break::Done) => {
|
|
||||||
return Action::Done(self.result.take().expect("Break::Done without result"));
|
|
||||||
}
|
|
||||||
Step::Break(Break::LoadFile) => {
|
|
||||||
return Action::LoadFile(
|
|
||||||
self.pending_load
|
|
||||||
.take()
|
|
||||||
.expect("Break::LoadFile without pending_load"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
[package]
|
|
||||||
name = "fix"
|
|
||||||
version = "0.1.0"
|
|
||||||
edition = "2024"
|
|
||||||
|
|
||||||
[dependencies]
|
|
||||||
mimalloc = "0.1"
|
|
||||||
|
|
||||||
# REPL
|
|
||||||
anyhow = "1.0"
|
|
||||||
rustyline = "18.0"
|
|
||||||
|
|
||||||
# CLI
|
|
||||||
clap = { version = "4", features = ["derive"] }
|
|
||||||
|
|
||||||
# Logging
|
|
||||||
tracing = "0.1"
|
|
||||||
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
|
||||||
|
|
||||||
# Error Reporting
|
|
||||||
thiserror = "2"
|
|
||||||
miette = { version = "7.4", features = ["fancy"] }
|
|
||||||
|
|
||||||
# Data Structure
|
|
||||||
hashbrown = { workspace = true }
|
|
||||||
string-interner = { workspace = true }
|
|
||||||
|
|
||||||
# Memory Management
|
|
||||||
bumpalo = { workspace = true }
|
|
||||||
|
|
||||||
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" }
|
|
||||||
fix-error = { path = "../fix-error" }
|
|
||||||
fix-ir = { path = "../fix-ir" }
|
|
||||||
fix-vm = { path = "../fix-vm" }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
|
||||||
criterion = { version = "0.8", features = ["html_reports"] }
|
|
||||||
tempfile = "3.24"
|
|
||||||
test-log = { version = "0.2", features = ["trace"] }
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "basic_ops"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "builtins"
|
|
||||||
harness = false
|
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "thunk_scope"
|
|
||||||
harness = false
|
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use fix::Evaluator;
|
|
||||||
use fix_common::Value;
|
|
||||||
use fix_error::{Result, Source};
|
|
||||||
|
|
||||||
pub fn eval(expr: &str) -> Value {
|
|
||||||
Evaluator::new()
|
|
||||||
.eval(Source::new_eval(expr.into()).unwrap())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_result(expr: &str) -> Result<Value> {
|
|
||||||
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap())
|
|
||||||
}
|
|
||||||
-724
@@ -1,724 +0,0 @@
|
|||||||
#![warn(clippy::unwrap_used)]
|
|
||||||
#![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};
|
|
||||||
use fix_common::{StringId, Symbol};
|
|
||||||
use fix_error::{Error, Result, Source};
|
|
||||||
use fix_ir::downgrade::{Downgrade as _, DowngradeContext};
|
|
||||||
use fix_ir::{
|
|
||||||
GhostMaybeThunkRef, GhostRoIrRef, GhostRoMaybeThunkRef, GhostRoRef, Ir, MaybeThunk, RawIrRef,
|
|
||||||
ThunkId,
|
|
||||||
};
|
|
||||||
use fix_vm::Vm;
|
|
||||||
use ghost_cell::{GhostCell, GhostToken};
|
|
||||||
use hashbrown::{HashMap, HashSet};
|
|
||||||
use string_interner::{DefaultStringInterner, Symbol as _};
|
|
||||||
|
|
||||||
mod derivation;
|
|
||||||
pub mod logging;
|
|
||||||
|
|
||||||
#[global_allocator]
|
|
||||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
|
||||||
|
|
||||||
pub struct RuntimeState {
|
|
||||||
pub strings: DefaultStringInterner,
|
|
||||||
pub constants: Constants,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct CodeState {
|
|
||||||
pub bytecode: Vec<u8>,
|
|
||||||
pub sources: Vec<Source>,
|
|
||||||
pub spans: Vec<(usize, rnix::TextRange)>,
|
|
||||||
pub thunk_count: usize,
|
|
||||||
pub global_env: HashMap<StringId, MaybeThunk>,
|
|
||||||
pub current_scope_slot: Option<u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct Evaluator {
|
|
||||||
pub runtime: RuntimeState,
|
|
||||||
pub code: CodeState,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Default for Evaluator {
|
|
||||||
fn default() -> Self {
|
|
||||||
Self::new()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Evaluator {
|
|
||||||
pub fn new() -> Self {
|
|
||||||
let mut strings = DefaultStringInterner::new();
|
|
||||||
let global_env = fix_ir::new_global_env(&mut strings);
|
|
||||||
let mut bytecode = Vec::with_capacity(PrimOpPhase::Illegal as usize * 2);
|
|
||||||
for phase in 0..=PrimOpPhase::Illegal as u8 {
|
|
||||||
bytecode.push(Op::DispatchPrimOp as u8);
|
|
||||||
bytecode.push(phase);
|
|
||||||
}
|
|
||||||
Self {
|
|
||||||
runtime: RuntimeState {
|
|
||||||
strings,
|
|
||||||
constants: Constants::default(),
|
|
||||||
},
|
|
||||||
code: CodeState {
|
|
||||||
sources: Vec::new(),
|
|
||||||
spans: Vec::new(),
|
|
||||||
thunk_count: 0,
|
|
||||||
bytecode,
|
|
||||||
global_env,
|
|
||||||
current_scope_slot: None,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval(&mut self, source: Source) -> Result<fix_common::Value> {
|
|
||||||
self.do_eval(source, None, ForceMode::AsIs)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_shallow(&mut self, source: Source) -> Result<fix_common::Value> {
|
|
||||||
self.do_eval(source, None, ForceMode::Shallow)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_deep(&mut self, source: Source) -> Result<fix_common::Value> {
|
|
||||||
self.do_eval(source, None, ForceMode::Deep)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_repl(
|
|
||||||
&mut self,
|
|
||||||
source: Source,
|
|
||||||
scope: &HashSet<StringId>,
|
|
||||||
) -> Result<fix_common::Value> {
|
|
||||||
self.do_eval(source, Some(ExtraScope::Repl(scope)), ForceMode::Shallow)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn do_eval<'ctx>(
|
|
||||||
&'ctx mut self,
|
|
||||||
source: Source,
|
|
||||||
extra_scope: Option<ExtraScope<'ctx>>,
|
|
||||||
force_mode: ForceMode,
|
|
||||||
) -> Result<fix_common::Value> {
|
|
||||||
let ip = {
|
|
||||||
let mut compiler = CompilerCtx {
|
|
||||||
code: &mut self.code,
|
|
||||||
runtime: &mut self.runtime,
|
|
||||||
};
|
|
||||||
compiler.compile_bytecode(source, extra_scope)?
|
|
||||||
};
|
|
||||||
Vm::run(self, ip, force_mode)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn add_binding(
|
|
||||||
&mut self,
|
|
||||||
_ident: &str,
|
|
||||||
_expr: &str,
|
|
||||||
_scope: &mut HashSet<StringId>,
|
|
||||||
) -> Result<fix_common::Value> {
|
|
||||||
todo!("add_binding")
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn compile_bytecode(&mut self, source: Source) -> Result<InstructionPtr> {
|
|
||||||
let mut compiler = CompilerCtx {
|
|
||||||
code: &mut self.code,
|
|
||||||
runtime: &mut self.runtime,
|
|
||||||
};
|
|
||||||
compiler.compile_bytecode(source, None)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn disassemble_colored(&self, ip: InstructionPtr) -> String {
|
|
||||||
Disassembler::new(ip, self).disassemble_colored()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VmRuntimeCtx for RuntimeState {
|
|
||||||
fn intern_string(&mut self, s: impl AsRef<str>) -> StringId {
|
|
||||||
StringId(self.strings.get_or_intern(s))
|
|
||||||
}
|
|
||||||
fn resolve_string(&self, id: StringId) -> &str {
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
self.strings.resolve(id.0).unwrap()
|
|
||||||
}
|
|
||||||
fn get_const(&self, id: u32) -> StaticValue {
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
self.constants.get(id).unwrap()
|
|
||||||
}
|
|
||||||
fn add_const(&mut self, val: StaticValue) -> u32 {
|
|
||||||
self.constants.insert(val)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VmCode for CodeState {
|
|
||||||
fn bytecode(&self) -> &[u8] {
|
|
||||||
&self.bytecode
|
|
||||||
}
|
|
||||||
fn compile_with_scope(
|
|
||||||
&mut self,
|
|
||||||
source: Source,
|
|
||||||
extra_scope: Option<fix_abstract_vm::ExtraScope>,
|
|
||||||
runtime: &mut impl VmRuntimeCtx,
|
|
||||||
) -> Result<InstructionPtr> {
|
|
||||||
let mut compiler = CompilerCtx {
|
|
||||||
code: self,
|
|
||||||
runtime,
|
|
||||||
};
|
|
||||||
let extra = extra_scope.map(|s| match s {
|
|
||||||
fix_abstract_vm::ExtraScope::ScopedImport { keys, slot_id } => {
|
|
||||||
ExtraScope::ScopedImport { keys, slot_id }
|
|
||||||
}
|
|
||||||
});
|
|
||||||
compiler.compile_bytecode(source, extra)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl VmContext for Evaluator {
|
|
||||||
fn split(&mut self) -> (&mut impl VmCode, &mut impl VmRuntimeCtx) {
|
|
||||||
(&mut self.code, &mut self.runtime)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CompilerCtx<'a, R: VmRuntimeCtx> {
|
|
||||||
code: &'a mut CodeState,
|
|
||||||
runtime: &'a mut R,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: VmRuntimeCtx> CompilerCtx<'a, R> {
|
|
||||||
fn compile_bytecode(
|
|
||||||
&mut self,
|
|
||||||
source: Source,
|
|
||||||
extra_scope: Option<ExtraScope>,
|
|
||||||
) -> Result<InstructionPtr> {
|
|
||||||
let prev_scope_slot = self.code.current_scope_slot;
|
|
||||||
self.code.current_scope_slot = match &extra_scope {
|
|
||||||
Some(ExtraScope::ScopedImport { slot_id, .. }) => Some(*slot_id),
|
|
||||||
_ => None,
|
|
||||||
};
|
|
||||||
let result = (|| -> Result<InstructionPtr> {
|
|
||||||
let root = self.downgrade(source, extra_scope)?;
|
|
||||||
let ip = fix_codegen::compile_bytecode(root.as_ref(), self);
|
|
||||||
Ok(ip)
|
|
||||||
})();
|
|
||||||
self.code.current_scope_slot = prev_scope_slot;
|
|
||||||
result
|
|
||||||
}
|
|
||||||
|
|
||||||
fn downgrade(&mut self, source: Source, extra_scope: Option<ExtraScope>) -> Result<OwnedIr> {
|
|
||||||
tracing::debug!("Parsing Nix expression");
|
|
||||||
|
|
||||||
self.code.sources.push(source.clone());
|
|
||||||
|
|
||||||
let root = rnix::Root::parse(&source.src);
|
|
||||||
handle_parse_error(root.errors(), source.clone()).map_or(Ok(()), Err)?;
|
|
||||||
|
|
||||||
tracing::debug!("Downgrading Nix expression");
|
|
||||||
let expr = root
|
|
||||||
.tree()
|
|
||||||
.expr()
|
|
||||||
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
|
|
||||||
let bump = Bump::new();
|
|
||||||
GhostToken::new(|token| {
|
|
||||||
let downgrade_ctx = DowngradeCtx::new(
|
|
||||||
&bump,
|
|
||||||
token,
|
|
||||||
self.runtime,
|
|
||||||
&self.code.global_env,
|
|
||||||
extra_scope.map(Into::into),
|
|
||||||
&mut self.code.thunk_count,
|
|
||||||
source,
|
|
||||||
);
|
|
||||||
let ir = downgrade_ctx.downgrade_toplevel(expr)?;
|
|
||||||
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
|
|
||||||
Ok(OwnedIr { _bump: bump, ir })
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, R: VmRuntimeCtx> BytecodeContext for CompilerCtx<'a, R> {
|
|
||||||
fn intern_string(&mut self, s: &str) -> StringId {
|
|
||||||
self.runtime.intern_string(s)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn register_span(&mut self, range: rnix::TextRange) -> u32 {
|
|
||||||
let id = self.code.spans.len();
|
|
||||||
let source_id = self
|
|
||||||
.code
|
|
||||||
.sources
|
|
||||||
.len()
|
|
||||||
.checked_sub(1)
|
|
||||||
.expect("current_source not set");
|
|
||||||
self.code.spans.push((source_id, range));
|
|
||||||
id as u32
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_code(&self) -> &[u8] {
|
|
||||||
&self.code.bytecode
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_code_mut(&mut self) -> &mut Vec<u8> {
|
|
||||||
&mut self.code.bytecode
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_constant(&mut self, val: fix_codegen::Const) -> u32 {
|
|
||||||
use fix_codegen::Const::*;
|
|
||||||
let val = match val {
|
|
||||||
Smi(x) => StaticValue::new_inline(x),
|
|
||||||
Float(x) => StaticValue::new_float(x),
|
|
||||||
Bool(x) => StaticValue::new_inline(x),
|
|
||||||
String(x) => StaticValue::new_inline(x),
|
|
||||||
Path(_) => todo!("path value type"),
|
|
||||||
PrimOp {
|
|
||||||
id,
|
|
||||||
arity,
|
|
||||||
dispatch_ip,
|
|
||||||
} => StaticValue::new_primop(id, arity, dispatch_ip),
|
|
||||||
Null => StaticValue::default(),
|
|
||||||
};
|
|
||||||
self.runtime.add_const(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_source_dir(&mut self) -> StringId {
|
|
||||||
let dir = self
|
|
||||||
.code
|
|
||||||
.sources
|
|
||||||
.last()
|
|
||||||
.expect("current_source not set")
|
|
||||||
.get_dir()
|
|
||||||
.to_string_lossy()
|
|
||||||
.into_owned();
|
|
||||||
self.runtime.intern_string(dir)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn current_scope_slot(&self) -> Option<u32> {
|
|
||||||
self.code.current_scope_slot
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
|
||||||
pub struct Constants {
|
|
||||||
data: Vec<StaticValue>,
|
|
||||||
dedup: HashMap<u64, u32>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Constants {
|
|
||||||
fn insert(&mut self, val: StaticValue) -> u32 {
|
|
||||||
let bits = val.to_bits();
|
|
||||||
*self.dedup.entry(bits).or_insert_with(|| {
|
|
||||||
let idx = self.data.len() as u32;
|
|
||||||
self.data.push(val);
|
|
||||||
idx
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get(&self, id: u32) -> Option<StaticValue> {
|
|
||||||
self.data.get(id as usize).copied()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_error_span(error: &rnix::ParseError) -> Option<rnix::TextRange> {
|
|
||||||
use rnix::ParseError::*;
|
|
||||||
match error {
|
|
||||||
Unexpected(range)
|
|
||||||
| UnexpectedExtra(range)
|
|
||||||
| UnexpectedWanted(_, range, _)
|
|
||||||
| UnexpectedDoubleBind(range)
|
|
||||||
| DuplicatedArgs(range, _) => Some(*range),
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn handle_parse_error<'a>(
|
|
||||||
errors: impl IntoIterator<Item = &'a rnix::ParseError>,
|
|
||||||
source: Source,
|
|
||||||
) -> Option<Box<Error>> {
|
|
||||||
for err in errors {
|
|
||||||
if let Some(span) = parse_error_span(err) {
|
|
||||||
return Some(
|
|
||||||
Error::parse_error(err.to_string())
|
|
||||||
.with_source(source)
|
|
||||||
.with_span(span),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
struct DowngradeCtx<'ctx, 'id, 'ir, R: VmRuntimeCtx> {
|
|
||||||
bump: &'ir Bump,
|
|
||||||
token: GhostToken<'id>,
|
|
||||||
runtime: &'ctx mut R,
|
|
||||||
source: Source,
|
|
||||||
scopes: Vec<Scope<'ctx, 'id, 'ir>>,
|
|
||||||
with_stack: Vec<GhostRoMaybeThunkRef<'id, 'ir>>,
|
|
||||||
arg_count: u32,
|
|
||||||
thunk_count: &'ctx mut usize,
|
|
||||||
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'ctx, 'id, 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
|
|
||||||
fn new(
|
|
||||||
bump: &'ir Bump,
|
|
||||||
token: GhostToken<'id>,
|
|
||||||
runtime: &'ctx mut R,
|
|
||||||
global: &'ctx HashMap<StringId, MaybeThunk>,
|
|
||||||
extra_scope: Option<Scope<'ctx, 'id, 'ir>>,
|
|
||||||
thunk_count: &'ctx mut usize,
|
|
||||||
source: Source,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
|
||||||
bump,
|
|
||||||
token,
|
|
||||||
runtime,
|
|
||||||
source,
|
|
||||||
scopes: std::iter::once(Scope::Global(global))
|
|
||||||
.chain(extra_scope)
|
|
||||||
.collect(),
|
|
||||||
thunk_count,
|
|
||||||
arg_count: 0,
|
|
||||||
with_stack: Vec::new(),
|
|
||||||
thunk_scopes: vec![ThunkScope::new_in(bump)],
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'ctx: 'ir, 'id, 'ir, R: VmRuntimeCtx> DowngradeContext<'id, 'ir>
|
|
||||||
for DowngradeCtx<'ctx, 'id, 'ir, R>
|
|
||||||
{
|
|
||||||
fn new_expr(&self, expr: Ir<'ir, GhostRoRef<'id, 'ir>>) -> GhostRoIrRef<'id, 'ir> {
|
|
||||||
self.bump.alloc(GhostCell::new(expr).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn maybe_thunk(&mut self, ir: GhostRoIrRef<'id, 'ir>) -> GhostRoMaybeThunkRef<'id, 'ir> {
|
|
||||||
use MaybeThunk::*;
|
|
||||||
let expr = (|| {
|
|
||||||
let expr = match *ir.borrow(&self.token) {
|
|
||||||
Ir::Builtin(x) => Builtin(x),
|
|
||||||
Ir::Int(x) => Int(x),
|
|
||||||
Ir::Float(x) => Float(x),
|
|
||||||
Ir::Bool(x) => Bool(x),
|
|
||||||
Ir::Str(x) => Str(x),
|
|
||||||
Ir::Arg { layer } => Arg { layer },
|
|
||||||
Ir::Builtins => Builtins,
|
|
||||||
Ir::Null => Null,
|
|
||||||
Ir::MaybeThunk(thunk) => return Some(thunk),
|
|
||||||
_ => return None,
|
|
||||||
};
|
|
||||||
Some(self.bump.alloc(GhostCell::new(expr).into()))
|
|
||||||
})();
|
|
||||||
if let Some(thunk) = expr {
|
|
||||||
return thunk;
|
|
||||||
}
|
|
||||||
let id = ThunkId(*self.thunk_count);
|
|
||||||
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
|
|
||||||
self.thunk_scopes
|
|
||||||
.last_mut()
|
|
||||||
.expect("no active cache scope")
|
|
||||||
.add_binding(id, ir);
|
|
||||||
self.bump.alloc(GhostCell::new(Thunk(id)).into())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn intern_string(&mut self, sym: impl AsRef<str>) -> StringId {
|
|
||||||
self.runtime.intern_string(sym)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn resolve_sym(&self, id: StringId) -> Symbol<'_> {
|
|
||||||
self.runtime.resolve_string(id).into()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn lookup(
|
|
||||||
&mut self,
|
|
||||||
sym: StringId,
|
|
||||||
span: rnix::TextRange,
|
|
||||||
) -> Result<GhostRoMaybeThunkRef<'id, 'ir>> {
|
|
||||||
for scope in self.scopes.iter().rev() {
|
|
||||||
match scope {
|
|
||||||
&Scope::Global(global_scope) => {
|
|
||||||
if let Some(expr) = global_scope.get(&sym) {
|
|
||||||
return Ok(expr.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&Scope::Repl(repl_bindings) => {
|
|
||||||
if repl_bindings.contains(&sym) {
|
|
||||||
return Ok(self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(MaybeThunk::ReplBinding(sym)).into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Scope::ScopedImport { keys, .. } => {
|
|
||||||
if keys.contains(&sym) {
|
|
||||||
return Ok(self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(MaybeThunk::ScopedImportBinding(sym)).into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Scope::Let(let_scope) => {
|
|
||||||
if let Some(&expr) = let_scope.get(&sym) {
|
|
||||||
return Ok(expr.into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
&Scope::Param {
|
|
||||||
sym: param_sym,
|
|
||||||
abs_layer,
|
|
||||||
} => {
|
|
||||||
if param_sym == sym {
|
|
||||||
let layers: u8 =
|
|
||||||
self.thunk_scopes.len().try_into().expect("scope too deep!");
|
|
||||||
let layer = layers - abs_layer;
|
|
||||||
return Ok(self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(MaybeThunk::Arg { layer }).into()));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !self.with_stack.is_empty() {
|
|
||||||
let id = ThunkId(*self.thunk_count);
|
|
||||||
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
|
|
||||||
let mut namespaces =
|
|
||||||
bumpalo::collections::Vec::with_capacity_in(self.with_stack.len(), self.bump);
|
|
||||||
namespaces.extend(self.with_stack.iter().rev().copied());
|
|
||||||
let body = self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(Ir::WithLookup { sym, namespaces }).into());
|
|
||||||
self.thunk_scopes
|
|
||||||
.last_mut()
|
|
||||||
.expect("no active thunk scope")
|
|
||||||
.add_binding(id, body);
|
|
||||||
Ok(self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(MaybeThunk::Thunk(id)).into()))
|
|
||||||
} else {
|
|
||||||
Err(Error::downgrade_error(
|
|
||||||
format!("'{}' not found", self.resolve_sym(sym)),
|
|
||||||
self.get_current_source(),
|
|
||||||
span,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_current_source(&self) -> Source {
|
|
||||||
self.source.clone()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_let_scope<F, Ret>(&mut self, keys: &[StringId], f: F) -> Result<Ret>
|
|
||||||
where
|
|
||||||
F: FnOnce(
|
|
||||||
&mut Self,
|
|
||||||
) -> Result<(
|
|
||||||
bumpalo::collections::Vec<'ir, GhostRoMaybeThunkRef<'id, 'ir>>,
|
|
||||||
Ret,
|
|
||||||
)>,
|
|
||||||
{
|
|
||||||
let base = *self.thunk_count;
|
|
||||||
*self.thunk_count = self
|
|
||||||
.thunk_count
|
|
||||||
.checked_add(keys.len())
|
|
||||||
.expect("thunk id overflow");
|
|
||||||
let handles = (base..base + keys.len())
|
|
||||||
.map(|id| {
|
|
||||||
&*self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(MaybeThunk::Thunk(ThunkId(id))))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let scope = keys.iter().copied().zip(handles.iter().copied()).collect();
|
|
||||||
self.scopes.push(Scope::Let(scope));
|
|
||||||
let (vals, ret) = { f(self)? };
|
|
||||||
self.scopes.pop();
|
|
||||||
assert_eq!(keys.len(), vals.len());
|
|
||||||
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
|
|
||||||
for (i, (val, handle)) in vals.into_iter().zip(handles).enumerate() {
|
|
||||||
let thunk = *val.borrow(&self.token);
|
|
||||||
*handle.borrow_mut(&mut self.token) = thunk;
|
|
||||||
let id = ThunkId(base + i);
|
|
||||||
let ir_ref = self
|
|
||||||
.bump
|
|
||||||
.alloc(GhostCell::new(Ir::MaybeThunk(handle.into())).into());
|
|
||||||
scope.add_binding(id, ir_ref);
|
|
||||||
}
|
|
||||||
Ok(ret)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_param_scope<F, Ret>(&mut self, sym: StringId, f: F) -> Ret
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Self) -> Ret,
|
|
||||||
{
|
|
||||||
self.scopes.push(Scope::Param {
|
|
||||||
sym,
|
|
||||||
abs_layer: self.thunk_scopes.len().try_into().expect("scope too deep!"),
|
|
||||||
});
|
|
||||||
let mut guard = ScopeGuard { ctx: self };
|
|
||||||
f(guard.as_ctx())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_with_scope<F, Ret>(&mut self, namespace: GhostRoMaybeThunkRef<'id, 'ir>, f: F) -> Ret
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Self) -> Ret,
|
|
||||||
{
|
|
||||||
self.with_stack.push(namespace);
|
|
||||||
let ret = f(self);
|
|
||||||
self.with_stack.pop();
|
|
||||||
ret
|
|
||||||
}
|
|
||||||
|
|
||||||
fn with_thunk_scope<F, Ret>(
|
|
||||||
&mut self,
|
|
||||||
f: F,
|
|
||||||
) -> (
|
|
||||||
Ret,
|
|
||||||
bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
|
|
||||||
)
|
|
||||||
where
|
|
||||||
F: FnOnce(&mut Self) -> Ret,
|
|
||||||
{
|
|
||||||
if self.thunk_scopes.len() == u8::MAX as usize {
|
|
||||||
panic!("scope too deep!");
|
|
||||||
}
|
|
||||||
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
|
|
||||||
let ret = f(self);
|
|
||||||
(
|
|
||||||
ret,
|
|
||||||
self.thunk_scopes
|
|
||||||
.pop()
|
|
||||||
.expect("no thunk scope left???")
|
|
||||||
.bindings,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
fn bump(&self) -> &'ir bumpalo::Bump {
|
|
||||||
self.bump
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'id, 'ir, 'ctx: 'ir, R: VmRuntimeCtx> DowngradeCtx<'ctx, 'id, 'ir, R> {
|
|
||||||
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
|
|
||||||
let body = root.downgrade(&mut self)?;
|
|
||||||
let thunks = self
|
|
||||||
.thunk_scopes
|
|
||||||
.pop()
|
|
||||||
.expect("no thunk scope left???")
|
|
||||||
.bindings;
|
|
||||||
Ok(Ir::freeze(
|
|
||||||
self.new_expr(Ir::TopLevel { body, thunks }),
|
|
||||||
self.token,
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ThunkScope<'id, 'ir> {
|
|
||||||
bindings: bumpalo::collections::Vec<'ir, (ThunkId, GhostRoIrRef<'id, 'ir>)>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'id, 'ir> ThunkScope<'id, 'ir> {
|
|
||||||
fn new_in(bump: &'ir Bump) -> Self {
|
|
||||||
Self {
|
|
||||||
bindings: bumpalo::collections::Vec::new_in(bump),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_binding(&mut self, id: ThunkId, ir: GhostRoIrRef<'id, 'ir>) {
|
|
||||||
self.bindings.push((id, ir));
|
|
||||||
}
|
|
||||||
|
|
||||||
fn extend_bindings(
|
|
||||||
&mut self,
|
|
||||||
iter: impl IntoIterator<Item = (ThunkId, GhostRoIrRef<'id, 'ir>)>,
|
|
||||||
) {
|
|
||||||
self.bindings.extend(iter);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
enum Scope<'ctx, 'id, 'ir> {
|
|
||||||
Global(&'ctx HashMap<StringId, MaybeThunk>),
|
|
||||||
Repl(&'ctx HashSet<StringId>),
|
|
||||||
ScopedImport {
|
|
||||||
keys: HashSet<StringId>,
|
|
||||||
slot_id: u32,
|
|
||||||
},
|
|
||||||
Let(HashMap<StringId, GhostMaybeThunkRef<'id, 'ir>>),
|
|
||||||
Param {
|
|
||||||
sym: StringId,
|
|
||||||
abs_layer: u8,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
pub enum ExtraScope<'ctx> {
|
|
||||||
Repl(&'ctx HashSet<StringId>),
|
|
||||||
ScopedImport {
|
|
||||||
keys: HashSet<StringId>,
|
|
||||||
slot_id: u32,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'ctx> From<ExtraScope<'ctx>> for Scope<'ctx, '_, '_> {
|
|
||||||
fn from(value: ExtraScope<'ctx>) -> Self {
|
|
||||||
use ExtraScope::*;
|
|
||||||
match value {
|
|
||||||
ScopedImport { keys, slot_id } => Scope::ScopedImport { keys, slot_id },
|
|
||||||
Repl(scope) => Scope::Repl(scope),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct ScopeGuard<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> {
|
|
||||||
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir, R>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> Drop for ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
|
|
||||||
fn drop(&mut self) {
|
|
||||||
self.ctx.scopes.pop();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl<'a, 'ctx, 'id, 'ir, R: VmRuntimeCtx> ScopeGuard<'a, 'ctx, 'id, 'ir, R> {
|
|
||||||
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir, R> {
|
|
||||||
self.ctx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct OwnedIr {
|
|
||||||
_bump: Bump,
|
|
||||||
ir: RawIrRef<'static>,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl OwnedIr {
|
|
||||||
/// # Safety
|
|
||||||
/// `ir` must be an allocation backed by `bump`. The reference's
|
|
||||||
/// lifetime is extended to `'static` as a placeholder; the stored IR
|
|
||||||
/// must only be re-borrowed via [`OwnedIr::as_ref`], which narrows
|
|
||||||
/// the lifetime back to that of the `&self` borrow. Moving `bump`
|
|
||||||
/// into the struct keeps the underlying allocation live for the
|
|
||||||
/// lifetime of the `OwnedIr`.
|
|
||||||
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
|
|
||||||
Self {
|
|
||||||
_bump: bump,
|
|
||||||
// SAFETY: see function docs - caller guarantees `ir` is in `bump`,
|
|
||||||
// and the `'static` lifetime is a placeholder narrowed by `as_ref`.
|
|
||||||
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn as_ref<'ir>(&'ir self) -> RawIrRef<'ir> {
|
|
||||||
// SAFETY: narrows the placeholder `'static` lifetime stored in
|
|
||||||
// `self.ir` down to `'ir = &'ir self`. Lifetime shortening is
|
|
||||||
// logically sound for covariant positions; the transmute is only
|
|
||||||
// needed because `RawRef<'ir>` carries `'ir` through a GAT
|
|
||||||
// (`Ref::Ref<T>`), which prevents the compiler from inferring
|
|
||||||
// covariance automatically. The bump arena that backs the IR is
|
|
||||||
// owned by `self._bump`, so the data is live for at least `'ir`.
|
|
||||||
unsafe { std::mem::transmute::<RawIrRef<'static>, RawIrRef<'ir>>(self.ir) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl DisassemblerContext for Evaluator {
|
|
||||||
fn get_code(&self) -> &[u8] {
|
|
||||||
&self.code.bytecode
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::unwrap_used)]
|
|
||||||
fn resolve_string(&self, id: u32) -> &str {
|
|
||||||
let id = string_interner::symbol::SymbolU32::try_from_usize(id as usize).unwrap();
|
|
||||||
self.runtime.strings.resolve(id).unwrap()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,326 +0,0 @@
|
|||||||
#![allow(non_snake_case)]
|
|
||||||
|
|
||||||
use std::path::PathBuf;
|
|
||||||
|
|
||||||
use fix::Evaluator;
|
|
||||||
use fix_common::Value;
|
|
||||||
use fix_error::{Source, SourceType};
|
|
||||||
|
|
||||||
fn get_lang_dir() -> PathBuf {
|
|
||||||
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/tests/lang")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn eval_file(name: &str) -> Result<(Value, Source), String> {
|
|
||||||
let lang_dir = get_lang_dir();
|
|
||||||
let nix_path = lang_dir.join(format!("{name}.nix"));
|
|
||||||
|
|
||||||
let mut ctx = Evaluator::new();
|
|
||||||
let content = std::fs::read_to_string(&nix_path).unwrap();
|
|
||||||
let source = Source {
|
|
||||||
ty: SourceType::File(nix_path.into()),
|
|
||||||
src: content.into(),
|
|
||||||
};
|
|
||||||
ctx.eval_deep(source.clone())
|
|
||||||
.map(|val| (val, source))
|
|
||||||
.map_err(|e| e.to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn read_expected(name: &str) -> String {
|
|
||||||
let lang_dir = get_lang_dir();
|
|
||||||
let exp_path = lang_dir.join(format!("{name}.exp"));
|
|
||||||
std::fs::read_to_string(exp_path)
|
|
||||||
.expect("expected file should exist")
|
|
||||||
.trim_end()
|
|
||||||
.to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn format_value(value: &Value) -> String {
|
|
||||||
value.display_compat().to_string()
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! eval_okay_test {
|
|
||||||
($(#[$attr:meta])* $name:ident$(, $pre:expr)?) => {
|
|
||||||
$(#[$attr])*
|
|
||||||
#[test_log::test]
|
|
||||||
fn $name() {
|
|
||||||
$(($pre)();)?
|
|
||||||
let test_name = concat!("eval-okay-", stringify!($name))
|
|
||||||
.replace("_", "-")
|
|
||||||
.replace("r#", "");
|
|
||||||
let result = eval_file(&test_name);
|
|
||||||
match result {
|
|
||||||
Ok((value, source)) => {
|
|
||||||
let actual = format_value(&value);
|
|
||||||
let actual = actual.replace(
|
|
||||||
source
|
|
||||||
.get_dir()
|
|
||||||
.parent()
|
|
||||||
.unwrap()
|
|
||||||
.to_string_lossy()
|
|
||||||
.as_ref(),
|
|
||||||
"/pwd",
|
|
||||||
);
|
|
||||||
let expected = read_expected(&test_name);
|
|
||||||
assert_eq!(actual, expected, "Output mismatch for {}", test_name);
|
|
||||||
}
|
|
||||||
Err(e) => {
|
|
||||||
panic!("Test {} failed to evaluate: {}", test_name, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! eval_fail_test {
|
|
||||||
($name:ident) => {
|
|
||||||
#[test_log::test]
|
|
||||||
fn $name() {
|
|
||||||
let test_name = concat!("eval-fail-", stringify!($name))
|
|
||||||
.replace("_", "-")
|
|
||||||
.replace("r#", "");
|
|
||||||
let result = eval_file(&test_name);
|
|
||||||
assert!(
|
|
||||||
result.is_err(),
|
|
||||||
"Test {} should have failed but succeeded with: {:?}",
|
|
||||||
test_name,
|
|
||||||
result
|
|
||||||
);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
mod okay {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
eval_okay_test!(any_all);
|
|
||||||
eval_okay_test!(arithmetic);
|
|
||||||
eval_okay_test!(attrnames);
|
|
||||||
eval_okay_test!(attrs);
|
|
||||||
eval_okay_test!(attrs2);
|
|
||||||
eval_okay_test!(attrs3);
|
|
||||||
eval_okay_test!(attrs4);
|
|
||||||
eval_okay_test!(attrs5);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "__overrides is not supported"]
|
|
||||||
attrs6
|
|
||||||
);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "requires --arg/--argstr CLI flags"]
|
|
||||||
autoargs
|
|
||||||
);
|
|
||||||
eval_okay_test!(backslash_newline_1);
|
|
||||||
eval_okay_test!(backslash_newline_2);
|
|
||||||
eval_okay_test!(baseNameOf);
|
|
||||||
eval_okay_test!(builtins);
|
|
||||||
eval_okay_test!(builtins_add);
|
|
||||||
eval_okay_test!(callable_attrs);
|
|
||||||
eval_okay_test!(catattrs);
|
|
||||||
eval_okay_test!(closure);
|
|
||||||
eval_okay_test!(comments);
|
|
||||||
eval_okay_test!(concat);
|
|
||||||
eval_okay_test!(concatmap);
|
|
||||||
eval_okay_test!(concatstringssep);
|
|
||||||
eval_okay_test!(context);
|
|
||||||
eval_okay_test!(context_introspection);
|
|
||||||
eval_okay_test!(convertHash);
|
|
||||||
eval_okay_test!(curpos);
|
|
||||||
eval_okay_test!(deepseq);
|
|
||||||
eval_okay_test!(delayed_with);
|
|
||||||
eval_okay_test!(delayed_with_inherit);
|
|
||||||
eval_okay_test!(deprecate_cursed_or);
|
|
||||||
eval_okay_test!(derivation_legacy);
|
|
||||||
eval_okay_test!(dynamic_attrs);
|
|
||||||
eval_okay_test!(dynamic_attrs_2);
|
|
||||||
eval_okay_test!(dynamic_attrs_bare);
|
|
||||||
eval_okay_test!(elem);
|
|
||||||
eval_okay_test!(empty_args);
|
|
||||||
eval_okay_test!(eq);
|
|
||||||
eval_okay_test!(eq_derivations);
|
|
||||||
eval_okay_test!(filter);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "not implemented: flakeRefToString"]
|
|
||||||
flake_ref_to_string
|
|
||||||
);
|
|
||||||
eval_okay_test!(flatten);
|
|
||||||
eval_okay_test!(float);
|
|
||||||
eval_okay_test!(floor_ceil);
|
|
||||||
eval_okay_test!(foldlStrict);
|
|
||||||
eval_okay_test!(foldlStrict_lazy_elements);
|
|
||||||
eval_okay_test!(foldlStrict_lazy_initial_accumulator);
|
|
||||||
eval_okay_test!(fromjson);
|
|
||||||
eval_okay_test!(fromjson_escapes);
|
|
||||||
eval_okay_test!(fromTOML);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "timestamps are not supported"]
|
|
||||||
fromTOML_timestamps
|
|
||||||
);
|
|
||||||
eval_okay_test!(functionargs);
|
|
||||||
eval_okay_test!(hashfile);
|
|
||||||
eval_okay_test!(hashstring);
|
|
||||||
eval_okay_test!(getattrpos);
|
|
||||||
eval_okay_test!(getattrpos_functionargs);
|
|
||||||
eval_okay_test!(getattrpos_undefined);
|
|
||||||
eval_okay_test!(getenv, || {
|
|
||||||
unsafe { std::env::set_var("TEST_VAR", "foo") };
|
|
||||||
});
|
|
||||||
eval_okay_test!(groupBy);
|
|
||||||
eval_okay_test!(r#if);
|
|
||||||
eval_okay_test!(ind_string);
|
|
||||||
eval_okay_test!(import);
|
|
||||||
eval_okay_test!(inherit_attr_pos);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "__overrides is not supported"]
|
|
||||||
inherit_from
|
|
||||||
);
|
|
||||||
eval_okay_test!(intersectAttrs);
|
|
||||||
eval_okay_test!(r#let);
|
|
||||||
eval_okay_test!(list);
|
|
||||||
eval_okay_test!(listtoattrs);
|
|
||||||
eval_okay_test!(logic);
|
|
||||||
eval_okay_test!(map);
|
|
||||||
eval_okay_test!(mapattrs);
|
|
||||||
eval_okay_test!(merge_dynamic_attrs);
|
|
||||||
eval_okay_test!(nested_with);
|
|
||||||
eval_okay_test!(new_let);
|
|
||||||
eval_okay_test!(null_dynamic_attrs);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "__overrides is not supported"]
|
|
||||||
overrides
|
|
||||||
);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "not implemented: parseFlakeRef"]
|
|
||||||
parse_flake_ref
|
|
||||||
);
|
|
||||||
eval_okay_test!(partition);
|
|
||||||
eval_okay_test!(path);
|
|
||||||
eval_okay_test!(pathexists);
|
|
||||||
eval_okay_test!(path_string_interpolation, || {
|
|
||||||
unsafe {
|
|
||||||
std::env::set_var("HOME", "/fake-home");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
eval_okay_test!(patterns);
|
|
||||||
eval_okay_test!(print);
|
|
||||||
eval_okay_test!(readDir);
|
|
||||||
eval_okay_test!(readfile);
|
|
||||||
eval_okay_test!(readFileType);
|
|
||||||
eval_okay_test!(redefine_builtin);
|
|
||||||
eval_okay_test!(regex_match);
|
|
||||||
eval_okay_test!(regex_split);
|
|
||||||
eval_okay_test!(regression_20220122);
|
|
||||||
eval_okay_test!(regression_20220125);
|
|
||||||
eval_okay_test!(regrettable_rec_attrset_merge);
|
|
||||||
eval_okay_test!(remove);
|
|
||||||
eval_okay_test!(repeated_empty_attrs);
|
|
||||||
eval_okay_test!(repeated_empty_list);
|
|
||||||
eval_okay_test!(replacestrings);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "requires -I CLI flags"]
|
|
||||||
search_path
|
|
||||||
);
|
|
||||||
eval_okay_test!(scope_1);
|
|
||||||
eval_okay_test!(scope_2);
|
|
||||||
eval_okay_test!(scope_3);
|
|
||||||
eval_okay_test!(scope_4);
|
|
||||||
eval_okay_test!(scope_6);
|
|
||||||
eval_okay_test!(scope_7);
|
|
||||||
eval_okay_test!(seq);
|
|
||||||
eval_okay_test!(sort);
|
|
||||||
eval_okay_test!(splitversion);
|
|
||||||
eval_okay_test!(string);
|
|
||||||
eval_okay_test!(strings_as_attrs_names);
|
|
||||||
eval_okay_test!(substring);
|
|
||||||
eval_okay_test!(substring_context);
|
|
||||||
eval_okay_test!(symlink_resolution);
|
|
||||||
eval_okay_test!(
|
|
||||||
#[ignore = "TCO not implemented, also disabled in CppNix"]
|
|
||||||
tail_call_1
|
|
||||||
);
|
|
||||||
eval_okay_test!(tojson);
|
|
||||||
eval_okay_test!(toxml);
|
|
||||||
eval_okay_test!(toxml2);
|
|
||||||
eval_okay_test!(tryeval);
|
|
||||||
eval_okay_test!(types);
|
|
||||||
eval_okay_test!(versions);
|
|
||||||
eval_okay_test!(with);
|
|
||||||
eval_okay_test!(zipAttrsWith);
|
|
||||||
}
|
|
||||||
|
|
||||||
mod fail {
|
|
||||||
use super::*;
|
|
||||||
|
|
||||||
eval_fail_test!(abort);
|
|
||||||
eval_fail_test!(addDrvOutputDependencies_empty_context);
|
|
||||||
eval_fail_test!(addDrvOutputDependencies_multi_elem_context);
|
|
||||||
eval_fail_test!(addDrvOutputDependencies_wrong_element_kind);
|
|
||||||
eval_fail_test!(addErrorRuntime_example);
|
|
||||||
eval_fail_test!(assert);
|
|
||||||
eval_fail_test!(assert_equal_attrs_names);
|
|
||||||
eval_fail_test!(assert_equal_attrs_names_2);
|
|
||||||
eval_fail_test!(assert_equal_derivations);
|
|
||||||
eval_fail_test!(assert_equal_derivations_extra);
|
|
||||||
eval_fail_test!(assert_equal_floats);
|
|
||||||
eval_fail_test!(assert_equal_function_direct);
|
|
||||||
eval_fail_test!(assert_equal_int_float);
|
|
||||||
eval_fail_test!(assert_equal_ints);
|
|
||||||
eval_fail_test!(assert_equal_list_length);
|
|
||||||
eval_fail_test!(assert_equal_paths);
|
|
||||||
eval_fail_test!(assert_equal_type);
|
|
||||||
eval_fail_test!(assert_equal_type_nested);
|
|
||||||
eval_fail_test!(assert_nested_bool);
|
|
||||||
eval_fail_test!(attr_name_type);
|
|
||||||
eval_fail_test!(attrset_merge_drops_later_rec);
|
|
||||||
eval_fail_test!(bad_string_interpolation_1);
|
|
||||||
eval_fail_test!(bad_string_interpolation_2);
|
|
||||||
eval_fail_test!(bad_string_interpolation_3);
|
|
||||||
eval_fail_test!(bad_string_interpolation_4);
|
|
||||||
eval_fail_test!(blackhole);
|
|
||||||
eval_fail_test!(call_primop);
|
|
||||||
eval_fail_test!(deepseq);
|
|
||||||
eval_fail_test!(derivation_name);
|
|
||||||
eval_fail_test!(dup_dynamic_attrs);
|
|
||||||
eval_fail_test!(duplicate_traces);
|
|
||||||
eval_fail_test!(eol_1);
|
|
||||||
eval_fail_test!(eol_2);
|
|
||||||
eval_fail_test!(eol_3);
|
|
||||||
eval_fail_test!(fetchTree_negative);
|
|
||||||
eval_fail_test!(fetchurl_baseName);
|
|
||||||
eval_fail_test!(fetchurl_baseName_attrs);
|
|
||||||
eval_fail_test!(fetchurl_baseName_attrs_name);
|
|
||||||
eval_fail_test!(flake_ref_to_string_negative_integer);
|
|
||||||
eval_fail_test!(foldlStrict_strict_op_application);
|
|
||||||
eval_fail_test!(fromJSON_keyWithNullByte);
|
|
||||||
eval_fail_test!(fromJSON_overflowing);
|
|
||||||
eval_fail_test!(fromJSON_valueWithNullByte);
|
|
||||||
eval_fail_test!(fromTOML_keyWithNullByte);
|
|
||||||
eval_fail_test!(fromTOML_timestamps);
|
|
||||||
eval_fail_test!(fromTOML_valueWithNullByte);
|
|
||||||
eval_fail_test!(hashfile_missing);
|
|
||||||
eval_fail_test!(infinite_recursion_lambda);
|
|
||||||
eval_fail_test!(list);
|
|
||||||
eval_fail_test!(missing_arg);
|
|
||||||
eval_fail_test!(mutual_recursion);
|
|
||||||
eval_fail_test!(nested_list_items);
|
|
||||||
eval_fail_test!(nonexist_path);
|
|
||||||
eval_fail_test!(not_throws);
|
|
||||||
eval_fail_test!(overflowing_add);
|
|
||||||
eval_fail_test!(overflowing_div);
|
|
||||||
eval_fail_test!(overflowing_mul);
|
|
||||||
eval_fail_test!(overflowing_sub);
|
|
||||||
eval_fail_test!(path_slash);
|
|
||||||
eval_fail_test!(pipe_operators);
|
|
||||||
eval_fail_test!(recursion);
|
|
||||||
eval_fail_test!(remove);
|
|
||||||
eval_fail_test!(scope_5);
|
|
||||||
eval_fail_test!(seq);
|
|
||||||
eval_fail_test!(set);
|
|
||||||
eval_fail_test!(set_override);
|
|
||||||
eval_fail_test!(string_nul_1);
|
|
||||||
eval_fail_test!(string_nul_2);
|
|
||||||
eval_fail_test!(substring);
|
|
||||||
eval_fail_test!(toJSON);
|
|
||||||
eval_fail_test!(toJSON_non_utf_8);
|
|
||||||
eval_fail_test!(to_path);
|
|
||||||
eval_fail_test!(undeclared_arg);
|
|
||||||
eval_fail_test!(using_set_as_attr_name);
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[ { } { a = 1; } { a = 1; } { a = "a"; } { m = 1; } { m = "m"; } { n = 1; } { n = "n"; } { n = 1; p = 2; } { n = "n"; p = "p"; } «repeated» «repeated» { a = "a"; b = "b"; c = "c"; d = "d"; e = "e"; f = "f"; g = "g"; h = "h"; i = "i"; j = "j"; k = "k"; l = "l"; m = "m"; n = "n"; o = "o"; p = "p"; q = "q"; r = "r"; s = "s"; t = "t"; u = "u"; v = "v"; w = "w"; x = "x"; y = "y"; z = "z"; } true ]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
[ null <PRIMOP> <PRIMOP-APP> <LAMBDA> [ «repeated» ] ]
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
utils/
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
mod derivation;
|
|
||||||
mod findfile;
|
|
||||||
mod io_operations;
|
|
||||||
mod lang;
|
|
||||||
mod string_context;
|
|
||||||
mod utils;
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use fix::Evaluator;
|
|
||||||
use fix_common::Value;
|
|
||||||
use fix_error::{Result, Source};
|
|
||||||
|
|
||||||
pub fn eval(expr: &str) -> Value {
|
|
||||||
Evaluator::new()
|
|
||||||
.eval(Source::new_eval(expr.into()).unwrap())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_shallow(expr: &str) -> Value {
|
|
||||||
Evaluator::new()
|
|
||||||
.eval_shallow(Source::new_eval(expr.into()).unwrap())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_deep(expr: &str) -> Value {
|
|
||||||
Evaluator::new()
|
|
||||||
.eval_deep(Source::new_eval(expr.into()).unwrap())
|
|
||||||
.unwrap()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_deep_result(expr: &str) -> Result<Value> {
|
|
||||||
Evaluator::new().eval_deep(Source::new_eval(expr.into()).unwrap())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn eval_result(expr: &str) -> Result<Value> {
|
|
||||||
Evaluator::new().eval(Source::new_eval(expr.into()).unwrap())
|
|
||||||
}
|
|
||||||
Generated
+13
-155
@@ -1,64 +1,5 @@
|
|||||||
{
|
{
|
||||||
"nodes": {
|
"nodes": {
|
||||||
"blueprint": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"llm-agents",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"systems": [
|
|
||||||
"llm-agents",
|
|
||||||
"systems"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1776249299,
|
|
||||||
"narHash": "sha256-Dt9t1TGRmJFc0xVYhttNBD6QsAgHOHCArqGa0AyjrJY=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "blueprint",
|
|
||||||
"rev": "56131e8628f173d24a27f6d27c0215eff57e40dd",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "blueprint",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"bun2nix": {
|
|
||||||
"inputs": {
|
|
||||||
"flake-parts": [
|
|
||||||
"llm-agents",
|
|
||||||
"flake-parts"
|
|
||||||
],
|
|
||||||
"nixpkgs": [
|
|
||||||
"llm-agents",
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"systems": [
|
|
||||||
"llm-agents",
|
|
||||||
"systems"
|
|
||||||
],
|
|
||||||
"treefmt-nix": [
|
|
||||||
"llm-agents",
|
|
||||||
"treefmt-nix"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1778445566,
|
|
||||||
"narHash": "sha256-oQvcadh2BCkrog+SGrG6YffKJrveYpjj3TdQJWaKhaM=",
|
|
||||||
"owner": "nix-community",
|
|
||||||
"repo": "bun2nix",
|
|
||||||
"rev": "2499dedd70744dba1815875b854818a3019e9e4c",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-community",
|
|
||||||
"ref": "staging-2.1.0",
|
|
||||||
"repo": "bun2nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"fenix": {
|
"fenix": {
|
||||||
"inputs": {
|
"inputs": {
|
||||||
"nixpkgs": [
|
"nixpkgs": [
|
||||||
@@ -67,11 +8,11 @@
|
|||||||
"rust-analyzer-src": "rust-analyzer-src"
|
"rust-analyzer-src": "rust-analyzer-src"
|
||||||
},
|
},
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778919578,
|
"lastModified": 1770966612,
|
||||||
"narHash": "sha256-+z+jgTly48gsAiX8rOe/vs8C/2G4vdCpcEtqMJUpFqw=",
|
"narHash": "sha256-S6k14z/JsDwX6zZyLucDBTOe/9RsvxH9GTUxHn2o4vc=",
|
||||||
"owner": "nix-community",
|
"owner": "nix-community",
|
||||||
"repo": "fenix",
|
"repo": "fenix",
|
||||||
"rev": "ecd6d4ff22cfdb1339b2915455a2ff4dc85bf52e",
|
"rev": "e90d48dcfaebac7ea7a5687888a2d0733be26343",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -83,70 +24,24 @@
|
|||||||
"flake-compat": {
|
"flake-compat": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1777699697,
|
"lastModified": 1751685974,
|
||||||
"narHash": "sha256-Eg9b/rq/ECYwNwEXs5i9wHyhxNI0JrYx2srdI2uZMaQ=",
|
"narHash": "sha256-NKw96t+BgHIYzHUjkTK95FqYRVKB8DHpVhefWSz/kTw=",
|
||||||
"rev": "382052b74656a369c5408822af3f2501e9b1af81",
|
"rev": "549f2762aebeff29a2e5ece7a7dc0f955281a1d1",
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/382052b74656a369c5408822af3f2501e9b1af81.tar.gz"
|
"url": "https://git.lix.systems/api/v1/repos/lix-project/flake-compat/archive/549f2762aebeff29a2e5ece7a7dc0f955281a1d1.tar.gz"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
|
"url": "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"flake-parts": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs-lib": [
|
|
||||||
"llm-agents",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1778716662,
|
|
||||||
"narHash": "sha256-m1Yf0wZ8j1OHjTc2UwHwyQRSnNeSgLJOd7q5Y45hzi4=",
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"rev": "f7c1a2d347e4c52d5fb8d10cb4d94b5884e546fb",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "hercules-ci",
|
|
||||||
"repo": "flake-parts",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"llm-agents": {
|
|
||||||
"inputs": {
|
|
||||||
"blueprint": "blueprint",
|
|
||||||
"bun2nix": "bun2nix",
|
|
||||||
"flake-parts": "flake-parts",
|
|
||||||
"nixpkgs": [
|
|
||||||
"nixpkgs"
|
|
||||||
],
|
|
||||||
"systems": "systems",
|
|
||||||
"treefmt-nix": "treefmt-nix"
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1778929997,
|
|
||||||
"narHash": "sha256-iAfbBUHBbR0N4DFqFWr4Jtmpc1YcOK7kpVM4f0MK1V8=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "llm-agents.nix",
|
|
||||||
"rev": "0da8f0313c9f68c155e0932f880fc1913e7be846",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "llm-agents.nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"nixpkgs": {
|
"nixpkgs": {
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778443072,
|
"lastModified": 1770841267,
|
||||||
"narHash": "sha256-zi7/fsqM/kFdNuED//4WOCUtezGtKKqRNORjMvfwjnA=",
|
"narHash": "sha256-9xejG0KoqsoKEGp2kVbXRlEYtFFcDTHjidiuX8hGO44=",
|
||||||
"owner": "nixos",
|
"owner": "nixos",
|
||||||
"repo": "nixpkgs",
|
"repo": "nixpkgs",
|
||||||
"rev": "da5ad661ba4e5ef59ba743f0d112cbc30e474f32",
|
"rev": "ec7c70d12ce2fc37cb92aff673dcdca89d187bae",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -160,18 +55,17 @@
|
|||||||
"inputs": {
|
"inputs": {
|
||||||
"fenix": "fenix",
|
"fenix": "fenix",
|
||||||
"flake-compat": "flake-compat",
|
"flake-compat": "flake-compat",
|
||||||
"llm-agents": "llm-agents",
|
|
||||||
"nixpkgs": "nixpkgs"
|
"nixpkgs": "nixpkgs"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"rust-analyzer-src": {
|
"rust-analyzer-src": {
|
||||||
"flake": false,
|
"flake": false,
|
||||||
"locked": {
|
"locked": {
|
||||||
"lastModified": 1778854817,
|
"lastModified": 1770934477,
|
||||||
"narHash": "sha256-iG+VuMy8W585geVVCUd7pR025WsY3ZkgSv5Yt5bxDmQ=",
|
"narHash": "sha256-GX0cINHhhzUbQHyDYN2Mc+ovb6Sx/4yrF95VVou9aW4=",
|
||||||
"owner": "rust-lang",
|
"owner": "rust-lang",
|
||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"rev": "1a68212c5683555ad80f0eab71db9715c6d52145",
|
"rev": "931cd553be123b11db1435ac7ea5657e62e5e601",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
},
|
},
|
||||||
"original": {
|
"original": {
|
||||||
@@ -180,42 +74,6 @@
|
|||||||
"repo": "rust-analyzer",
|
"repo": "rust-analyzer",
|
||||||
"type": "github"
|
"type": "github"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"systems": {
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1681028828,
|
|
||||||
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "nix-systems",
|
|
||||||
"repo": "default",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"treefmt-nix": {
|
|
||||||
"inputs": {
|
|
||||||
"nixpkgs": [
|
|
||||||
"llm-agents",
|
|
||||||
"nixpkgs"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"locked": {
|
|
||||||
"lastModified": 1775636079,
|
|
||||||
"narHash": "sha256-pc20NRoMdiar8oPQceQT47UUZMBTiMdUuWrYu2obUP0=",
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "treefmt-nix",
|
|
||||||
"rev": "790751ff7fd3801feeaf96d7dc416a8d581265ba",
|
|
||||||
"type": "github"
|
|
||||||
},
|
|
||||||
"original": {
|
|
||||||
"owner": "numtide",
|
|
||||||
"repo": "treefmt-nix",
|
|
||||||
"type": "github"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"root": "root",
|
"root": "root",
|
||||||
|
|||||||
@@ -3,30 +3,18 @@
|
|||||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||||
fenix.url = "github:nix-community/fenix";
|
fenix.url = "github:nix-community/fenix";
|
||||||
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
fenix.inputs.nixpkgs.follows = "nixpkgs";
|
||||||
llm-agents = {
|
|
||||||
url = "github:numtide/llm-agents.nix";
|
|
||||||
inputs.nixpkgs.follows = "nixpkgs";
|
|
||||||
};
|
|
||||||
flake-compat = {
|
flake-compat = {
|
||||||
url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz";
|
url = "https://git.lix.systems/lix-project/flake-compat/archive/main.tar.gz";
|
||||||
flake = false;
|
flake = false;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
outputs =
|
outputs = { nixpkgs, fenix, ... }:
|
||||||
inputs@{ nixpkgs, fenix, ... }:
|
|
||||||
let
|
let
|
||||||
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
forAllSystems = nixpkgs.lib.genAttrs nixpkgs.lib.systems.flakeExposed;
|
||||||
in
|
in
|
||||||
{
|
{
|
||||||
devShells = forAllSystems (
|
devShells = forAllSystems (system:
|
||||||
system:
|
let pkgs = import nixpkgs { inherit system; config.allowUnfree = true; }; in
|
||||||
let
|
|
||||||
pkgs = import nixpkgs {
|
|
||||||
inherit system;
|
|
||||||
config.allowUnfree = true;
|
|
||||||
};
|
|
||||||
llm-agents = inputs.llm-agents.packages.${pkgs.stdenv.hostPlatform.system};
|
|
||||||
in
|
|
||||||
{
|
{
|
||||||
default = pkgs.mkShell {
|
default = pkgs.mkShell {
|
||||||
packages = with pkgs; [
|
packages = with pkgs; [
|
||||||
@@ -38,20 +26,23 @@
|
|||||||
"rustfmt"
|
"rustfmt"
|
||||||
"rust-analyzer"
|
"rust-analyzer"
|
||||||
])
|
])
|
||||||
|
cargo-outdated
|
||||||
cargo-machete
|
cargo-machete
|
||||||
cargo-bloat
|
|
||||||
lldb
|
lldb
|
||||||
valgrind
|
valgrind
|
||||||
kdePackages.kcachegrind
|
|
||||||
hyperfine
|
hyperfine
|
||||||
just
|
just
|
||||||
samply
|
samply
|
||||||
|
jq
|
||||||
tokei
|
tokei
|
||||||
|
|
||||||
llm-agents.codex
|
nodejs
|
||||||
llm-agents.claude-code
|
nodePackages.npm
|
||||||
llm-agents.opencode
|
biome
|
||||||
llm-agents.forge
|
|
||||||
|
claude-code
|
||||||
|
codex
|
||||||
|
opencode
|
||||||
];
|
];
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
[package]
|
||||||
|
name = "nix-js-macros"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[lib]
|
||||||
|
proc-macro = true
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
convert_case = "0.11"
|
||||||
|
quote = "1.0"
|
||||||
|
syn = { version = "2.0", features = ["full"] }
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
//! Implements the `ir!` procedural macro.
|
||||||
|
//!
|
||||||
|
//! This macro is designed to reduce the boilerplate associated with defining
|
||||||
|
//! an Intermediate Representation (IR) that follows a specific pattern. It generates:
|
||||||
|
//! 1. An enum representing the different kinds of IR nodes.
|
||||||
|
//! 2. Structs for each of the variants that have fields.
|
||||||
|
//! 3. `Ref` and `Mut` versions of the main enum for ergonomic pattern matching on references.
|
||||||
|
//! 4. `From` implementations to easily convert from a struct variant (e.g., `BinOp`) to the main enum (`Ir::BinOp`).
|
||||||
|
//! 5. A `To[IrName]` trait to provide a convenient `.to_ir()` method on the variant structs.
|
||||||
|
|
||||||
|
use convert_case::{Case, Casing};
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
use quote::{format_ident, quote};
|
||||||
|
use syn::{
|
||||||
|
FieldsNamed, Ident, Token, Type, parenthesized,
|
||||||
|
parse::{Parse, ParseStream, Result},
|
||||||
|
punctuated::Punctuated,
|
||||||
|
token,
|
||||||
|
};
|
||||||
|
|
||||||
|
/// Represents one of the variants passed to the `ir!` macro.
|
||||||
|
pub enum VariantInput {
|
||||||
|
/// A unit-like variant, e.g., `Arg`.
|
||||||
|
Unit(Ident),
|
||||||
|
/// A tuple-like variant with one unnamed field, e.g., `ExprRef(ExprId)`.
|
||||||
|
Tuple(Ident, Type),
|
||||||
|
/// A struct-like variant with named fields, e.g., `BinOp { lhs: ExprId, rhs: ExprId, kind: BinOpKind }`.
|
||||||
|
Struct(Ident, FieldsNamed),
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The top-level input for the `ir!` macro.
|
||||||
|
pub struct MacroInput {
|
||||||
|
/// The name of the main IR enum to be generated (e.g., `Ir`).
|
||||||
|
pub base_name: Ident,
|
||||||
|
/// The list of variants for the enum.
|
||||||
|
pub variants: Punctuated<VariantInput, Token![,]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for VariantInput {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
let name: Ident = input.parse()?;
|
||||||
|
|
||||||
|
if input.peek(token::Paren) {
|
||||||
|
// Parse a tuple-like variant: `Variant(Type)`
|
||||||
|
let content;
|
||||||
|
parenthesized!(content in input);
|
||||||
|
let ty: Type = content.parse()?;
|
||||||
|
|
||||||
|
if !content.is_empty() {
|
||||||
|
return Err(content.error("Expected a single type inside parentheses"));
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(VariantInput::Tuple(name, ty))
|
||||||
|
} else if input.peek(token::Brace) {
|
||||||
|
// Parse a struct-like variant: `Variant { field: Type, ... }`
|
||||||
|
let fields: FieldsNamed = input.parse()?;
|
||||||
|
Ok(VariantInput::Struct(name, fields))
|
||||||
|
} else {
|
||||||
|
// Parse a unit-like variant: `Variant`
|
||||||
|
Ok(VariantInput::Unit(name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Parse for MacroInput {
|
||||||
|
fn parse(input: ParseStream) -> Result<Self> {
|
||||||
|
// The macro input is expected to be: `IrName, Variant1, Variant2, ...`
|
||||||
|
let base_name = input.parse()?;
|
||||||
|
input.parse::<Token![,]>()?;
|
||||||
|
let variants = Punctuated::parse_terminated(input)?;
|
||||||
|
|
||||||
|
Ok(MacroInput {
|
||||||
|
base_name,
|
||||||
|
variants,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The implementation of the `ir!` macro.
|
||||||
|
pub fn ir_impl(input: TokenStream) -> TokenStream {
|
||||||
|
let parsed_input = syn::parse_macro_input!(input as MacroInput);
|
||||||
|
|
||||||
|
let base_name = &parsed_input.base_name;
|
||||||
|
let ref_name = format_ident!("{}Ref", base_name);
|
||||||
|
let mut_name = format_ident!("{}Mut", base_name);
|
||||||
|
let to_trait_name = format_ident!("To{}", base_name);
|
||||||
|
let to_trait_fn_name = format_ident!("to_{}", base_name.to_string().to_case(Case::Snake));
|
||||||
|
|
||||||
|
let mut enum_variants = Vec::new();
|
||||||
|
let mut struct_defs = Vec::new();
|
||||||
|
let mut ref_variants = Vec::new();
|
||||||
|
let mut mut_variants = Vec::new();
|
||||||
|
let mut as_ref_arms = Vec::new();
|
||||||
|
let mut as_mut_arms = Vec::new();
|
||||||
|
let mut span_arms = Vec::new();
|
||||||
|
let mut from_impls = Vec::new();
|
||||||
|
let mut to_trait_impls = Vec::new();
|
||||||
|
|
||||||
|
for variant in parsed_input.variants {
|
||||||
|
match variant {
|
||||||
|
VariantInput::Unit(name) => {
|
||||||
|
let inner_type = name.clone();
|
||||||
|
|
||||||
|
struct_defs.push(quote! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct #name {
|
||||||
|
pub span: rnix::TextRange,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
enum_variants.push(quote! { #name(#inner_type) });
|
||||||
|
ref_variants.push(quote! { #name(&'a #inner_type) });
|
||||||
|
mut_variants.push(quote! { #name(&'a mut #inner_type) });
|
||||||
|
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
|
||||||
|
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
|
||||||
|
span_arms.push(quote! { Self::#name(inner) => inner.span });
|
||||||
|
from_impls.push(quote! {
|
||||||
|
impl From<#inner_type> for #base_name {
|
||||||
|
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
to_trait_impls.push(quote! {
|
||||||
|
impl #to_trait_name for #name {
|
||||||
|
fn #to_trait_fn_name(self) -> #base_name { #base_name::from(self) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
VariantInput::Tuple(name, ty) => {
|
||||||
|
let field_name = format_ident!("inner");
|
||||||
|
|
||||||
|
struct_defs.push(quote! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct #name {
|
||||||
|
pub #field_name: #ty,
|
||||||
|
pub span: rnix::TextRange,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
let inner_type = name.clone();
|
||||||
|
enum_variants.push(quote! { #name(#inner_type) });
|
||||||
|
ref_variants.push(quote! { #name(&'a #inner_type) });
|
||||||
|
mut_variants.push(quote! { #name(&'a mut #inner_type) });
|
||||||
|
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
|
||||||
|
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
|
||||||
|
span_arms.push(quote! { Self::#name(inner) => inner.span });
|
||||||
|
from_impls.push(quote! {
|
||||||
|
impl From<#inner_type> for #base_name {
|
||||||
|
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
to_trait_impls.push(quote! {
|
||||||
|
impl #to_trait_name for #name {
|
||||||
|
fn #to_trait_fn_name(self) -> #base_name { #base_name::from(self) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
VariantInput::Struct(name, mut fields) => {
|
||||||
|
let inner_type = name.clone();
|
||||||
|
|
||||||
|
fields.named.push(syn::Field {
|
||||||
|
attrs: vec![],
|
||||||
|
vis: syn::Visibility::Public(syn::token::Pub::default()),
|
||||||
|
mutability: syn::FieldMutability::None,
|
||||||
|
ident: Some(format_ident!("span")),
|
||||||
|
colon_token: Some(syn::token::Colon::default()),
|
||||||
|
ty: syn::parse_quote!(rnix::TextRange),
|
||||||
|
});
|
||||||
|
|
||||||
|
struct_defs.push(quote! {
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct #name #fields
|
||||||
|
});
|
||||||
|
enum_variants.push(quote! { #name(#inner_type) });
|
||||||
|
ref_variants.push(quote! { #name(&'a #inner_type) });
|
||||||
|
mut_variants.push(quote! { #name(&'a mut #inner_type) });
|
||||||
|
as_ref_arms.push(quote! { Self::#name(inner) => #ref_name::#name(inner) });
|
||||||
|
as_mut_arms.push(quote! { Self::#name(inner) => #mut_name::#name(inner) });
|
||||||
|
span_arms.push(quote! { Self::#name(inner) => inner.span });
|
||||||
|
from_impls.push(quote! {
|
||||||
|
impl From<#inner_type> for #base_name {
|
||||||
|
fn from(val: #inner_type) -> Self { #base_name::#name(val) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
to_trait_impls.push(quote! {
|
||||||
|
impl #to_trait_name for #name {
|
||||||
|
fn #to_trait_fn_name(self) -> #base_name { #base_name::from(self) }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assemble the final generated code.
|
||||||
|
let expanded = quote! {
|
||||||
|
/// The main IR enum, generated by the `ir!` macro.
|
||||||
|
#[derive(Debug, IsVariant, Unwrap, TryUnwrap)]
|
||||||
|
pub enum #base_name {
|
||||||
|
#( #enum_variants ),*
|
||||||
|
}
|
||||||
|
|
||||||
|
// The struct definitions for the enum variants.
|
||||||
|
#( #struct_defs )*
|
||||||
|
|
||||||
|
/// An immutable reference version of the IR enum.
|
||||||
|
#[derive(Debug, IsVariant, Unwrap, TryUnwrap)]
|
||||||
|
pub enum #ref_name<'a> {
|
||||||
|
#( #ref_variants ),*
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A mutable reference version of the IR enum.
|
||||||
|
#[derive(Debug, IsVariant, Unwrap, TryUnwrap)]
|
||||||
|
pub enum #mut_name<'a> {
|
||||||
|
#( #mut_variants ),*
|
||||||
|
}
|
||||||
|
|
||||||
|
impl #base_name {
|
||||||
|
/// Converts a `&Ir` into a `IrRef`.
|
||||||
|
pub fn as_ref(&self) -> #ref_name<'_> {
|
||||||
|
match self {
|
||||||
|
#( #as_ref_arms ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a `&mut Ir` into a `IrMut`.
|
||||||
|
pub fn as_mut(&mut self) -> #mut_name<'_> {
|
||||||
|
match self {
|
||||||
|
#( #as_mut_arms ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn span(&self) -> rnix::TextRange {
|
||||||
|
match self {
|
||||||
|
#( #span_arms ),*
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// `From` implementations for converting variant structs into the main enum.
|
||||||
|
#( #from_impls )*
|
||||||
|
|
||||||
|
/// A trait for converting a variant struct into the main IR enum.
|
||||||
|
pub trait #to_trait_name {
|
||||||
|
/// Performs the conversion.
|
||||||
|
fn #to_trait_fn_name(self) -> #base_name;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement the `ToIr` trait for each variant struct.
|
||||||
|
#( #to_trait_impls )*
|
||||||
|
};
|
||||||
|
|
||||||
|
TokenStream::from(expanded)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
//! This crate provides procedural macros for the nix-js project.
|
||||||
|
use proc_macro::TokenStream;
|
||||||
|
|
||||||
|
mod ir;
|
||||||
|
|
||||||
|
/// A procedural macro to reduce boilerplate when defining an Intermediate Representation (IR).
|
||||||
|
///
|
||||||
|
/// It generates an enum for the IR, along with `Ref` and `Mut` variants,
|
||||||
|
/// `From` implementations, and a `ToIr` trait.
|
||||||
|
#[proc_macro]
|
||||||
|
pub fn ir(input: TokenStream) -> TokenStream {
|
||||||
|
ir::ir_impl(input)
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
[package]
|
||||||
|
name = "nix-js"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
build = "build.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mimalloc = "0.1"
|
||||||
|
|
||||||
|
tokio = { version = "1.41", features = ["rt-multi-thread", "sync", "net", "io-util"] }
|
||||||
|
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = ["wire", "async"] }
|
||||||
|
|
||||||
|
# REPL
|
||||||
|
anyhow = "1.0"
|
||||||
|
rustyline = "17.0"
|
||||||
|
|
||||||
|
# CLI
|
||||||
|
clap = { version = "4", features = ["derive"] }
|
||||||
|
|
||||||
|
# Logging
|
||||||
|
tracing = "0.1"
|
||||||
|
tracing-subscriber = { version = "0.3", features = ["env-filter"] }
|
||||||
|
|
||||||
|
derive_more = { version = "2", features = ["full"] }
|
||||||
|
thiserror = "2"
|
||||||
|
miette = { version = "7.4", features = ["fancy"] }
|
||||||
|
|
||||||
|
hashbrown = "0.16"
|
||||||
|
string-interner = "0.19"
|
||||||
|
|
||||||
|
rust-embed="8.11"
|
||||||
|
|
||||||
|
itertools = "0.14"
|
||||||
|
|
||||||
|
regex = "1.11"
|
||||||
|
|
||||||
|
deno_core = "0.385"
|
||||||
|
deno_error = "0.7"
|
||||||
|
|
||||||
|
nix-nar = "0.3"
|
||||||
|
sha2 = "0.10"
|
||||||
|
sha1 = "0.10"
|
||||||
|
md5 = "0.8"
|
||||||
|
hex = "0.4"
|
||||||
|
|
||||||
|
base64 = "0.22"
|
||||||
|
|
||||||
|
reqwest = { version = "0.13", features = ["blocking", "rustls"], default-features = false }
|
||||||
|
tar = "0.4"
|
||||||
|
flate2 = "1.0"
|
||||||
|
xz2 = "0.1"
|
||||||
|
bzip2 = "0.6"
|
||||||
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
|
serde_json = "1.0"
|
||||||
|
# spec 1.0.0
|
||||||
|
toml = "0.9.9"
|
||||||
|
dirs = "6.0"
|
||||||
|
tempfile = "3.24"
|
||||||
|
rusqlite = { version = "0.38", features = ["bundled"] }
|
||||||
|
|
||||||
|
rnix = "0.13"
|
||||||
|
rowan = "0.16"
|
||||||
|
|
||||||
|
nix-js-macros = { path = "../nix-js-macros" }
|
||||||
|
ere = "0.2.4"
|
||||||
|
num_enum = "0.7.5"
|
||||||
|
tap = "1.0.1"
|
||||||
|
|
||||||
|
# Inspector (optional)
|
||||||
|
fastwebsockets = { version = "0.10", features = ["upgrade"], optional = true }
|
||||||
|
hyper = { version = "1", features = ["http1", "server"], optional = true }
|
||||||
|
hyper-util = { version = "0.1", features = ["tokio"], optional = true }
|
||||||
|
http-body-util = { version = "0.1", optional = true }
|
||||||
|
http = { version = "1", optional = true }
|
||||||
|
uuid = { version = "1", features = ["v4"], optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
inspector = ["dep:fastwebsockets", "dep:hyper", "dep:hyper-util", "dep:http-body-util", "dep:http", "dep:uuid"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "basic_ops"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "builtins"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "thunk_scope"
|
||||||
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "compile_time"
|
||||||
|
harness = false
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::hint::black_box;
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use std::hint::black_box;
|
||||||
use utils::eval;
|
use utils::eval;
|
||||||
|
|
||||||
fn bench_arithmetic(c: &mut Criterion) {
|
fn bench_arithmetic(c: &mut Criterion) {
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::hint::black_box;
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use std::hint::black_box;
|
||||||
use utils::eval;
|
use utils::eval;
|
||||||
|
|
||||||
fn bench_builtin_math(c: &mut Criterion) {
|
fn bench_builtin_math(c: &mut Criterion) {
|
||||||
@@ -0,0 +1,140 @@
|
|||||||
|
mod utils;
|
||||||
|
|
||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use nix_js::context::Context;
|
||||||
|
use std::hint::black_box;
|
||||||
|
use utils::compile;
|
||||||
|
|
||||||
|
fn bench_parse_and_downgrade(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("parse_and_downgrade");
|
||||||
|
|
||||||
|
group.bench_function("simple_expression", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box("1 + 1"));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("complex_function", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib",
|
||||||
|
));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("large_attrset", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"{ a = 1; b = 2; c = 3; d = 4; e = 5; f = 6; g = 7; h = 8; i = 9; j = 10; k = 11; l = 12; m = 13; n = 14; o = 15; }",
|
||||||
|
));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("nested_let_bindings", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let a = 1; b = 2; c = 3; in let d = a + b; e = b + c; in let f = d + e; in f",
|
||||||
|
));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_codegen(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("codegen");
|
||||||
|
|
||||||
|
group.bench_function("arithmetic_expression", |b| {
|
||||||
|
b.iter(|| compile(black_box("(1 + 2) * (3 - 4) / 5")))
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("function_with_closure", |b| {
|
||||||
|
b.iter(|| compile(black_box("let x = 10; f = y: x + y; in f 5")))
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("recursive_attrset", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"rec { a = 1; b = a + 1; c = b + 1; d = c + 1; e = d + 1; }",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_full_pipeline(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("full_pipeline");
|
||||||
|
|
||||||
|
group.bench_function("simple_eval", |b| b.iter(|| compile(black_box("1 + 1"))));
|
||||||
|
|
||||||
|
group.bench_function("fibonacci_10", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 10",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("map_operation", |b| {
|
||||||
|
b.iter(|| compile(black_box("map (x: x * 2) [1 2 3 4 5 6 7 8 9 10]")))
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("complex_attrset_access", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let attrs = { a.b.c = { d.e = 42; }; }; in attrs.a.b.c.d.e",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("with_expression", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let attrs = { x = 1; y = 2; z = 3; }; in with attrs; x + y + z",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_context_creation(c: &mut Criterion) {
|
||||||
|
c.bench_function("context_new", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
let _ = Context::new();
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bench_symbol_interning(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("symbol_interning");
|
||||||
|
|
||||||
|
group.bench_function("many_unique_symbols", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let a1 = 1; a2 = 2; a3 = 3; a4 = 4; a5 = 5; a6 = 6; a7 = 7; a8 = 8; a9 = 9; a10 = 10; in a1 + a2 + a3 + a4 + a5 + a6 + a7 + a8 + a9 + a10",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("repeated_symbols", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
compile(black_box(
|
||||||
|
"let x = 1; y = x; z = x; a = x; b = x; c = x; in x + y + z + a + b + c",
|
||||||
|
))
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(
|
||||||
|
benches,
|
||||||
|
bench_parse_and_downgrade,
|
||||||
|
bench_codegen,
|
||||||
|
bench_full_pipeline,
|
||||||
|
bench_context_creation,
|
||||||
|
bench_symbol_interning
|
||||||
|
);
|
||||||
|
criterion_main!(benches);
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use std::hint::black_box;
|
|
||||||
|
|
||||||
use criterion::{Criterion, criterion_group, criterion_main};
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
|
use std::hint::black_box;
|
||||||
use utils::eval;
|
use utils::eval;
|
||||||
|
|
||||||
fn bench_non_recursive(c: &mut Criterion) {
|
fn bench_non_recursive(c: &mut Criterion) {
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use nix_js::context::Context;
|
||||||
|
use nix_js::error::{Result, Source};
|
||||||
|
use nix_js::value::Value;
|
||||||
|
|
||||||
|
pub fn eval(expr: &str) -> Value {
|
||||||
|
Context::new()
|
||||||
|
.unwrap()
|
||||||
|
.eval(Source::new_eval(expr.into()).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_result(expr: &str) -> Result<Value> {
|
||||||
|
Context::new()
|
||||||
|
.unwrap()
|
||||||
|
.eval(Source::new_eval(expr.into()).unwrap())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn compile(expr: &str) -> String {
|
||||||
|
Context::new()
|
||||||
|
.unwrap()
|
||||||
|
.compile(Source::new_eval(expr.into()).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
@@ -0,0 +1,70 @@
|
|||||||
|
use std::path::Path;
|
||||||
|
use std::process::Command;
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let runtime_ts_dir = Path::new("runtime-ts");
|
||||||
|
let dist_runtime = runtime_ts_dir.join("dist/runtime.js");
|
||||||
|
|
||||||
|
if !runtime_ts_dir.exists() {
|
||||||
|
println!("cargo::warning=runtime-ts directory not found, using existing runtime.js");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("cargo::rerun-if-changed=runtime-ts/src");
|
||||||
|
println!("cargo::rerun-if-changed=runtime-ts/package.json");
|
||||||
|
println!("cargo::rerun-if-changed=runtime-ts/tsconfig.json");
|
||||||
|
println!("cargo::rerun-if-changed=runtime-ts/build.mjs");
|
||||||
|
|
||||||
|
if !runtime_ts_dir.join("node_modules").exists() {
|
||||||
|
println!("Installing npm dependencies...");
|
||||||
|
let npm_cmd = if cfg!(target_os = "windows") {
|
||||||
|
"npm.cmd"
|
||||||
|
} else {
|
||||||
|
"npm"
|
||||||
|
};
|
||||||
|
let status = Command::new(npm_cmd)
|
||||||
|
.arg("install")
|
||||||
|
.current_dir(runtime_ts_dir)
|
||||||
|
.status()
|
||||||
|
.expect("Failed to run npm install. Is Node.js installed?");
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
panic!("npm install failed. Please check your Node.js installation.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Running TypeScript type checking...");
|
||||||
|
let npm_cmd = if cfg!(target_os = "windows") {
|
||||||
|
"npm.cmd"
|
||||||
|
} else {
|
||||||
|
"npm"
|
||||||
|
};
|
||||||
|
let status = Command::new(npm_cmd)
|
||||||
|
.arg("run")
|
||||||
|
.arg("typecheck")
|
||||||
|
.current_dir(runtime_ts_dir)
|
||||||
|
.status()
|
||||||
|
.expect("Failed to run type checking");
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
panic!("TypeScript type checking failed! Fix type errors before building.");
|
||||||
|
}
|
||||||
|
|
||||||
|
println!("Building runtime.js from TypeScript...");
|
||||||
|
let status = Command::new(npm_cmd)
|
||||||
|
.arg("run")
|
||||||
|
.arg("build")
|
||||||
|
.current_dir(runtime_ts_dir)
|
||||||
|
.status()
|
||||||
|
.expect("Failed to build runtime");
|
||||||
|
|
||||||
|
if !status.success() {
|
||||||
|
panic!("Runtime build failed!");
|
||||||
|
}
|
||||||
|
|
||||||
|
if dist_runtime.exists() {
|
||||||
|
println!("Successfully built runtime.js",);
|
||||||
|
} else {
|
||||||
|
panic!("dist/runtime.js not found after build");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
/node_modules
|
||||||
|
/dist
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import * as esbuild from "esbuild";
|
||||||
|
|
||||||
|
await esbuild.build({
|
||||||
|
entryPoints: ["src/index.ts"],
|
||||||
|
outfile: "dist/runtime.js",
|
||||||
|
bundle: true,
|
||||||
|
// minify: true,
|
||||||
|
});
|
||||||
Generated
+509
@@ -0,0 +1,509 @@
|
|||||||
|
{
|
||||||
|
"name": "nix-js-runtime",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "nix-js-runtime",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"dependencies": {
|
||||||
|
"js-sdsl": "^4.4.2"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.2",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm/-/android-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/android-x64/-/android-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.24.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/esbuild/-/esbuild-0.24.2.tgz",
|
||||||
|
"integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.24.2",
|
||||||
|
"@esbuild/android-arm": "0.24.2",
|
||||||
|
"@esbuild/android-arm64": "0.24.2",
|
||||||
|
"@esbuild/android-x64": "0.24.2",
|
||||||
|
"@esbuild/darwin-arm64": "0.24.2",
|
||||||
|
"@esbuild/darwin-x64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/freebsd-x64": "0.24.2",
|
||||||
|
"@esbuild/linux-arm": "0.24.2",
|
||||||
|
"@esbuild/linux-arm64": "0.24.2",
|
||||||
|
"@esbuild/linux-ia32": "0.24.2",
|
||||||
|
"@esbuild/linux-loong64": "0.24.2",
|
||||||
|
"@esbuild/linux-mips64el": "0.24.2",
|
||||||
|
"@esbuild/linux-ppc64": "0.24.2",
|
||||||
|
"@esbuild/linux-riscv64": "0.24.2",
|
||||||
|
"@esbuild/linux-s390x": "0.24.2",
|
||||||
|
"@esbuild/linux-x64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/netbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-arm64": "0.24.2",
|
||||||
|
"@esbuild/openbsd-x64": "0.24.2",
|
||||||
|
"@esbuild/sunos-x64": "0.24.2",
|
||||||
|
"@esbuild/win32-arm64": "0.24.2",
|
||||||
|
"@esbuild/win32-ia32": "0.24.2",
|
||||||
|
"@esbuild/win32-x64": "0.24.2"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/js-sdsl": {
|
||||||
|
"version": "4.4.2",
|
||||||
|
"resolved": "https://registry.npmmirror.com/js-sdsl/-/js-sdsl-4.4.2.tgz",
|
||||||
|
"integrity": "sha512-dwXFwByc/ajSV6m5bcKAPwe4yDDF6D614pxmIi5odytzxRlwqF6nwoiCek80Ixc7Cvma5awClxrzFtxCQvcM8w==",
|
||||||
|
"license": "MIT",
|
||||||
|
"funding": {
|
||||||
|
"type": "opencollective",
|
||||||
|
"url": "https://opencollective.com/js-sdsl"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/typescript": {
|
||||||
|
"version": "5.9.3",
|
||||||
|
"resolved": "https://registry.npmmirror.com/typescript/-/typescript-5.9.3.tgz",
|
||||||
|
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
|
||||||
|
"dev": true,
|
||||||
|
"license": "Apache-2.0",
|
||||||
|
"bin": {
|
||||||
|
"tsc": "bin/tsc",
|
||||||
|
"tsserver": "bin/tsserver"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=14.17"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
{
|
||||||
|
"name": "nix-js-runtime",
|
||||||
|
"version": "0.1.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"typecheck": "tsc --noEmit",
|
||||||
|
"build": "node build.mjs",
|
||||||
|
"dev": "npm run typecheck && npm run build"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.24.2",
|
||||||
|
"typescript": "^5.7.2"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"js-sdsl": "^4.4.2"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
import { op } from "../operators";
|
||||||
|
import { coerceNumeric, forceInt, forceNumeric } from "../type-assert";
|
||||||
|
import type { NixBool, NixInt, NixNumber, NixValue } from "../types";
|
||||||
|
|
||||||
|
export const add =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): bigint | number => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
return (av as never) + (bv as never);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sub =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): bigint | number => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
return (av as never) - (bv as never);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mul =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): bigint | number => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
return (av as never) * (bv as never);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const div =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): NixNumber => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
|
||||||
|
if (bv === 0 || bv === 0n) {
|
||||||
|
throw new RangeError("Division by zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (av as never) / (bv as never);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bitAnd =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): NixInt => {
|
||||||
|
const av = forceInt(a);
|
||||||
|
const bv = forceInt(b);
|
||||||
|
return av & bv;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bitOr =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): NixInt => {
|
||||||
|
const av = forceInt(a);
|
||||||
|
const bv = forceInt(b);
|
||||||
|
return av | bv;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const bitXor =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): NixInt => {
|
||||||
|
const av = forceInt(a);
|
||||||
|
const bv = forceInt(b);
|
||||||
|
return av ^ bv;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const lessThan =
|
||||||
|
(a: NixValue) =>
|
||||||
|
(b: NixValue): NixBool =>
|
||||||
|
op.lt(a, b);
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
import { mkPos } from "../helpers";
|
||||||
|
import { createThunk } from "../thunk";
|
||||||
|
import { forceAttrs, forceFunction, forceList, forceStringValue } from "../type-assert";
|
||||||
|
import { ATTR_POSITIONS, type NixAttrs, type NixList, type NixValue } from "../types";
|
||||||
|
|
||||||
|
export const attrNames = (set: NixValue): string[] => Object.keys(forceAttrs(set)).sort();
|
||||||
|
|
||||||
|
export const attrValues = (set: NixValue): NixValue[] =>
|
||||||
|
Object.entries(forceAttrs(set))
|
||||||
|
.sort(([a], [b]) => {
|
||||||
|
if (a < b) {
|
||||||
|
return -1;
|
||||||
|
} else if (a === b) {
|
||||||
|
return 0;
|
||||||
|
} else {
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.map(([_, val]) => val);
|
||||||
|
|
||||||
|
export const getAttr =
|
||||||
|
(s: NixValue) =>
|
||||||
|
(set: NixValue): NixValue =>
|
||||||
|
forceAttrs(set)[forceStringValue(s)];
|
||||||
|
|
||||||
|
export const hasAttr =
|
||||||
|
(s: NixValue) =>
|
||||||
|
(set: NixValue): boolean =>
|
||||||
|
Object.hasOwn(forceAttrs(set), forceStringValue(s));
|
||||||
|
|
||||||
|
export const mapAttrs =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(attrs: NixValue): NixAttrs => {
|
||||||
|
const forcedAttrs = forceAttrs(attrs);
|
||||||
|
const forcedF = forceFunction(f);
|
||||||
|
const newAttrs: NixAttrs = {};
|
||||||
|
for (const key in forcedAttrs) {
|
||||||
|
newAttrs[key] = createThunk(() => forceFunction(forcedF(key))(forcedAttrs[key]), "created by mapAttrs");
|
||||||
|
}
|
||||||
|
return newAttrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const removeAttrs =
|
||||||
|
(attrs: NixValue) =>
|
||||||
|
(list: NixValue): NixAttrs => {
|
||||||
|
const newAttrs: NixAttrs = {};
|
||||||
|
const forcedAttrs = forceAttrs(attrs);
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
const keysToRemove = new Set(forcedList.map(forceStringValue));
|
||||||
|
|
||||||
|
for (const key in forcedAttrs) {
|
||||||
|
if (!keysToRemove.has(key)) {
|
||||||
|
newAttrs[key] = forcedAttrs[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return newAttrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const listToAttrs = (e: NixValue): NixAttrs => {
|
||||||
|
const attrs: NixAttrs = {};
|
||||||
|
const forcedE = [...forceList(e)].reverse();
|
||||||
|
for (const obj of forcedE) {
|
||||||
|
const item = forceAttrs(obj);
|
||||||
|
attrs[forceStringValue(item.name)] = item.value;
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const intersectAttrs =
|
||||||
|
(e1: NixValue) =>
|
||||||
|
(e2: NixValue): NixAttrs => {
|
||||||
|
const f1 = forceAttrs(e1);
|
||||||
|
const f2 = forceAttrs(e2);
|
||||||
|
const attrs: NixAttrs = {};
|
||||||
|
for (const key of Object.keys(f2)) {
|
||||||
|
if (Object.hasOwn(f1, key)) {
|
||||||
|
attrs[key] = f2[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const catAttrs =
|
||||||
|
(attr: NixValue) =>
|
||||||
|
(list: NixValue): NixList => {
|
||||||
|
const key = forceStringValue(attr);
|
||||||
|
return forceList(list)
|
||||||
|
.map((set) => forceAttrs(set)[key])
|
||||||
|
.filter((val) => val !== undefined);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const groupBy =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(list: NixValue): NixAttrs => {
|
||||||
|
const attrs: NixAttrs = {};
|
||||||
|
const forcedF = forceFunction(f);
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
for (const elem of forcedList) {
|
||||||
|
const key = forceStringValue(forcedF(elem));
|
||||||
|
if (!attrs[key]) attrs[key] = [];
|
||||||
|
(attrs[key] as NixList).push(elem);
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const zipAttrsWith =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(list: NixValue): NixValue => {
|
||||||
|
const listForced = forceList(list);
|
||||||
|
|
||||||
|
const attrMap = new Map<string, NixValue[]>();
|
||||||
|
|
||||||
|
for (const item of listForced) {
|
||||||
|
const attrs = forceAttrs(item);
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
if (!attrMap.has(key)) {
|
||||||
|
attrMap.set(key, []);
|
||||||
|
}
|
||||||
|
(attrMap.get(key) as NixValue[]).push(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, NixValue> = {};
|
||||||
|
|
||||||
|
for (const [name, values] of attrMap.entries()) {
|
||||||
|
result[name] = createThunk(() => forceFunction(forceFunction(f)(name))(values));
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const unsafeGetAttrPos =
|
||||||
|
(attrName: NixValue) =>
|
||||||
|
(attrSet: NixValue): NixValue => {
|
||||||
|
const name = forceStringValue(attrName);
|
||||||
|
const attrs = forceAttrs(attrSet);
|
||||||
|
|
||||||
|
if (!(name in attrs)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const positions = (attrs as NixAttrs & Record<symbol, unknown>)[ATTR_POSITIONS] as
|
||||||
|
| Record<string, string>
|
||||||
|
| undefined;
|
||||||
|
if (!positions || !(name in positions)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const span = positions[name];
|
||||||
|
return mkPos(span);
|
||||||
|
};
|
||||||
@@ -0,0 +1,202 @@
|
|||||||
|
import {
|
||||||
|
decodeContextElem,
|
||||||
|
getStringContext,
|
||||||
|
getStringValue,
|
||||||
|
mkStringWithContext,
|
||||||
|
type NixStringContext,
|
||||||
|
parseContextToInfoMap,
|
||||||
|
} from "../string-context";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import { forceAttrs, forceList, forceString, forceStringValue } from "../type-assert";
|
||||||
|
import type { NixAttrs, NixString, NixValue } from "../types";
|
||||||
|
import { isStringWithContext } from "../types";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.hasContext - Check if string has context
|
||||||
|
*
|
||||||
|
* Returns true if the string has any store path references.
|
||||||
|
*/
|
||||||
|
export const hasContext = (value: NixValue): boolean => {
|
||||||
|
const s = forceString(value);
|
||||||
|
return isStringWithContext(s) && s.context.size > 0;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.unsafeDiscardStringContext - Remove all context from string
|
||||||
|
*
|
||||||
|
* IMPORTANT: This discards string context, returning only the string value.
|
||||||
|
* Use with caution as it removes derivation dependencies.
|
||||||
|
*/
|
||||||
|
export const unsafeDiscardStringContext = (value: NixValue): string => {
|
||||||
|
const s = forceString(value);
|
||||||
|
return getStringValue(s);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.unsafeDiscardOutputDependency - Convert DrvDeep to Opaque context
|
||||||
|
*
|
||||||
|
* IMPORTANT: Transforms "all outputs" references (=) to plain path references.
|
||||||
|
* Preserves other context types unchanged.
|
||||||
|
*/
|
||||||
|
export const unsafeDiscardOutputDependency = (value: NixValue): NixString => {
|
||||||
|
const s = forceString(value);
|
||||||
|
const strValue = getStringValue(s);
|
||||||
|
const context = getStringContext(s);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return strValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContext: NixStringContext = new Set();
|
||||||
|
for (const encoded of context) {
|
||||||
|
const elem = decodeContextElem(encoded);
|
||||||
|
if (elem.type === "drvDeep") {
|
||||||
|
newContext.add(elem.drvPath);
|
||||||
|
} else {
|
||||||
|
newContext.add(encoded);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContext.size === 0) {
|
||||||
|
return strValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkStringWithContext(strValue, newContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.addDrvOutputDependencies - Convert Opaque to DrvDeep context
|
||||||
|
*
|
||||||
|
* IMPORTANT: Transforms plain derivation path references to "all outputs" references (=).
|
||||||
|
* The string must have exactly one context element which must be a .drv path.
|
||||||
|
*/
|
||||||
|
export const addDrvOutputDependencies = (value: NixValue): NixString => {
|
||||||
|
const s = forceString(value);
|
||||||
|
const strValue = getStringValue(s);
|
||||||
|
const context = getStringContext(s);
|
||||||
|
|
||||||
|
if (context.size !== 1) {
|
||||||
|
throw new Error(`context of string '${strValue}' must have exactly one element, but has ${context.size}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const [encoded] = context;
|
||||||
|
const elem = decodeContextElem(encoded);
|
||||||
|
|
||||||
|
if (elem.type === "drvDeep") {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (elem.type === "built") {
|
||||||
|
throw new Error(
|
||||||
|
`\`addDrvOutputDependencies\` can only act on derivations, not on a derivation output such as '${elem.output}'`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!elem.path.endsWith(".drv")) {
|
||||||
|
throw new Error(`path '${elem.path}' is not a derivation`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const newContext: NixStringContext = new Set([`=${elem.path}`]);
|
||||||
|
return mkStringWithContext(strValue, newContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.getContext - Extract context as structured attribute set
|
||||||
|
*
|
||||||
|
* Returns an attribute set mapping store paths to their context info:
|
||||||
|
* - path: true if it's a plain store path reference (opaque)
|
||||||
|
* - allOutputs: true if it references all derivation outputs (drvDeep, encoded as =path)
|
||||||
|
* - outputs: list of specific output names (built, encoded as !output!path)
|
||||||
|
*/
|
||||||
|
export const getContext = (value: NixValue): NixAttrs => {
|
||||||
|
const s = forceString(value);
|
||||||
|
const context = getStringContext(s);
|
||||||
|
|
||||||
|
const infoMap = parseContextToInfoMap(context);
|
||||||
|
const result: NixAttrs = {};
|
||||||
|
|
||||||
|
for (const [path, info] of infoMap) {
|
||||||
|
const attrs: NixAttrs = {};
|
||||||
|
if (info.path) {
|
||||||
|
attrs.path = true;
|
||||||
|
}
|
||||||
|
if (info.allOutputs) {
|
||||||
|
attrs.allOutputs = true;
|
||||||
|
}
|
||||||
|
if (info.outputs.length > 0) {
|
||||||
|
attrs.outputs = info.outputs;
|
||||||
|
}
|
||||||
|
result[path] = attrs;
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.appendContext - Add context to a string
|
||||||
|
*
|
||||||
|
* IMPORTANT: Merges the provided context attribute set with any existing context
|
||||||
|
* from the input string. Used to manually construct strings with specific
|
||||||
|
* derivation dependencies.
|
||||||
|
*
|
||||||
|
* Context format matches getContext output:
|
||||||
|
* - path: boolean - add as opaque reference
|
||||||
|
* - allOutputs: boolean - add as drvDeep reference (=)
|
||||||
|
* - outputs: [string] - add as built references (!output!)
|
||||||
|
*/
|
||||||
|
export const appendContext =
|
||||||
|
(strValue: NixValue) =>
|
||||||
|
(ctxValue: NixValue): NixString => {
|
||||||
|
const s = forceString(strValue);
|
||||||
|
const strVal = getStringValue(s);
|
||||||
|
const existingContext = getStringContext(s);
|
||||||
|
|
||||||
|
const ctxAttrs = forceAttrs(ctxValue);
|
||||||
|
const newContext: NixStringContext = new Set(existingContext);
|
||||||
|
|
||||||
|
for (const [path, infoVal] of Object.entries(ctxAttrs)) {
|
||||||
|
if (!path.startsWith("/nix/store/")) {
|
||||||
|
throw new Error(`context key '${path}' is not a store path`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const info = forceAttrs(infoVal);
|
||||||
|
|
||||||
|
if ("path" in info) {
|
||||||
|
const pathVal = force(info.path);
|
||||||
|
if (pathVal === true) {
|
||||||
|
newContext.add(path);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("allOutputs" in info) {
|
||||||
|
const allOutputs = force(info.allOutputs);
|
||||||
|
if (allOutputs === true) {
|
||||||
|
if (!path.endsWith(".drv")) {
|
||||||
|
throw new Error(
|
||||||
|
`tried to add all-outputs context of ${path}, which is not a derivation, to a string`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
newContext.add(`=${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("outputs" in info) {
|
||||||
|
const outputs = forceList(info.outputs);
|
||||||
|
if (outputs.length > 0 && !path.endsWith(".drv")) {
|
||||||
|
throw new Error(
|
||||||
|
`tried to add derivation output context of ${path}, which is not a derivation, to a string`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
for (const output of outputs) {
|
||||||
|
const outputName = forceStringValue(output);
|
||||||
|
newContext.add(`!${outputName}!${path}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (newContext.size === 0) {
|
||||||
|
return strVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkStringWithContext(strVal, newContext);
|
||||||
|
};
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
/**
|
||||||
|
* Conversion and serialization builtin functions
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { addBuiltContext, mkStringWithContext, type NixStringContext } from "../string-context";
|
||||||
|
import { force, isThunk } from "../thunk";
|
||||||
|
import { forceFunction, forceStringNoCtx } from "../type-assert";
|
||||||
|
import type { NixString, NixValue } from "../types";
|
||||||
|
import { HAS_CONTEXT, IS_PATH, isNixPath, isStringWithContext } from "../types";
|
||||||
|
import { isAttrs, isPath, typeOf } from "./type-check";
|
||||||
|
|
||||||
|
export const fromJSON = (e: NixValue): NixValue => {
|
||||||
|
const str = force(e);
|
||||||
|
if (typeof str !== "string" && !isStringWithContext(str)) {
|
||||||
|
throw new TypeError(`builtins.fromJSON: expected a string, got ${typeOf(str)}`);
|
||||||
|
}
|
||||||
|
const jsonStr = isStringWithContext(str) ? str.value : str;
|
||||||
|
return Deno.core.ops.op_from_json(jsonStr) as NixValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fromTOML = (e: NixValue): NixValue => {
|
||||||
|
const toml = forceStringNoCtx(e);
|
||||||
|
return Deno.core.ops.op_from_toml(toml) as NixValue;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toJSON = (e: NixValue): NixString => {
|
||||||
|
const context: Set<string> = new Set();
|
||||||
|
const string = JSON.stringify(nixValueToJson(e, true, context, true));
|
||||||
|
if (context.size === 0) {
|
||||||
|
return string;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(string, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toXML = (_e: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: toXML");
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* String coercion modes control which types can be coerced to strings
|
||||||
|
*
|
||||||
|
* - Base: Only strings are allowed (no coercion)
|
||||||
|
* - Interpolation: Used in string interpolation "${expr}" - allows strings and integers
|
||||||
|
* - ToString: Used in builtins.toString - allows all types (bools, floats, null, lists, etc.)
|
||||||
|
*/
|
||||||
|
export enum StringCoercionMode {
|
||||||
|
Base = 0,
|
||||||
|
Interpolation = 1,
|
||||||
|
ToString = 2,
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CoerceResult {
|
||||||
|
value: string;
|
||||||
|
context: NixStringContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a Nix value to a string according to the specified mode.
|
||||||
|
* This implements the same behavior as Lix's EvalState::coerceToString.
|
||||||
|
*
|
||||||
|
* IMPORTANT: String context preservation rules:
|
||||||
|
* - StringWithContext: Context is collected in outContext parameter
|
||||||
|
* - Derivations (with outPath): Built context is added for the drvPath/outputName
|
||||||
|
* - Lists (ToString mode): Context from all elements is merged
|
||||||
|
* - All other coercions: No context added
|
||||||
|
*
|
||||||
|
* @param value - The value to coerce
|
||||||
|
* @param mode - The coercion mode (controls which types are allowed)
|
||||||
|
* @param copyToStore - If true, paths should be copied to the Nix store (not implemented yet)
|
||||||
|
* @param outContext - Optional context set to collect string contexts
|
||||||
|
* @returns The string representation of the value
|
||||||
|
* @throws TypeError if the value cannot be coerced in the given mode
|
||||||
|
*
|
||||||
|
* Coercion rules by type:
|
||||||
|
* - String: Always returns as-is
|
||||||
|
* - Path: Returns the path string (copyToStore not implemented yet)
|
||||||
|
* - Integer: Only in Interpolation or ToString mode
|
||||||
|
* - Float: Only in ToString mode
|
||||||
|
* - Boolean: Only in ToString mode (true → "1", false → "")
|
||||||
|
* - Null: Only in ToString mode (→ "")
|
||||||
|
* - List: Only in ToString mode (recursively coerce elements, join with spaces)
|
||||||
|
* - Attrs: Check for __toString method or outPath attribute
|
||||||
|
* - Function: Never coercible (throws error)
|
||||||
|
*/
|
||||||
|
export const coerceToString = (
|
||||||
|
value: NixValue,
|
||||||
|
mode: StringCoercionMode,
|
||||||
|
copyToStore: boolean = false,
|
||||||
|
outContext: NixStringContext,
|
||||||
|
): string => {
|
||||||
|
const v = force(value);
|
||||||
|
|
||||||
|
// Strings are always returned as-is, regardless of mode
|
||||||
|
if (typeof v === "string") {
|
||||||
|
return v;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStringWithContext(v)) {
|
||||||
|
for (const elem of v.context) {
|
||||||
|
outContext.add(elem);
|
||||||
|
}
|
||||||
|
return v.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Paths coerce to their string value
|
||||||
|
if (isNixPath(v)) {
|
||||||
|
if (copyToStore) {
|
||||||
|
const pathStr = v.value;
|
||||||
|
const storePath = Deno.core.ops.op_copy_path_to_store(pathStr);
|
||||||
|
outContext.add(storePath);
|
||||||
|
return storePath;
|
||||||
|
}
|
||||||
|
return v.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof v === "object" && v !== null && !Array.isArray(v)) {
|
||||||
|
// First, try the __toString method if present
|
||||||
|
// This allows custom types to define their own string representation
|
||||||
|
if ("__toString" in v) {
|
||||||
|
// Force the method in case it's a thunk
|
||||||
|
const toStringMethod = forceFunction(v.__toString);
|
||||||
|
const result = force(toStringMethod(v));
|
||||||
|
// Recursively coerceToString
|
||||||
|
return coerceToString(result, mode, copyToStore, outContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no __toString, try outPath (used for derivations and store paths)
|
||||||
|
// This allows derivation objects like { outPath = "/nix/store/..."; } to be coerced
|
||||||
|
if ("outPath" in v) {
|
||||||
|
// Recursively coerce the outPath value
|
||||||
|
const outPath = coerceToString(v.outPath, mode, copyToStore, outContext);
|
||||||
|
if ("type" in v && v.type === "derivation" && "drvPath" in v && outContext) {
|
||||||
|
const drvPathValue = force(v.drvPath);
|
||||||
|
const drvPathStr = isStringWithContext(drvPathValue)
|
||||||
|
? drvPathValue.value
|
||||||
|
: typeof drvPathValue === "string"
|
||||||
|
? drvPathValue
|
||||||
|
: null;
|
||||||
|
if (drvPathStr) {
|
||||||
|
const outputName = "outputName" in v ? String(force(v.outputName)) : "out";
|
||||||
|
addBuiltContext(outContext, drvPathStr, outputName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return outPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attribute sets without __toString or outPath cannot be coerced
|
||||||
|
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Integer coercion is allowed in Interpolation and ToString modes
|
||||||
|
// This enables string interpolation like "value: ${42}"
|
||||||
|
if (mode >= StringCoercionMode.Interpolation) {
|
||||||
|
if (typeof v === "bigint") {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The following types are only coercible in ToString mode (builtins.toString)
|
||||||
|
if (mode >= StringCoercionMode.ToString) {
|
||||||
|
// Booleans: true → "1", false → ""
|
||||||
|
// This is for shell scripting convenience (same as null)
|
||||||
|
if (typeof v === "boolean") {
|
||||||
|
return v ? "1" : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Floats are converted using JavaScript's default toString
|
||||||
|
if (typeof v === "number") {
|
||||||
|
return v.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Null becomes empty string (for shell scripting convenience)
|
||||||
|
if (v === null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lists are recursively converted and joined with spaces
|
||||||
|
// We cannot use Array.join() directly because of special spacing rules:
|
||||||
|
// - Elements are recursively coerced to strings
|
||||||
|
// - Spaces are added between elements, BUT:
|
||||||
|
// * No space is added after an element if it's an empty list
|
||||||
|
// * The last element never gets a trailing space
|
||||||
|
//
|
||||||
|
// Examples:
|
||||||
|
// - [ 1 2 3 ] → "1 2 3"
|
||||||
|
// - [ 1 [ ] 2 ] → "1 2" (empty list doesn't add space)
|
||||||
|
// - [ 1 [ [ ] ] 2 ] → "1 2" (nested empty list is not itself empty, so adds space)
|
||||||
|
// - [ [ 1 2 ] [ 3 4 ] ] → "1 2 3 4" (nested lists flatten)
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
let result = "";
|
||||||
|
for (let i = 0; i < v.length; i++) {
|
||||||
|
const item = v[i];
|
||||||
|
// Recursively convert element to string
|
||||||
|
const str = coerceToString(item, mode, copyToStore, outContext);
|
||||||
|
result += str;
|
||||||
|
|
||||||
|
// Add space after this element if:
|
||||||
|
// 1. It's not the last element, AND
|
||||||
|
// 2. The element is not an empty list
|
||||||
|
//
|
||||||
|
// Note: We check if the ELEMENT is an empty list, not if its
|
||||||
|
// string representation is empty.
|
||||||
|
// For example, [[]] is not an empty list (length 1), so it gets
|
||||||
|
// a trailing space even though its toString is "".
|
||||||
|
if (i < v.length - 1) {
|
||||||
|
const forcedItem = force(item);
|
||||||
|
if (!Array.isArray(forcedItem) || forcedItem.length !== 0) {
|
||||||
|
result += " ";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new TypeError(`cannot coerce ${typeOf(v)} to a string`);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a Nix value to a string with context tracking
|
||||||
|
*/
|
||||||
|
export const coerceToStringWithContext = (
|
||||||
|
value: NixValue,
|
||||||
|
mode: StringCoercionMode = StringCoercionMode.ToString,
|
||||||
|
copyToStore: boolean = false,
|
||||||
|
): NixString => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const str = coerceToString(value, mode, copyToStore, context);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return str;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(str, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Coerce a Nix value to an absolute path string.
|
||||||
|
* This implements the same behavior as Lix's EvalState::coerceToPath.
|
||||||
|
*
|
||||||
|
* @param value - The value to coerce
|
||||||
|
* @param outContext - Optional context set to collect string contexts
|
||||||
|
* @returns The absolute path string
|
||||||
|
* @throws TypeError if the value cannot be coerced to a string
|
||||||
|
* @throws Error if the result is not an absolute path
|
||||||
|
*
|
||||||
|
* Semantics:
|
||||||
|
* - Coerces to string using Strict mode (same as coerceToString with Base mode)
|
||||||
|
* - Validates the result is non-empty and starts with '/'
|
||||||
|
* - Returns the path string (not a NixPath object)
|
||||||
|
* - Preserves string context if present
|
||||||
|
*/
|
||||||
|
export const coerceToPath = (value: NixValue, outContext: NixStringContext): string => {
|
||||||
|
const forced = force(value);
|
||||||
|
|
||||||
|
if (isPath(forced)) {
|
||||||
|
return forced.value;
|
||||||
|
}
|
||||||
|
if (isAttrs(forced) && Object.hasOwn(forced, "__toString")) {
|
||||||
|
const toStringFunc = forceFunction(forced.__toString);
|
||||||
|
return coerceToPath(toStringFunc(forced), outContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathStr = coerceToString(value, StringCoercionMode.Base, false, outContext);
|
||||||
|
|
||||||
|
if (pathStr === "") {
|
||||||
|
throw new Error("string doesn't represent an absolute path: empty string");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pathStr[0] !== "/") {
|
||||||
|
throw new Error(`string '${pathStr}' doesn't represent an absolute path`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.toString - Convert a value to a string
|
||||||
|
*
|
||||||
|
* This is the public builtin function exposed to Nix code.
|
||||||
|
* It uses ToString mode, which allows coercing all types except functions.
|
||||||
|
*
|
||||||
|
* @param value - The value to convert to a string
|
||||||
|
* @returns The string representation
|
||||||
|
*/
|
||||||
|
export const toStringFunc = (value: NixValue): NixString => {
|
||||||
|
return coerceToStringWithContext(value, StringCoercionMode.ToString, false);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const nixValueToJson = (
|
||||||
|
value: NixValue,
|
||||||
|
strict: boolean,
|
||||||
|
outContext: NixStringContext,
|
||||||
|
copyToStore: boolean,
|
||||||
|
seen: Set<NixValue> = new Set(),
|
||||||
|
): unknown => {
|
||||||
|
const v = strict ? force(value) : value;
|
||||||
|
|
||||||
|
if (isThunk(v) || typeof v === "function")
|
||||||
|
throw new Error(`cannot convert ${isThunk(v) ? "thunk" : "lambda"} to JSON`);
|
||||||
|
if (v === null) return null;
|
||||||
|
if (typeof v === "bigint") {
|
||||||
|
const num = Number(v);
|
||||||
|
if (v > Number.MAX_SAFE_INTEGER || v < Number.MIN_SAFE_INTEGER) {
|
||||||
|
console.warn(`integer ${v} exceeds safe range, precision may be lost`);
|
||||||
|
}
|
||||||
|
return num;
|
||||||
|
}
|
||||||
|
if (typeof v === "number") return v;
|
||||||
|
if (typeof v === "boolean") return v;
|
||||||
|
if (typeof v === "string") return v;
|
||||||
|
if (typeof v === "object" && HAS_CONTEXT in v) {
|
||||||
|
for (const elem of v.context) {
|
||||||
|
outContext.add(elem);
|
||||||
|
}
|
||||||
|
return v.value;
|
||||||
|
}
|
||||||
|
if (typeof v === "object" && IS_PATH in v) {
|
||||||
|
if (copyToStore) {
|
||||||
|
const storePath = Deno.core.ops.op_copy_path_to_store(v.value);
|
||||||
|
outContext.add(storePath);
|
||||||
|
return storePath;
|
||||||
|
} else {
|
||||||
|
return v.value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: is this check necessary?
|
||||||
|
// if (seen.has(v)) {
|
||||||
|
// throw new Error("cycle detected in toJSON");
|
||||||
|
// } else {
|
||||||
|
// seen.add(v)
|
||||||
|
// }
|
||||||
|
|
||||||
|
if (Array.isArray(v)) {
|
||||||
|
return v.map((item) => nixValueToJson(item, strict, outContext, copyToStore, seen));
|
||||||
|
}
|
||||||
|
|
||||||
|
// NixAttrs
|
||||||
|
if ("__toString" in v && typeof force(v.__toString) === "function") {
|
||||||
|
const toStringMethod = force(v.__toString) as (self: typeof v) => NixValue;
|
||||||
|
const result = force(toStringMethod(v));
|
||||||
|
if (typeof result === "string") {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
if (isStringWithContext(result)) {
|
||||||
|
for (const elem of result.context) {
|
||||||
|
outContext.add(elem);
|
||||||
|
}
|
||||||
|
return result.value;
|
||||||
|
}
|
||||||
|
return nixValueToJson(result, strict, outContext, copyToStore, seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("outPath" in v) {
|
||||||
|
return nixValueToJson(v.outPath, strict, outContext, copyToStore, seen);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, unknown> = {};
|
||||||
|
const keys = Object.keys(v).sort();
|
||||||
|
for (const key of keys) {
|
||||||
|
result[key] = nixValueToJson(v[key], strict, outContext, copyToStore, seen);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,396 @@
|
|||||||
|
import {
|
||||||
|
addBuiltContext,
|
||||||
|
addDrvDeepContext,
|
||||||
|
mkStringWithContext,
|
||||||
|
type NixStringContext,
|
||||||
|
} from "../string-context";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import { forceAttrs, forceList, forceStringNoCtx, forceStringValue } from "../type-assert";
|
||||||
|
import type { NixAttrs, NixValue } from "../types";
|
||||||
|
import { coerceToString, nixValueToJson, StringCoercionMode } from "./conversion";
|
||||||
|
|
||||||
|
export interface OutputInfo {
|
||||||
|
path: string;
|
||||||
|
hashAlgo: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface DerivationData {
|
||||||
|
name: string;
|
||||||
|
outputs: Map<string, OutputInfo>;
|
||||||
|
inputDrvs: Map<string, Set<string>>;
|
||||||
|
inputSrcs: Set<string>;
|
||||||
|
platform: string;
|
||||||
|
builder: string;
|
||||||
|
args: string[];
|
||||||
|
env: Map<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const escapeString = (s: string): string => {
|
||||||
|
let result = "";
|
||||||
|
for (const char of s) {
|
||||||
|
switch (char) {
|
||||||
|
case '"':
|
||||||
|
result += '\\"';
|
||||||
|
break;
|
||||||
|
case "\\":
|
||||||
|
result += "\\\\";
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
result += "\\n";
|
||||||
|
break;
|
||||||
|
case "\r":
|
||||||
|
result += "\\r";
|
||||||
|
break;
|
||||||
|
case "\t":
|
||||||
|
result += "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result += char;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `"${result}"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const quoteString = (s: string): string => `"${s}"`;
|
||||||
|
|
||||||
|
const cmpByKey = <T>(a: [string, T], b: [string, T]): number => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0);
|
||||||
|
|
||||||
|
export const generateAterm = (drv: DerivationData): string => {
|
||||||
|
const outputEntries: string[] = [];
|
||||||
|
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
|
||||||
|
for (const [name, info] of sortedOutputs) {
|
||||||
|
outputEntries.push(
|
||||||
|
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputs = outputEntries.join(",");
|
||||||
|
|
||||||
|
const inputDrvEntries: string[] = [];
|
||||||
|
const sortedInputDrvs = Array.from(drv.inputDrvs.entries()).sort(cmpByKey);
|
||||||
|
for (const [drvPath, outputs] of sortedInputDrvs) {
|
||||||
|
const sortedOuts = Array.from(outputs).sort();
|
||||||
|
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
|
||||||
|
inputDrvEntries.push(`(${quoteString(drvPath)},${outList})`);
|
||||||
|
}
|
||||||
|
const inputDrvs = inputDrvEntries.join(",");
|
||||||
|
|
||||||
|
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
|
||||||
|
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
|
||||||
|
|
||||||
|
const args = drv.args.map(escapeString).join(",");
|
||||||
|
const envs = Array.from(drv.env.entries())
|
||||||
|
.sort(cmpByKey)
|
||||||
|
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
|
||||||
|
|
||||||
|
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const generateAtermModulo = (drv: DerivationData, inputDrvHashes: Map<string, string>): string => {
|
||||||
|
const outputEntries: string[] = [];
|
||||||
|
const sortedOutputs = Array.from(drv.outputs.entries()).sort(cmpByKey);
|
||||||
|
for (const [name, info] of sortedOutputs) {
|
||||||
|
outputEntries.push(
|
||||||
|
`(${quoteString(name)},${quoteString(info.path)},${quoteString(info.hashAlgo)},${quoteString(info.hash)})`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
const outputs = outputEntries.join(",");
|
||||||
|
|
||||||
|
const inputDrvEntries: string[] = [];
|
||||||
|
const sortedInputDrvHashes = Array.from(inputDrvHashes.entries()).sort(cmpByKey);
|
||||||
|
for (const [drvHash, outputs] of sortedInputDrvHashes) {
|
||||||
|
const sortedOuts = outputs.split(",").sort();
|
||||||
|
const outList = `[${sortedOuts.map(quoteString).join(",")}]`;
|
||||||
|
inputDrvEntries.push(`(${quoteString(drvHash)},${outList})`);
|
||||||
|
}
|
||||||
|
const inputDrvs = inputDrvEntries.join(",");
|
||||||
|
|
||||||
|
const sortedInputSrcs = Array.from(drv.inputSrcs).sort();
|
||||||
|
const inputSrcs = sortedInputSrcs.map(quoteString).join(",");
|
||||||
|
|
||||||
|
const args = drv.args.map(escapeString).join(",");
|
||||||
|
const envs = Array.from(drv.env.entries())
|
||||||
|
.sort(cmpByKey)
|
||||||
|
.map(([k, v]) => `(${escapeString(k)},${escapeString(v)})`);
|
||||||
|
|
||||||
|
return `Derive([${outputs}],[${inputDrvs}],[${inputSrcs}],${quoteString(drv.platform)},${escapeString(drv.builder)},[${args}],[${envs}])`;
|
||||||
|
};
|
||||||
|
const validateName = (attrs: NixAttrs): string => {
|
||||||
|
if (!("name" in attrs)) {
|
||||||
|
throw new Error("derivation: missing required attribute 'name'");
|
||||||
|
}
|
||||||
|
const name = forceStringValue(attrs.name);
|
||||||
|
if (!name) {
|
||||||
|
throw new Error("derivation: 'name' cannot be empty");
|
||||||
|
}
|
||||||
|
if (name.endsWith(".drv")) {
|
||||||
|
throw new Error(`derivation: invalid name '${name}' (cannot end with .drv)`);
|
||||||
|
}
|
||||||
|
return name;
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateBuilder = (attrs: NixAttrs, outContext: NixStringContext): string => {
|
||||||
|
if (!("builder" in attrs)) {
|
||||||
|
throw new Error("derivation: missing required attribute 'builder'");
|
||||||
|
}
|
||||||
|
return coerceToString(attrs.builder, StringCoercionMode.ToString, true, outContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateSystem = (attrs: NixAttrs): string => {
|
||||||
|
if (!("system" in attrs)) {
|
||||||
|
throw new Error("derivation: missing required attribute 'system'");
|
||||||
|
}
|
||||||
|
return forceStringValue(attrs.system);
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateOutputs = (outputs: string[]): void => {
|
||||||
|
if (outputs.length === 0) {
|
||||||
|
throw new Error("derivation: outputs list cannot be empty");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outputs.includes("drv")) {
|
||||||
|
throw new Error("derivation: invalid output name 'drv'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const seen = new Set<string>();
|
||||||
|
for (const output of outputs) {
|
||||||
|
if (seen.has(output)) {
|
||||||
|
throw new Error(`derivation: duplicate output '${output}'`);
|
||||||
|
}
|
||||||
|
seen.add(output);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractOutputs = (attrs: NixAttrs, structuredAttrs: boolean): string[] => {
|
||||||
|
if (!("outputs" in attrs)) {
|
||||||
|
return ["out"];
|
||||||
|
}
|
||||||
|
|
||||||
|
let outputs: string[];
|
||||||
|
if (structuredAttrs) {
|
||||||
|
const outputsList = forceList(attrs.outputs);
|
||||||
|
outputs = outputsList.map((o) => forceStringValue(o));
|
||||||
|
} else {
|
||||||
|
const outputsStr = coerceToString(attrs.outputs, StringCoercionMode.ToString, false, new Set());
|
||||||
|
outputs = outputsStr.split(/\s+/).filter((s) => s.length > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
validateOutputs(outputs);
|
||||||
|
return outputs;
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractArgs = (attrs: NixAttrs, outContext: NixStringContext): string[] => {
|
||||||
|
if (!("args" in attrs)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const argsList = forceList(attrs.args);
|
||||||
|
return argsList.map((a) => coerceToString(a, StringCoercionMode.ToString, true, outContext));
|
||||||
|
};
|
||||||
|
|
||||||
|
const structuredAttrsExcludedKeys = new Set([
|
||||||
|
"__structuredAttrs",
|
||||||
|
"__ignoreNulls",
|
||||||
|
"__contentAddressed",
|
||||||
|
"__impure",
|
||||||
|
"args",
|
||||||
|
]);
|
||||||
|
|
||||||
|
const specialAttrs = new Set(["args", "__ignoreNulls", "__contentAddressed", "__impure"]);
|
||||||
|
|
||||||
|
const sortedJsonStringify = (obj: Record<string, unknown>): string => {
|
||||||
|
const sortedKeys = Object.keys(obj).sort();
|
||||||
|
const sortedObj: Record<string, unknown> = {};
|
||||||
|
for (const key of sortedKeys) {
|
||||||
|
sortedObj[key] = obj[key];
|
||||||
|
}
|
||||||
|
return JSON.stringify(sortedObj);
|
||||||
|
};
|
||||||
|
|
||||||
|
const extractEnv = (
|
||||||
|
attrs: NixAttrs,
|
||||||
|
structuredAttrs: boolean,
|
||||||
|
ignoreNulls: boolean,
|
||||||
|
outContext: NixStringContext,
|
||||||
|
drvName: string,
|
||||||
|
): Map<string, string> => {
|
||||||
|
const env = new Map<string, string>();
|
||||||
|
|
||||||
|
if (structuredAttrs) {
|
||||||
|
const jsonAttrs: Record<string, unknown> = {};
|
||||||
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
if (!structuredAttrsExcludedKeys.has(key)) {
|
||||||
|
const forcedValue = force(value);
|
||||||
|
if (ignoreNulls && forcedValue === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
jsonAttrs[key] = nixValueToJson(value, true, outContext, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (key === "allowedReferences") {
|
||||||
|
console.warn(
|
||||||
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
|
`the derivation attribute 'allowedReferences'; use ` +
|
||||||
|
`'outputChecks.<output>.allowedReferences' instead`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === "allowedRequisites") {
|
||||||
|
console.warn(
|
||||||
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
|
`the derivation attribute 'allowedRequisites'; use ` +
|
||||||
|
`'outputChecks.<output>.allowedRequisites' instead`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === "disallowedReferences") {
|
||||||
|
console.warn(
|
||||||
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
|
`the derivation attribute 'disallowedReferences'; use ` +
|
||||||
|
`'outputChecks.<output>.disallowedReferences' instead`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === "disallowedRequisites") {
|
||||||
|
console.warn(
|
||||||
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
|
`the derivation attribute 'disallowedRequisites'; use ` +
|
||||||
|
`'outputChecks.<output>.disallowedRequisites' instead`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === "maxSize") {
|
||||||
|
console.warn(
|
||||||
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
|
`the derivation attribute 'maxSize'; use ` +
|
||||||
|
`'outputChecks.<output>.maxSize' instead`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (key === "maxClosureSize") {
|
||||||
|
console.warn(
|
||||||
|
`In a derivation named '${drvName}', 'structuredAttrs' disables the effect of ` +
|
||||||
|
`the derivation attribute 'maxClosureSize'; use ` +
|
||||||
|
`'outputChecks.<output>.maxClosureSize' instead`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
env.set("__json", sortedJsonStringify(jsonAttrs));
|
||||||
|
} else {
|
||||||
|
for (const [key, value] of Object.entries(attrs)) {
|
||||||
|
if (!specialAttrs.has(key)) {
|
||||||
|
const forcedValue = force(value);
|
||||||
|
if (ignoreNulls && forcedValue === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
env.set(key, coerceToString(value, StringCoercionMode.ToString, true, outContext));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return env;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface FixedOutputInfo {
|
||||||
|
hash: string;
|
||||||
|
hashAlgo: string;
|
||||||
|
hashMode: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const extractFixedOutputInfo = (attrs: NixAttrs, ignoreNulls: boolean): FixedOutputInfo | null => {
|
||||||
|
if (!("outputHash" in attrs)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hashValue = force(attrs.outputHash);
|
||||||
|
if (ignoreNulls && hashValue === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const hashRaw = forceStringNoCtx(attrs.outputHash);
|
||||||
|
|
||||||
|
let hashAlgo = null;
|
||||||
|
if ("outputHashAlgo" in attrs) {
|
||||||
|
const algoValue = force(attrs.outputHashAlgo);
|
||||||
|
if (!(ignoreNulls && algoValue === null)) {
|
||||||
|
hashAlgo = forceStringNoCtx(attrs.outputHashAlgo);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let hashMode = "flat";
|
||||||
|
if ("outputHashMode" in attrs) {
|
||||||
|
const modeValue = force(attrs.outputHashMode);
|
||||||
|
if (!(ignoreNulls && modeValue === null)) {
|
||||||
|
hashMode = forceStringValue(attrs.outputHashMode);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hashMode !== "flat" && hashMode !== "recursive") {
|
||||||
|
throw new Error(`derivation: invalid outputHashMode '${hashMode}' (must be 'flat' or 'recursive')`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parsed = Deno.core.ops.op_parse_hash(hashRaw, hashAlgo);
|
||||||
|
|
||||||
|
return { hash: parsed.hex, hashAlgo: parsed.algo, hashMode };
|
||||||
|
};
|
||||||
|
|
||||||
|
const validateFixedOutputConstraints = (fixedOutput: FixedOutputInfo | null, outputs: string[]) => {
|
||||||
|
if (fixedOutput && (outputs.length !== 1 || outputs[0] !== "out")) {
|
||||||
|
throw new Error("derivation: fixed-output derivations must have exactly one 'out' output");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const derivationStrict = (args: NixValue): NixAttrs => {
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
|
||||||
|
const drvName = validateName(attrs);
|
||||||
|
const collectedContext: NixStringContext = new Set();
|
||||||
|
const builder = validateBuilder(attrs, collectedContext);
|
||||||
|
const platform = validateSystem(attrs);
|
||||||
|
|
||||||
|
const structuredAttrs = "__structuredAttrs" in attrs ? force(attrs.__structuredAttrs) === true : false;
|
||||||
|
const ignoreNulls = "__ignoreNulls" in attrs ? force(attrs.__ignoreNulls) === true : false;
|
||||||
|
|
||||||
|
const outputs = extractOutputs(attrs, structuredAttrs);
|
||||||
|
const fixedOutputInfo = extractFixedOutputInfo(attrs, ignoreNulls);
|
||||||
|
validateFixedOutputConstraints(fixedOutputInfo, outputs);
|
||||||
|
|
||||||
|
if ("__contentAddressed" in attrs && force(attrs.__contentAddressed) === true) {
|
||||||
|
throw new Error("ca derivations are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("__impure" in attrs && force(attrs.__impure) === true) {
|
||||||
|
throw new Error("impure derivations are not supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
const drvArgs = extractArgs(attrs, collectedContext);
|
||||||
|
const env = extractEnv(attrs, structuredAttrs, ignoreNulls, collectedContext, drvName);
|
||||||
|
|
||||||
|
const envEntries: [string, string][] = Array.from(env.entries());
|
||||||
|
const contextArray: string[] = Array.from(collectedContext);
|
||||||
|
|
||||||
|
const rustResult: {
|
||||||
|
drvPath: string;
|
||||||
|
outputs: [string, string][];
|
||||||
|
} = Deno.core.ops.op_finalize_derivation({
|
||||||
|
name: drvName,
|
||||||
|
builder,
|
||||||
|
platform,
|
||||||
|
outputs,
|
||||||
|
args: drvArgs,
|
||||||
|
env: envEntries,
|
||||||
|
context: contextArray,
|
||||||
|
fixedOutput: fixedOutputInfo,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result: NixAttrs = {};
|
||||||
|
|
||||||
|
const drvPathContext = new Set<string>();
|
||||||
|
addDrvDeepContext(drvPathContext, rustResult.drvPath);
|
||||||
|
result.drvPath = mkStringWithContext(rustResult.drvPath, drvPathContext);
|
||||||
|
|
||||||
|
for (const [outputName, outputPath] of rustResult.outputs) {
|
||||||
|
const outputContext = new Set<string>();
|
||||||
|
addBuiltContext(outputContext, rustResult.drvPath, outputName);
|
||||||
|
result[outputName] = mkStringWithContext(outputPath, outputContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const derivationStub = (_: NixValue): NixAttrs => {
|
||||||
|
throw new Error("unreachable: stub derivation implementation called");
|
||||||
|
};
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
import type { NixValue } from "../types";
|
||||||
|
|
||||||
|
export const getFlake = (_attrs: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: getFlake");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseFlakeName = (_s: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: parseFlakeName");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseFlakeRef = (_s: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: parseFlakeRef");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const flakeRefToString = (_attrs: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: flakeRefToString");
|
||||||
|
};
|
||||||
@@ -0,0 +1,61 @@
|
|||||||
|
import { printValue } from "../print";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import { CatchableError, type NixValue } from "../types";
|
||||||
|
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
import { isAttrs } from "./type-check";
|
||||||
|
|
||||||
|
export const seq =
|
||||||
|
(e1: NixValue) =>
|
||||||
|
(e2: NixValue): NixValue => {
|
||||||
|
force(e1); // Force evaluation of e1
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deepSeq =
|
||||||
|
(e1: NixValue) =>
|
||||||
|
(e2: NixValue): NixValue => {
|
||||||
|
const seen: Set<NixValue> = new Set();
|
||||||
|
const recurse = (e: NixValue) => {
|
||||||
|
if (!seen.has(e)) {
|
||||||
|
seen.add(e);
|
||||||
|
} else {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const forced = force(e);
|
||||||
|
if (Array.isArray(forced)) {
|
||||||
|
for (const val of forced) {
|
||||||
|
recurse(val);
|
||||||
|
}
|
||||||
|
} else if (isAttrs(forced)) {
|
||||||
|
for (const [_, val] of Object.entries(forced)) {
|
||||||
|
recurse(val);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
recurse(e1);
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const abort = (s: NixValue): never => {
|
||||||
|
throw new Error(`evaluation aborted with the following error message: '${force(s)}'`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const throwFunc = (s: NixValue): never => {
|
||||||
|
throw new CatchableError(coerceToString(s, StringCoercionMode.Base, false, new Set()));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const trace =
|
||||||
|
(e1: NixValue) =>
|
||||||
|
(e2: NixValue): NixValue => {
|
||||||
|
console.error(`trace: ${printValue(force(e1))}`);
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const warn =
|
||||||
|
(e1: NixValue) =>
|
||||||
|
(e2: NixValue): NixValue => {
|
||||||
|
console.log(`evaluation warning: ${force(e1)}`);
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const breakFunc = (v: NixValue): NixValue => v;
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { forceAttrs, forceStringNoCtx, forceStringValue } from "../type-assert";
|
||||||
|
import type { NixValue } from "../types";
|
||||||
|
import { realisePath } from "./io";
|
||||||
|
|
||||||
|
export const hashFile =
|
||||||
|
(type: NixValue) =>
|
||||||
|
(p: NixValue): string => {
|
||||||
|
const algo = forceStringNoCtx(type);
|
||||||
|
const pathStr = realisePath(p);
|
||||||
|
return Deno.core.ops.op_hash_file(algo, pathStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const hashString =
|
||||||
|
(type: NixValue) =>
|
||||||
|
(s: NixValue): string => {
|
||||||
|
const algo = forceStringNoCtx(type);
|
||||||
|
const data = forceStringValue(s);
|
||||||
|
return Deno.core.ops.op_hash_string(algo, data);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const convertHash = (args: NixValue): string => {
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
const hash = forceStringNoCtx(attrs.hash);
|
||||||
|
|
||||||
|
let hashAlgo: string | null = null;
|
||||||
|
if ("hashAlgo" in attrs) {
|
||||||
|
hashAlgo = forceStringNoCtx(attrs.hashAlgo);
|
||||||
|
}
|
||||||
|
|
||||||
|
const toHashFormat = forceStringNoCtx(attrs.toHashFormat);
|
||||||
|
|
||||||
|
return Deno.core.ops.op_convert_hash({
|
||||||
|
hash,
|
||||||
|
hashAlgo,
|
||||||
|
toHashFormat,
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,222 @@
|
|||||||
|
import { createThunk, force } from "../thunk";
|
||||||
|
import type { NixValue } from "../types";
|
||||||
|
import * as arithmetic from "./arithmetic";
|
||||||
|
import * as attrs from "./attrs";
|
||||||
|
import * as conversion from "./conversion";
|
||||||
|
import * as derivation from "./derivation";
|
||||||
|
import * as flake from "./flake";
|
||||||
|
import * as functional from "./functional";
|
||||||
|
import * as hash from "./hash";
|
||||||
|
import * as io from "./io";
|
||||||
|
import * as list from "./list";
|
||||||
|
import * as math from "./math";
|
||||||
|
import * as misc from "./misc";
|
||||||
|
import * as pathOps from "./path";
|
||||||
|
import * as string from "./string";
|
||||||
|
import * as typeCheck from "./type-check";
|
||||||
|
|
||||||
|
export const PRIMOP_METADATA = Symbol("primop_metadata");
|
||||||
|
|
||||||
|
export interface PrimopMetadata {
|
||||||
|
name: string;
|
||||||
|
arity: number;
|
||||||
|
applied: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const mkPrimop = (
|
||||||
|
func: (...args: NixValue[]) => NixValue,
|
||||||
|
name: string,
|
||||||
|
arity: number,
|
||||||
|
applied: number = 0,
|
||||||
|
): ((...args: NixValue[]) => NixValue) => {
|
||||||
|
(func as unknown as Record<symbol, unknown>)[PRIMOP_METADATA] = {
|
||||||
|
name,
|
||||||
|
arity,
|
||||||
|
applied,
|
||||||
|
} satisfies PrimopMetadata;
|
||||||
|
|
||||||
|
if (applied < arity - 1) {
|
||||||
|
const wrappedFunc = ((...args: NixValue[]) => {
|
||||||
|
const result = func(...args);
|
||||||
|
if (typeof result === "function") {
|
||||||
|
return mkPrimop(result, name, arity, applied + args.length);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}) as (...args: NixValue[]) => NixValue;
|
||||||
|
|
||||||
|
(wrappedFunc as unknown as Record<symbol, unknown>)[PRIMOP_METADATA] = {
|
||||||
|
name,
|
||||||
|
arity,
|
||||||
|
applied,
|
||||||
|
} satisfies PrimopMetadata;
|
||||||
|
|
||||||
|
return wrappedFunc;
|
||||||
|
}
|
||||||
|
|
||||||
|
return func;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isPrimop = (
|
||||||
|
value: unknown,
|
||||||
|
): value is ((...args: never[]) => unknown) & { [PRIMOP_METADATA]: PrimopMetadata } => {
|
||||||
|
return (
|
||||||
|
typeof value === "function" &&
|
||||||
|
PRIMOP_METADATA in value &&
|
||||||
|
typeof value[PRIMOP_METADATA] === "object" &&
|
||||||
|
value[PRIMOP_METADATA] !== null
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPrimopMetadata = (func: unknown): PrimopMetadata | undefined => {
|
||||||
|
if (isPrimop(func)) {
|
||||||
|
return func[PRIMOP_METADATA];
|
||||||
|
}
|
||||||
|
return undefined;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const builtins: Record<string, NixValue> = {
|
||||||
|
add: mkPrimop(arithmetic.add, "add", 2),
|
||||||
|
sub: mkPrimop(arithmetic.sub, "sub", 2),
|
||||||
|
mul: mkPrimop(arithmetic.mul, "mul", 2),
|
||||||
|
div: mkPrimop(arithmetic.div, "div", 2),
|
||||||
|
bitAnd: mkPrimop(arithmetic.bitAnd, "bitAnd", 2),
|
||||||
|
bitOr: mkPrimop(arithmetic.bitOr, "bitOr", 2),
|
||||||
|
bitXor: mkPrimop(arithmetic.bitXor, "bitXor", 2),
|
||||||
|
lessThan: mkPrimop(arithmetic.lessThan, "lessThan", 2),
|
||||||
|
|
||||||
|
ceil: mkPrimop(math.ceil, "ceil", 1),
|
||||||
|
floor: mkPrimop(math.floor, "floor", 1),
|
||||||
|
|
||||||
|
isAttrs: mkPrimop((e: NixValue) => typeCheck.isAttrs(force(e)), "isAttrs", 1),
|
||||||
|
isBool: mkPrimop((e: NixValue) => typeCheck.isBool(force(e)), "isBool", 1),
|
||||||
|
isFloat: mkPrimop((e: NixValue) => typeCheck.isFloat(force(e)), "isFloat", 1),
|
||||||
|
isFunction: mkPrimop((e: NixValue) => typeCheck.isFunction(force(e)), "isFunction", 1),
|
||||||
|
isInt: mkPrimop((e: NixValue) => typeCheck.isInt(force(e)), "isInt", 1),
|
||||||
|
isList: mkPrimop((e: NixValue) => typeCheck.isList(force(e)), "isList", 1),
|
||||||
|
isNull: mkPrimop((e: NixValue) => typeCheck.isNull(force(e)), "isNull", 1),
|
||||||
|
isPath: mkPrimop((e: NixValue) => typeCheck.isPath(force(e)), "isPath", 1),
|
||||||
|
isString: mkPrimop((e: NixValue) => typeCheck.isString(force(e)), "isString", 1),
|
||||||
|
typeOf: mkPrimop((e: NixValue) => typeCheck.typeOf(force(e)), "typeOf", 1),
|
||||||
|
|
||||||
|
map: mkPrimop(list.map, "map", 2),
|
||||||
|
filter: mkPrimop(list.filter, "filter", 2),
|
||||||
|
length: mkPrimop(list.length, "length", 1),
|
||||||
|
head: mkPrimop(list.head, "head", 1),
|
||||||
|
tail: mkPrimop(list.tail, "tail", 1),
|
||||||
|
elem: mkPrimop(list.elem, "elem", 2),
|
||||||
|
elemAt: mkPrimop(list.elemAt, "elemAt", 2),
|
||||||
|
concatLists: mkPrimop(list.concatLists, "concatLists", 1),
|
||||||
|
concatMap: mkPrimop(list.concatMap, "concatMap", 2),
|
||||||
|
"foldl'": mkPrimop(list.foldlPrime, "foldl'", 3),
|
||||||
|
sort: mkPrimop(list.sort, "sort", 2),
|
||||||
|
partition: mkPrimop(list.partition, "partition", 2),
|
||||||
|
genList: mkPrimop(list.genList, "genList", 2),
|
||||||
|
all: mkPrimop(list.all, "all", 2),
|
||||||
|
any: mkPrimop(list.any, "any", 2),
|
||||||
|
|
||||||
|
attrNames: mkPrimop(attrs.attrNames, "attrNames", 1),
|
||||||
|
attrValues: mkPrimop(attrs.attrValues, "attrValues", 1),
|
||||||
|
getAttr: mkPrimop(attrs.getAttr, "getAttr", 2),
|
||||||
|
hasAttr: mkPrimop(attrs.hasAttr, "hasAttr", 2),
|
||||||
|
mapAttrs: mkPrimop(attrs.mapAttrs, "mapAttrs", 2),
|
||||||
|
removeAttrs: mkPrimop(attrs.removeAttrs, "removeAttrs", 2),
|
||||||
|
listToAttrs: mkPrimop(attrs.listToAttrs, "listToAttrs", 1),
|
||||||
|
intersectAttrs: mkPrimop(attrs.intersectAttrs, "intersectAttrs", 2),
|
||||||
|
catAttrs: mkPrimop(attrs.catAttrs, "catAttrs", 2),
|
||||||
|
groupBy: mkPrimop(attrs.groupBy, "groupBy", 2),
|
||||||
|
zipAttrsWith: mkPrimop(attrs.zipAttrsWith, "zipAttrsWith", 2),
|
||||||
|
unsafeGetAttrPos: mkPrimop(attrs.unsafeGetAttrPos, "unsafeGetAttrPos", 2),
|
||||||
|
|
||||||
|
stringLength: mkPrimop(string.stringLength, "stringLength", 1),
|
||||||
|
substring: mkPrimop(string.substring, "substring", 3),
|
||||||
|
concatStringsSep: mkPrimop(string.concatStringsSep, "concatStringsSep", 2),
|
||||||
|
baseNameOf: mkPrimop(pathOps.baseNameOf, "baseNameOf", 1),
|
||||||
|
dirOf: mkPrimop(pathOps.dirOf, "dirOf", 1),
|
||||||
|
toPath: mkPrimop(pathOps.toPath, "toPath", 1),
|
||||||
|
match: mkPrimop(string.match, "match", 2),
|
||||||
|
split: mkPrimop(string.split, "split", 2),
|
||||||
|
|
||||||
|
seq: mkPrimop(functional.seq, "seq", 2),
|
||||||
|
deepSeq: mkPrimop(functional.deepSeq, "deepSeq", 2),
|
||||||
|
abort: mkPrimop(functional.abort, "abort", 1),
|
||||||
|
throw: mkPrimop(functional.throwFunc, "throw", 1),
|
||||||
|
trace: mkPrimop(functional.trace, "trace", 2),
|
||||||
|
warn: mkPrimop(functional.warn, "warn", 2),
|
||||||
|
break: mkPrimop(functional.breakFunc, "break", 1),
|
||||||
|
|
||||||
|
derivation: mkPrimop(derivation.derivationStub, "derivation", 1),
|
||||||
|
derivationStrict: mkPrimop(derivation.derivationStrict, "derivationStrict", 1),
|
||||||
|
|
||||||
|
import: mkPrimop(io.importFunc, "import", 1),
|
||||||
|
scopedImport: mkPrimop(io.scopedImport, "scopedImport", 2),
|
||||||
|
storePath: mkPrimop(io.storePath, "storePath", 1),
|
||||||
|
fetchClosure: mkPrimop(io.fetchClosure, "fetchClosure", 1),
|
||||||
|
fetchMercurial: mkPrimop(io.fetchMercurial, "fetchMercurial", 1),
|
||||||
|
fetchGit: mkPrimop(io.fetchGit, "fetchGit", 1),
|
||||||
|
fetchTarball: mkPrimop(io.fetchTarball, "fetchTarball", 1),
|
||||||
|
fetchTree: mkPrimop(io.fetchTree, "fetchTree", 1),
|
||||||
|
fetchurl: mkPrimop(io.fetchurl, "fetchurl", 1),
|
||||||
|
readDir: mkPrimop(io.readDir, "readDir", 1),
|
||||||
|
readFile: mkPrimop(io.readFile, "readFile", 1),
|
||||||
|
readFileType: mkPrimop(io.readFileType, "readFileType", 1),
|
||||||
|
pathExists: mkPrimop(io.pathExists, "pathExists", 1),
|
||||||
|
path: mkPrimop(io.path, "path", 1),
|
||||||
|
toFile: mkPrimop(io.toFile, "toFile", 2),
|
||||||
|
filterSource: mkPrimop(io.filterSource, "filterSource", 2),
|
||||||
|
findFile: mkPrimop(io.findFile, "findFile", 2),
|
||||||
|
getEnv: mkPrimop(io.getEnv, "getEnv", 1),
|
||||||
|
|
||||||
|
fromJSON: mkPrimop(conversion.fromJSON, "fromJSON", 1),
|
||||||
|
fromTOML: mkPrimop(conversion.fromTOML, "fromTOML", 1),
|
||||||
|
toJSON: mkPrimop(conversion.toJSON, "toJSON", 1),
|
||||||
|
toXML: mkPrimop(conversion.toXML, "toXML", 1),
|
||||||
|
toString: mkPrimop(conversion.toStringFunc, "toString", 1),
|
||||||
|
|
||||||
|
hashFile: mkPrimop(hash.hashFile, "hashFile", 2),
|
||||||
|
hashString: mkPrimop(hash.hashString, "hashString", 2),
|
||||||
|
convertHash: mkPrimop(hash.convertHash, "convertHash", 2),
|
||||||
|
|
||||||
|
flakeRefToString: mkPrimop(flake.flakeRefToString, "flakeRefToString", 1),
|
||||||
|
getFlake: mkPrimop(flake.getFlake, "getFlake", 1),
|
||||||
|
parseFlakeName: mkPrimop(flake.parseFlakeName, "parseFlakeName", 1),
|
||||||
|
parseFlakeRef: mkPrimop(flake.parseFlakeRef, "parseFlakeRef", 1),
|
||||||
|
|
||||||
|
addErrorContext: mkPrimop(misc.addErrorContext, "addErrorContext", 1),
|
||||||
|
appendContext: mkPrimop(misc.appendContext, "appendContext", 1),
|
||||||
|
getContext: mkPrimop(misc.getContext, "getContext", 1),
|
||||||
|
hasContext: mkPrimop(misc.hasContext, "hasContext", 1),
|
||||||
|
unsafeDiscardOutputDependency: mkPrimop(
|
||||||
|
misc.unsafeDiscardOutputDependency,
|
||||||
|
"unsafeDiscardOutputDependency",
|
||||||
|
1,
|
||||||
|
),
|
||||||
|
unsafeDiscardStringContext: mkPrimop(misc.unsafeDiscardStringContext, "unsafeDiscardStringContext", 1),
|
||||||
|
addDrvOutputDependencies: mkPrimop(misc.addDrvOutputDependencies, "addDrvOutputDependencies", 2),
|
||||||
|
compareVersions: mkPrimop(misc.compareVersions, "compareVersions", 2),
|
||||||
|
functionArgs: mkPrimop(misc.functionArgs, "functionArgs", 1),
|
||||||
|
genericClosure: mkPrimop(misc.genericClosure, "genericClosure", 1),
|
||||||
|
outputOf: mkPrimop(misc.outputOf, "outputOf", 2),
|
||||||
|
parseDrvName: mkPrimop(misc.parseDrvName, "parseDrvName", 1),
|
||||||
|
placeholder: mkPrimop(misc.placeholder, "placeholder", 1),
|
||||||
|
replaceStrings: mkPrimop(misc.replaceStrings, "replaceStrings", 3),
|
||||||
|
splitVersion: mkPrimop(misc.splitVersion, "splitVersion", 1),
|
||||||
|
traceVerbose: mkPrimop(misc.traceVerbose, "traceVerbose", 2),
|
||||||
|
tryEval: mkPrimop(misc.tryEval, "tryEval", 1),
|
||||||
|
|
||||||
|
builtins: createThunk(() => builtins, "builtins"),
|
||||||
|
currentSystem: createThunk(() => {
|
||||||
|
return "x86_64-linux";
|
||||||
|
}, "currentSystem"),
|
||||||
|
currentTime: createThunk(() => Date.now(), "currentTime"),
|
||||||
|
|
||||||
|
false: false,
|
||||||
|
true: true,
|
||||||
|
null: null,
|
||||||
|
|
||||||
|
langVersion: 6,
|
||||||
|
nixPath: [],
|
||||||
|
nixVersion: "2.31.2",
|
||||||
|
storeDir: createThunk(() => {
|
||||||
|
throw new Error("stub storeDir evaluated");
|
||||||
|
}),
|
||||||
|
};
|
||||||
@@ -0,0 +1,485 @@
|
|||||||
|
import { getPathValue } from "../path";
|
||||||
|
import type { NixStringContext, StringWithContext } from "../string-context";
|
||||||
|
import { addOpaqueContext, decodeContextElem, mkStringWithContext } from "../string-context";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import {
|
||||||
|
forceAttrs,
|
||||||
|
forceBool,
|
||||||
|
forceFunction,
|
||||||
|
forceList,
|
||||||
|
forceStringNoCtx,
|
||||||
|
forceStringValue,
|
||||||
|
} from "../type-assert";
|
||||||
|
import type { NixAttrs, NixPath, NixString, NixValue } from "../types";
|
||||||
|
import { CatchableError, IS_PATH, isNixPath } from "../types";
|
||||||
|
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
import { baseNameOf } from "./path";
|
||||||
|
import { isAttrs, isPath, isString } from "./type-check";
|
||||||
|
|
||||||
|
const importCache = new Map<string, NixValue>();
|
||||||
|
|
||||||
|
const realiseContext = (context: NixStringContext): void => {
|
||||||
|
for (const encoded of context) {
|
||||||
|
const elem = decodeContextElem(encoded);
|
||||||
|
if (elem.type === "built") {
|
||||||
|
throw new Error(
|
||||||
|
`cannot build derivation '${elem.drvPath}' during evaluation because import-from-derivation is not supported`,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const realisePath = (value: NixValue): string => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const pathStr = coerceToPath(value, context);
|
||||||
|
|
||||||
|
if (context.size > 0) {
|
||||||
|
realiseContext(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return pathStr;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const importFunc = (path: NixValue): NixValue => {
|
||||||
|
const pathStr = realisePath(path);
|
||||||
|
|
||||||
|
const cached = importCache.get(pathStr);
|
||||||
|
if (cached !== undefined) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
|
||||||
|
const code = Deno.core.ops.op_import(pathStr);
|
||||||
|
const result = Function(`return (${code})`)();
|
||||||
|
|
||||||
|
importCache.set(pathStr, result);
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const scopedImport =
|
||||||
|
(scope: NixValue) =>
|
||||||
|
(path: NixValue): NixValue => {
|
||||||
|
const scopeAttrs = forceAttrs(scope);
|
||||||
|
const scopeKeys = Object.keys(scopeAttrs);
|
||||||
|
|
||||||
|
const pathStr = realisePath(path);
|
||||||
|
|
||||||
|
const code = Deno.core.ops.op_scoped_import(pathStr, scopeKeys);
|
||||||
|
|
||||||
|
const scopedFunc = Function(`return (${code})`)();
|
||||||
|
return scopedFunc(scopeAttrs);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const storePath = (pathArg: NixValue): StringWithContext => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const pathStr = coerceToPath(pathArg, context);
|
||||||
|
|
||||||
|
const validatedPath: string = Deno.core.ops.op_store_path(pathStr);
|
||||||
|
|
||||||
|
context.add(validatedPath);
|
||||||
|
return mkStringWithContext(validatedPath, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchClosure = (_args: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: fetchClosure");
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface FetchUrlResult {
|
||||||
|
store_path: string;
|
||||||
|
hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchTarballResult {
|
||||||
|
store_path: string;
|
||||||
|
nar_hash: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface FetchGitResult {
|
||||||
|
out_path: string;
|
||||||
|
rev: string;
|
||||||
|
short_rev: string;
|
||||||
|
rev_count: number;
|
||||||
|
last_modified: number;
|
||||||
|
last_modified_date: string;
|
||||||
|
submodules: boolean;
|
||||||
|
nar_hash: string | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const normalizeUrlInput = (
|
||||||
|
args: NixValue,
|
||||||
|
): { url: string; hash?: string; name?: string; executable?: boolean } => {
|
||||||
|
const forced = force(args);
|
||||||
|
if (typeof forced === "string") {
|
||||||
|
return { url: forced };
|
||||||
|
}
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
const url = forceStringValue(attrs.url);
|
||||||
|
const hash =
|
||||||
|
"sha256" in attrs
|
||||||
|
? forceStringValue(attrs.sha256)
|
||||||
|
: "hash" in attrs
|
||||||
|
? forceStringValue(attrs.hash)
|
||||||
|
: undefined;
|
||||||
|
const name = "name" in attrs ? forceStringValue(attrs.name) : undefined;
|
||||||
|
const executable = "executable" in attrs ? forceBool(attrs.executable) : false;
|
||||||
|
return { url, hash, name, executable };
|
||||||
|
};
|
||||||
|
|
||||||
|
const normalizeTarballInput = (args: NixValue): { url: string; sha256?: string; name?: string } => {
|
||||||
|
const forced = force(args);
|
||||||
|
if (isAttrs(forced)) {
|
||||||
|
const url = resolvePseudoUrl(forceStringNoCtx(forced.url));
|
||||||
|
const sha256 = "sha256" in forced ? forceStringNoCtx(forced.sha256) : undefined;
|
||||||
|
const nameRaw = "name" in forced ? forceStringNoCtx(forced.name) : undefined;
|
||||||
|
// FIXME: extract baseNameOfRaw
|
||||||
|
const name = nameRaw === "" ? (baseNameOf(nameRaw) as string) : nameRaw;
|
||||||
|
return { url, sha256, name };
|
||||||
|
} else {
|
||||||
|
return { url: forceStringNoCtx(forced) };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const resolvePseudoUrl = (url: string) => {
|
||||||
|
if (url.startsWith("channel:")) {
|
||||||
|
return `https://channels.nixos.org/${url.substring(8)}/nixexprs.tar.xz`;
|
||||||
|
} else {
|
||||||
|
return url;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchurl = (args: NixValue): NixString => {
|
||||||
|
const { url, hash, name, executable } = normalizeUrlInput(args);
|
||||||
|
const result: FetchUrlResult = Deno.core.ops.op_fetch_url(
|
||||||
|
url,
|
||||||
|
hash ?? null,
|
||||||
|
name ?? null,
|
||||||
|
executable ?? false,
|
||||||
|
);
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
addOpaqueContext(context, result.store_path);
|
||||||
|
return mkStringWithContext(result.store_path, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTarball = (args: NixValue): NixString => {
|
||||||
|
const { url, name, sha256 } = normalizeTarballInput(args);
|
||||||
|
const result: FetchTarballResult = Deno.core.ops.op_fetch_tarball(url, name ?? null, sha256 ?? null);
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
addOpaqueContext(context, result.store_path);
|
||||||
|
return mkStringWithContext(result.store_path, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchGit = (args: NixValue): NixAttrs => {
|
||||||
|
const forced = force(args);
|
||||||
|
const disposedContext: NixStringContext = new Set();
|
||||||
|
if (isString(forced) || isPath(forced)) {
|
||||||
|
const url = coerceToString(forced, StringCoercionMode.Base, false, disposedContext);
|
||||||
|
const result = Deno.core.ops.op_fetch_git(url, null, null, false, false, false, null);
|
||||||
|
const outContext: NixStringContext = new Set();
|
||||||
|
addOpaqueContext(outContext, result.out_path);
|
||||||
|
return {
|
||||||
|
outPath: mkStringWithContext(result.out_path, outContext),
|
||||||
|
rev: result.rev,
|
||||||
|
shortRev: result.short_rev,
|
||||||
|
revCount: BigInt(result.rev_count),
|
||||||
|
lastModified: BigInt(result.last_modified),
|
||||||
|
lastModifiedDate: result.last_modified_date,
|
||||||
|
submodules: result.submodules,
|
||||||
|
narHash: result.nar_hash,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
const url = forceStringValue(attrs.url);
|
||||||
|
const gitRef = "ref" in attrs ? forceStringValue(attrs.ref) : null;
|
||||||
|
const rev = "rev" in attrs ? forceStringValue(attrs.rev) : null;
|
||||||
|
const shallow = "shallow" in attrs ? forceBool(attrs.shallow) : false;
|
||||||
|
const submodules = "submodules" in attrs ? forceBool(attrs.submodules) : false;
|
||||||
|
const allRefs = "allRefs" in attrs ? forceBool(attrs.allRefs) : false;
|
||||||
|
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
|
||||||
|
|
||||||
|
const result: FetchGitResult = Deno.core.ops.op_fetch_git(
|
||||||
|
url,
|
||||||
|
gitRef,
|
||||||
|
rev,
|
||||||
|
shallow,
|
||||||
|
submodules,
|
||||||
|
allRefs,
|
||||||
|
name,
|
||||||
|
);
|
||||||
|
|
||||||
|
const outContext: NixStringContext = new Set();
|
||||||
|
addOpaqueContext(outContext, result.out_path);
|
||||||
|
return {
|
||||||
|
outPath: mkStringWithContext(result.out_path, outContext),
|
||||||
|
rev: result.rev,
|
||||||
|
shortRev: result.short_rev,
|
||||||
|
revCount: BigInt(result.rev_count),
|
||||||
|
lastModified: BigInt(result.last_modified),
|
||||||
|
lastModifiedDate: result.last_modified_date,
|
||||||
|
submodules: result.submodules,
|
||||||
|
narHash: result.nar_hash,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchMercurial = (_args: NixValue): NixAttrs => {
|
||||||
|
throw new Error("Not implemented: fetchMercurial");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const fetchTree = (args: NixValue): NixAttrs => {
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
const type = "type" in attrs ? forceStringValue(attrs.type) : "auto";
|
||||||
|
|
||||||
|
switch (type) {
|
||||||
|
case "git":
|
||||||
|
return fetchGit(args);
|
||||||
|
case "hg":
|
||||||
|
case "mercurial":
|
||||||
|
return fetchMercurial(args);
|
||||||
|
case "tarball":
|
||||||
|
return { outPath: fetchTarball(args) };
|
||||||
|
case "file":
|
||||||
|
return { outPath: fetchurl(args) };
|
||||||
|
case "path": {
|
||||||
|
const path = forceStringValue(attrs.path);
|
||||||
|
return { outPath: path };
|
||||||
|
}
|
||||||
|
case "github":
|
||||||
|
case "gitlab":
|
||||||
|
case "sourcehut":
|
||||||
|
return fetchGitForge(type, attrs);
|
||||||
|
default:
|
||||||
|
return autoDetectAndFetch(attrs);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const fetchGitForge = (forge: string, attrs: NixAttrs): NixAttrs => {
|
||||||
|
const owner = forceStringValue(attrs.owner);
|
||||||
|
const repo = forceStringValue(attrs.repo);
|
||||||
|
const rev =
|
||||||
|
"rev" in attrs ? forceStringValue(attrs.rev) : "ref" in attrs ? forceStringValue(attrs.ref) : "HEAD";
|
||||||
|
const host = "host" in attrs ? forceStringValue(attrs.host) : undefined;
|
||||||
|
|
||||||
|
let tarballUrl: string;
|
||||||
|
switch (forge) {
|
||||||
|
case "github": {
|
||||||
|
const apiHost = host || "github.com";
|
||||||
|
tarballUrl = `https://api.${apiHost}/repos/${owner}/${repo}/tarball/${rev}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "gitlab": {
|
||||||
|
const glHost = host || "gitlab.com";
|
||||||
|
tarballUrl = `https://${glHost}/api/v4/projects/${owner}%2F${repo}/repository/archive.tar.gz?sha=${rev}`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "sourcehut": {
|
||||||
|
const shHost = host || "git.sr.ht";
|
||||||
|
tarballUrl = `https://${shHost}/${owner}/${repo}/archive/${rev}.tar.gz`;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
throw new Error(`Unknown forge type: ${forge}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const outPath = fetchTarball({ url: tarballUrl, ...attrs });
|
||||||
|
|
||||||
|
return {
|
||||||
|
outPath,
|
||||||
|
rev,
|
||||||
|
shortRev: rev.substring(0, 7),
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const autoDetectAndFetch = (attrs: NixAttrs): NixAttrs => {
|
||||||
|
const url = forceStringValue(attrs.url);
|
||||||
|
if (url.endsWith(".git") || url.includes("github.com") || url.includes("gitlab.com")) {
|
||||||
|
return fetchGit(attrs);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
url.endsWith(".tar.gz") ||
|
||||||
|
url.endsWith(".tar.xz") ||
|
||||||
|
url.endsWith(".tar.bz2") ||
|
||||||
|
url.endsWith(".tgz")
|
||||||
|
) {
|
||||||
|
return { outPath: fetchTarball(attrs) };
|
||||||
|
}
|
||||||
|
return { outPath: fetchurl(attrs) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readDir = (path: NixValue): NixAttrs => {
|
||||||
|
const pathStr = realisePath(path);
|
||||||
|
const entries: Record<string, string> = Deno.core.ops.op_read_dir(pathStr);
|
||||||
|
const result: NixAttrs = {};
|
||||||
|
for (const [name, type] of Object.entries(entries)) {
|
||||||
|
result[name] = type;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readFile = (path: NixValue): string => {
|
||||||
|
const pathStr = realisePath(path);
|
||||||
|
return Deno.core.ops.op_read_file(pathStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const readFileType = (path: NixValue): string => {
|
||||||
|
const pathStr = realisePath(path);
|
||||||
|
return Deno.core.ops.op_read_file_type(pathStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const pathExists = (path: NixValue): boolean => {
|
||||||
|
try {
|
||||||
|
const pathStr = realisePath(path);
|
||||||
|
return Deno.core.ops.op_path_exists(pathStr);
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.path
|
||||||
|
* Add a path to the Nix store with fine-grained control
|
||||||
|
*
|
||||||
|
* Parameters (attribute set):
|
||||||
|
* - path (required): Path to add to the store
|
||||||
|
* - name (optional): Name to use in store path (defaults to basename)
|
||||||
|
* - filter (optional): Function (path, type) -> bool
|
||||||
|
* - recursive (optional): Boolean, default true (NAR vs flat hashing)
|
||||||
|
* - sha256 (optional): Expected SHA-256 hash (hex-encoded)
|
||||||
|
*
|
||||||
|
* Returns: Store path string
|
||||||
|
*/
|
||||||
|
export const path = (args: NixValue): NixString => {
|
||||||
|
const attrs = forceAttrs(args);
|
||||||
|
|
||||||
|
if (!("path" in attrs)) {
|
||||||
|
throw new TypeError("builtins.path: 'path' attribute is required");
|
||||||
|
}
|
||||||
|
|
||||||
|
const pathValue = force(attrs.path);
|
||||||
|
let pathStr: string;
|
||||||
|
|
||||||
|
if (isNixPath(pathValue)) {
|
||||||
|
pathStr = getPathValue(pathValue);
|
||||||
|
} else {
|
||||||
|
pathStr = forceStringValue(pathValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
const name = "name" in attrs ? forceStringValue(attrs.name) : null;
|
||||||
|
const recursive = "recursive" in attrs ? forceBool(attrs.recursive) : true;
|
||||||
|
const sha256 = "sha256" in attrs ? forceStringValue(attrs.sha256) : null;
|
||||||
|
|
||||||
|
let storePath: string;
|
||||||
|
|
||||||
|
if ("filter" in attrs) {
|
||||||
|
const filterFn = forceFunction(attrs.filter);
|
||||||
|
|
||||||
|
const entries: [string, string][] = Deno.core.ops.op_walk_dir(pathStr);
|
||||||
|
|
||||||
|
const includePaths: string[] = [];
|
||||||
|
for (const [relPath, fileType] of entries) {
|
||||||
|
const fullPath = `${pathStr}/${relPath}`;
|
||||||
|
const innerFn = forceFunction(filterFn(fullPath));
|
||||||
|
const shouldInclude = force(innerFn(fileType));
|
||||||
|
if (shouldInclude === true) {
|
||||||
|
includePaths.push(relPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
storePath = Deno.core.ops.op_add_filtered_path(pathStr, name, recursive, sha256, includePaths);
|
||||||
|
} else {
|
||||||
|
storePath = Deno.core.ops.op_add_path(pathStr, name, recursive, sha256);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
addOpaqueContext(context, storePath);
|
||||||
|
return mkStringWithContext(storePath, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const toFile =
|
||||||
|
(nameArg: NixValue) =>
|
||||||
|
(contentsArg: NixValue): StringWithContext => {
|
||||||
|
const name = forceStringValue(nameArg);
|
||||||
|
|
||||||
|
if (name.includes("/")) {
|
||||||
|
throw new Error("builtins.toFile: name cannot contain '/'");
|
||||||
|
}
|
||||||
|
if (name === "." || name === "..") {
|
||||||
|
throw new Error("builtins.toFile: invalid name");
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const contents = coerceToString(contentsArg, StringCoercionMode.ToString, false, context);
|
||||||
|
|
||||||
|
const references: string[] = Array.from(context);
|
||||||
|
|
||||||
|
const storePath: string = Deno.core.ops.op_to_file(name, contents, references);
|
||||||
|
|
||||||
|
return mkStringWithContext(storePath, new Set([storePath]));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filterSource =
|
||||||
|
(_filter: NixValue) =>
|
||||||
|
(_path: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: filterSource");
|
||||||
|
};
|
||||||
|
|
||||||
|
const suffixIfPotentialMatch = (prefix: string, path: string): string | null => {
|
||||||
|
const n = prefix.length;
|
||||||
|
|
||||||
|
const needSeparator = n > 0 && n < path.length;
|
||||||
|
|
||||||
|
if (needSeparator && path[n] !== "/") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!path.startsWith(prefix)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return needSeparator ? path.substring(n + 1) : path.substring(n);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findFile =
|
||||||
|
(searchPath: NixValue) =>
|
||||||
|
(lookupPath: NixValue): NixPath => {
|
||||||
|
const forcedSearchPath = forceList(searchPath);
|
||||||
|
const lookupPathStr = forceStringNoCtx(lookupPath);
|
||||||
|
|
||||||
|
for (const item of forcedSearchPath) {
|
||||||
|
const attrs = forceAttrs(item);
|
||||||
|
|
||||||
|
const prefix = "prefix" in attrs ? forceStringNoCtx(attrs.prefix) : "";
|
||||||
|
|
||||||
|
if (!("path" in attrs)) {
|
||||||
|
throw new Error("findFile: search path element is missing 'path' attribute");
|
||||||
|
}
|
||||||
|
|
||||||
|
const suffix = suffixIfPotentialMatch(prefix, lookupPathStr);
|
||||||
|
if (suffix === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const pathVal = coerceToString(attrs.path, StringCoercionMode.Interpolation, false, context);
|
||||||
|
|
||||||
|
if (context.size > 0) {
|
||||||
|
throw new Error("findFile: path with string context is not yet supported");
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolvedPath = Deno.core.ops.op_resolve_path(pathVal, "");
|
||||||
|
const candidatePath =
|
||||||
|
suffix.length > 0 ? Deno.core.ops.op_resolve_path(suffix, resolvedPath) : resolvedPath;
|
||||||
|
|
||||||
|
if (Deno.core.ops.op_path_exists(candidatePath)) {
|
||||||
|
return { [IS_PATH]: true, value: candidatePath };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (lookupPathStr.startsWith("nix/")) {
|
||||||
|
// FIXME: special path type
|
||||||
|
return { [IS_PATH]: true, value: `<${lookupPathStr}>` };
|
||||||
|
}
|
||||||
|
|
||||||
|
throw new CatchableError(`file '${lookupPathStr}' was not found in the Nix search path`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getEnv = (s: NixValue): string => {
|
||||||
|
return Deno.core.ops.op_get_env(forceStringValue(s));
|
||||||
|
};
|
||||||
@@ -0,0 +1,144 @@
|
|||||||
|
import { op } from "../operators";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import { forceBool, forceFunction, forceInt, forceList } from "../type-assert";
|
||||||
|
import type { NixAttrs, NixList, NixValue } from "../types";
|
||||||
|
|
||||||
|
export const map =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(list: NixValue): NixList => {
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
if (forcedList.length) {
|
||||||
|
const func = forceFunction(f);
|
||||||
|
return forcedList.map(func);
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const filter =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(list: NixValue): NixList => {
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
if (forcedList.length) {
|
||||||
|
const func = forceFunction(f);
|
||||||
|
return forcedList.filter((e) => forceBool(func(e)));
|
||||||
|
}
|
||||||
|
return [];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const length = (e: NixValue): bigint => {
|
||||||
|
const forced = force(e);
|
||||||
|
if (typeof forced === "string") return BigInt(forced.length);
|
||||||
|
return BigInt(forceList(forced).length);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const head = (list: NixValue): NixValue => forceList(list)[0];
|
||||||
|
|
||||||
|
export const tail = (list: NixValue): NixList => forceList(list).slice(1);
|
||||||
|
|
||||||
|
export const elem =
|
||||||
|
(x: NixValue) =>
|
||||||
|
(xs: NixValue): boolean =>
|
||||||
|
forceList(xs).find((e) => op.eq(x, e)) !== undefined;
|
||||||
|
|
||||||
|
export const elemAt =
|
||||||
|
(xs: NixValue) =>
|
||||||
|
(n: NixValue): NixValue => {
|
||||||
|
const list = forceList(xs);
|
||||||
|
const idx = Number(forceInt(n));
|
||||||
|
|
||||||
|
if (idx < 0 || idx >= list.length) {
|
||||||
|
throw new RangeError(`Index ${idx} out of bounds for list of length ${list.length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return list[idx];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const concatLists = (lists: NixValue): NixList => {
|
||||||
|
return forceList(lists).reduce((acc: NixList, cur: NixValue) => {
|
||||||
|
return acc.concat(forceList(cur));
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const concatMap =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(lists: NixValue): NixList => {
|
||||||
|
const fn = forceFunction(f);
|
||||||
|
return forceList(lists).reduce((acc: NixList, cur: NixValue) => {
|
||||||
|
return acc.concat(force(fn(cur)) as NixList);
|
||||||
|
}, []);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const foldlPrime =
|
||||||
|
(opFn: NixValue) =>
|
||||||
|
(nul: NixValue) =>
|
||||||
|
(list: NixValue): NixValue => {
|
||||||
|
const forcedOp = forceFunction(opFn);
|
||||||
|
return forceList(list).reduce((acc: NixValue, cur: NixValue) => {
|
||||||
|
return forceFunction(forcedOp(acc))(cur);
|
||||||
|
}, nul);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sort =
|
||||||
|
(cmp: NixValue) =>
|
||||||
|
(list: NixValue): NixList => {
|
||||||
|
const forcedList = [...forceList(list)];
|
||||||
|
const forcedCmp = forceFunction(cmp);
|
||||||
|
return forcedList.sort((a, b) => {
|
||||||
|
if (force(forceFunction(forcedCmp(a))(b))) return -1;
|
||||||
|
if (force(forceFunction(forcedCmp(b))(a))) return 1;
|
||||||
|
return 0;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const partition =
|
||||||
|
(pred: NixValue) =>
|
||||||
|
(list: NixValue): NixAttrs => {
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
const forcedPred = forceFunction(pred);
|
||||||
|
const attrs = {
|
||||||
|
right: [] as NixList,
|
||||||
|
wrong: [] as NixList,
|
||||||
|
};
|
||||||
|
for (const elem of forcedList) {
|
||||||
|
if (force(forcedPred(elem))) {
|
||||||
|
attrs.right.push(elem);
|
||||||
|
} else {
|
||||||
|
attrs.wrong.push(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genList =
|
||||||
|
(f: NixValue) =>
|
||||||
|
(len: NixValue): NixList => {
|
||||||
|
const func = forceFunction(f);
|
||||||
|
const length = forceInt(len);
|
||||||
|
|
||||||
|
if (length < 0) {
|
||||||
|
throw new TypeError(`genList length must be non-negative integer, got ${length}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return [...Array(Number(length)).keys()].map((i) => func(BigInt(i)));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const all =
|
||||||
|
(pred: NixValue) =>
|
||||||
|
(list: NixValue): boolean => {
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
if (forcedList.length) {
|
||||||
|
const f = forceFunction(pred);
|
||||||
|
return forcedList.every((e) => forceBool(f(e)));
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const any =
|
||||||
|
(pred: NixValue) =>
|
||||||
|
(list: NixValue): boolean => {
|
||||||
|
// CppNix forces `pred` eagerly
|
||||||
|
const f = forceFunction(pred);
|
||||||
|
const forcedList = forceList(list);
|
||||||
|
// `false` when no element
|
||||||
|
return forcedList.some((e) => forceBool(f(e)));
|
||||||
|
};
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
import { forceNumeric } from "../type-assert";
|
||||||
|
import type { NixValue } from "../types";
|
||||||
|
|
||||||
|
export const ceil = (x: NixValue): bigint => {
|
||||||
|
const val = forceNumeric(x);
|
||||||
|
if (typeof val === "bigint") return val; // Already an integer
|
||||||
|
return BigInt(Math.ceil(val)); // Convert to integer
|
||||||
|
};
|
||||||
|
|
||||||
|
export const floor = (x: NixValue): bigint => {
|
||||||
|
const val = forceNumeric(x);
|
||||||
|
if (typeof val === "bigint") return val; // Already an integer
|
||||||
|
return BigInt(Math.floor(val)); // Convert to integer
|
||||||
|
};
|
||||||
@@ -0,0 +1,344 @@
|
|||||||
|
import { OrderedSet } from "js-sdsl";
|
||||||
|
import { compareValues } from "../operators";
|
||||||
|
import {
|
||||||
|
getStringContext,
|
||||||
|
getStringValue,
|
||||||
|
mkStringWithContext,
|
||||||
|
type NixStringContext,
|
||||||
|
} from "../string-context";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import {
|
||||||
|
forceAttrs,
|
||||||
|
forceFunction,
|
||||||
|
forceList,
|
||||||
|
forceString,
|
||||||
|
forceStringNoCtx,
|
||||||
|
forceStringValue,
|
||||||
|
} from "../type-assert";
|
||||||
|
import type { NixAttrs, NixBool, NixStrictValue, NixValue } from "../types";
|
||||||
|
import { ATTR_POSITIONS, CatchableError } from "../types";
|
||||||
|
import * as context from "./context";
|
||||||
|
import { isBool, isFloat, isInt, isList, isString, typeOf } from "./type-check";
|
||||||
|
|
||||||
|
export const addErrorContext =
|
||||||
|
(_e1: NixValue) =>
|
||||||
|
(e2: NixValue): NixValue => {
|
||||||
|
// FIXME:
|
||||||
|
// console.log("[WARNING]: addErrorContext not implemented");
|
||||||
|
return e2;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const appendContext = context.appendContext;
|
||||||
|
|
||||||
|
export const getContext = context.getContext;
|
||||||
|
|
||||||
|
export const hasContext = context.hasContext;
|
||||||
|
|
||||||
|
export const unsafeDiscardOutputDependency = context.unsafeDiscardOutputDependency;
|
||||||
|
|
||||||
|
export const unsafeDiscardStringContext = context.unsafeDiscardStringContext;
|
||||||
|
|
||||||
|
export const addDrvOutputDependencies = context.addDrvOutputDependencies;
|
||||||
|
|
||||||
|
export const compareVersions =
|
||||||
|
(s1: NixValue) =>
|
||||||
|
(s2: NixValue): NixValue => {
|
||||||
|
const str1 = forceStringValue(s1);
|
||||||
|
const str2 = forceStringValue(s2);
|
||||||
|
|
||||||
|
let i1 = 0;
|
||||||
|
let i2 = 0;
|
||||||
|
|
||||||
|
while (i1 < str1.length || i2 < str2.length) {
|
||||||
|
const c1 = nextComponent(str1, i1);
|
||||||
|
const c2 = nextComponent(str2, i2);
|
||||||
|
|
||||||
|
i1 = c1.nextIndex;
|
||||||
|
i2 = c2.nextIndex;
|
||||||
|
|
||||||
|
if (componentsLt(c1.component, c2.component)) {
|
||||||
|
return -1n;
|
||||||
|
} else if (componentsLt(c2.component, c1.component)) {
|
||||||
|
return 1n;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0n;
|
||||||
|
};
|
||||||
|
|
||||||
|
interface ComponentResult {
|
||||||
|
component: string;
|
||||||
|
nextIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextComponent(s: string, startIdx: number): ComponentResult {
|
||||||
|
let p = startIdx;
|
||||||
|
|
||||||
|
// Skip any dots and dashes (component separators)
|
||||||
|
while (p < s.length && (s[p] === "." || s[p] === "-")) {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (p >= s.length) {
|
||||||
|
return { component: "", nextIndex: p };
|
||||||
|
}
|
||||||
|
|
||||||
|
const start = p;
|
||||||
|
|
||||||
|
// If the first character is a digit, consume the longest sequence of digits
|
||||||
|
if (s[p] >= "0" && s[p] <= "9") {
|
||||||
|
while (p < s.length && s[p] >= "0" && s[p] <= "9") {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, consume the longest sequence of non-digit, non-separator characters
|
||||||
|
while (p < s.length && !(s[p] >= "0" && s[p] <= "9") && s[p] !== "." && s[p] !== "-") {
|
||||||
|
p++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return { component: s.substring(start, p), nextIndex: p };
|
||||||
|
}
|
||||||
|
|
||||||
|
function componentsLt(c1: string, c2: string): boolean {
|
||||||
|
const n1 = c1.match(/^[0-9]+$/) ? BigInt(c1) : null;
|
||||||
|
const n2 = c2.match(/^[0-9]+$/) ? BigInt(c2) : null;
|
||||||
|
|
||||||
|
// Both are numbers: compare numerically
|
||||||
|
if (n1 !== null && n2 !== null) {
|
||||||
|
return n1 < n2;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Empty string < number
|
||||||
|
if (c1 === "" && n2 !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Special case: "pre" comes before everything except another "pre"
|
||||||
|
if (c1 === "pre" && c2 !== "pre") {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (c2 === "pre") {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assume that `2.3a' < `2.3.1'
|
||||||
|
if (n2 !== null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (n1 !== null) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Both are strings: compare lexicographically
|
||||||
|
return c1 < c2;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const functionArgs = (f: NixValue): NixAttrs => {
|
||||||
|
const func = forceFunction(f);
|
||||||
|
if (func.args) {
|
||||||
|
const ret: NixAttrs = {};
|
||||||
|
for (const key of func.args.required) {
|
||||||
|
ret[key] = false;
|
||||||
|
}
|
||||||
|
for (const key of func.args.optional) {
|
||||||
|
ret[key] = true;
|
||||||
|
}
|
||||||
|
const positions = func.args.positions;
|
||||||
|
if (positions && Object.keys(positions).length > 0) {
|
||||||
|
Object.defineProperty(ret, ATTR_POSITIONS, {
|
||||||
|
value: positions,
|
||||||
|
enumerable: false,
|
||||||
|
writable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return ret;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
};
|
||||||
|
|
||||||
|
const checkComparable = (value: NixStrictValue): void => {
|
||||||
|
if (isString(value) || isInt(value) || isFloat(value) || isBool(value) || isList(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
throw new Error(`Unsupported key type for genericClosure: ${typeOf(value)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const genericClosure = (args: NixValue): NixValue => {
|
||||||
|
const forcedArgs = forceAttrs(args);
|
||||||
|
const { startSet, operator } = forcedArgs;
|
||||||
|
|
||||||
|
const initialList = forceList(startSet);
|
||||||
|
const opFunction = forceFunction(operator);
|
||||||
|
|
||||||
|
const resultSet = new OrderedSet<NixStrictValue>(undefined, compareValues);
|
||||||
|
const resultList: NixStrictValue[] = [];
|
||||||
|
const queue: NixStrictValue[] = [];
|
||||||
|
|
||||||
|
for (const item of initialList) {
|
||||||
|
const itemAttrs = forceAttrs(item);
|
||||||
|
const key = force(itemAttrs.key);
|
||||||
|
checkComparable(key);
|
||||||
|
if (resultSet.find(key).equals(resultSet.end())) {
|
||||||
|
resultSet.insert(key);
|
||||||
|
resultList.push(itemAttrs);
|
||||||
|
queue.push(itemAttrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let head = 0;
|
||||||
|
while (head < queue.length) {
|
||||||
|
const currentItem = queue[head++];
|
||||||
|
const newItems = forceList(opFunction(currentItem));
|
||||||
|
|
||||||
|
for (const newItem of newItems) {
|
||||||
|
const newItemAttrs = forceAttrs(newItem);
|
||||||
|
const key = force(newItemAttrs.key);
|
||||||
|
checkComparable(key);
|
||||||
|
if (resultSet.find(key).equals(resultSet.end())) {
|
||||||
|
resultSet.insert(key);
|
||||||
|
resultList.push(newItemAttrs);
|
||||||
|
queue.push(newItemAttrs);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resultList;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const outputOf =
|
||||||
|
(_drv: NixValue) =>
|
||||||
|
(_out: NixValue): never => {
|
||||||
|
throw new Error("Not implemented: outputOf (part of dynamic-derivation)");
|
||||||
|
};
|
||||||
|
|
||||||
|
export const parseDrvName = (s: NixValue): NixAttrs => {
|
||||||
|
const fullName = forceStringNoCtx(s);
|
||||||
|
let name = fullName;
|
||||||
|
let version = "";
|
||||||
|
for (let i = 0; i < fullName.length; ++i) {
|
||||||
|
if (fullName[i] === "-" && i + 1 < fullName.length && !/[a-zA-Z]/.test(fullName[i + 1])) {
|
||||||
|
name = fullName.substring(0, i);
|
||||||
|
version = fullName.substring(i + 1);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
name,
|
||||||
|
version,
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
export const placeholder = (output: NixValue): NixValue => {
|
||||||
|
const outputStr = forceStringNoCtx(output);
|
||||||
|
return Deno.core.ops.op_make_placeholder(outputStr);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const replaceStrings =
|
||||||
|
(from: NixValue) =>
|
||||||
|
(to: NixValue) =>
|
||||||
|
(s: NixValue): NixValue => {
|
||||||
|
const fromList = forceList(from);
|
||||||
|
const toList = forceList(to);
|
||||||
|
const inputStr = forceString(s);
|
||||||
|
const inputStrValue = getStringValue(inputStr);
|
||||||
|
const resultContext: NixStringContext = getStringContext(inputStr);
|
||||||
|
|
||||||
|
if (fromList.length !== toList.length) {
|
||||||
|
throw new Error("'from' and 'to' arguments passed to builtins.replaceStrings have different lengths");
|
||||||
|
}
|
||||||
|
|
||||||
|
const toCache = new Map<number, string>();
|
||||||
|
const toContextCache = new Map<number, NixStringContext>();
|
||||||
|
|
||||||
|
let result = "";
|
||||||
|
let pos = 0;
|
||||||
|
|
||||||
|
while (pos <= inputStrValue.length) {
|
||||||
|
let found = false;
|
||||||
|
|
||||||
|
for (let i = 0; i < fromList.length; i++) {
|
||||||
|
const pattern = forceStringValue(fromList[i]);
|
||||||
|
|
||||||
|
if (inputStrValue.substring(pos).startsWith(pattern)) {
|
||||||
|
found = true;
|
||||||
|
|
||||||
|
if (!toCache.has(i)) {
|
||||||
|
const replacementStr = forceString(toList[i]);
|
||||||
|
const replacementValue = getStringValue(replacementStr);
|
||||||
|
const replacementContext = getStringContext(replacementStr);
|
||||||
|
toCache.set(i, replacementValue);
|
||||||
|
toContextCache.set(i, replacementContext);
|
||||||
|
for (const elem of replacementContext) {
|
||||||
|
resultContext.add(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const replacement = toCache.get(i) as string;
|
||||||
|
|
||||||
|
result += replacement;
|
||||||
|
|
||||||
|
if (pattern.length === 0) {
|
||||||
|
if (pos < inputStrValue.length) {
|
||||||
|
result += inputStrValue[pos];
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
} else {
|
||||||
|
pos += pattern.length;
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!found) {
|
||||||
|
if (pos < inputStrValue.length) {
|
||||||
|
result += inputStrValue[pos];
|
||||||
|
}
|
||||||
|
pos++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resultContext.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, resultContext);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const splitVersion = (s: NixValue): NixValue => {
|
||||||
|
const version = forceStringValue(s);
|
||||||
|
const components: string[] = [];
|
||||||
|
let idx = 0;
|
||||||
|
|
||||||
|
while (idx < version.length) {
|
||||||
|
const result = nextComponent(version, idx);
|
||||||
|
if (result.component === "") {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
components.push(result.component);
|
||||||
|
idx = result.nextIndex;
|
||||||
|
}
|
||||||
|
|
||||||
|
return components;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const traceVerbose = (_e1: NixValue, e2: NixValue): NixStrictValue => {
|
||||||
|
// TODO: implement traceVerbose
|
||||||
|
return force(e2)
|
||||||
|
};
|
||||||
|
|
||||||
|
export const tryEval = (e: NixValue): { success: NixBool; value: NixStrictValue } => {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
value: force(e),
|
||||||
|
};
|
||||||
|
} catch (err) {
|
||||||
|
if (err instanceof CatchableError) {
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
value: false,
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
import { mkPath } from "../path";
|
||||||
|
import { mkStringWithContext, type NixStringContext } from "../string-context";
|
||||||
|
import { force } from "../thunk";
|
||||||
|
import type { NixPath, NixString, NixValue } from "../types";
|
||||||
|
import { isNixPath } from "../types";
|
||||||
|
import { coerceToPath, coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.baseNameOf
|
||||||
|
* Get the last component of a path or string
|
||||||
|
* Always returns a string (coerces paths)
|
||||||
|
* Preserves string context if present
|
||||||
|
*
|
||||||
|
* Implements Nix's legacyBaseNameOf logic:
|
||||||
|
* - If string ends with '/', removes only the final slash
|
||||||
|
* - Then returns everything after the last remaining '/'
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - baseNameOf ./foo/bar → "bar"
|
||||||
|
* - baseNameOf "/foo/bar/" → "bar" (trailing slash removed first)
|
||||||
|
* - baseNameOf "foo" → "foo"
|
||||||
|
*/
|
||||||
|
export const baseNameOf = (s: NixValue): NixString => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const pathStr = coerceToString(s, StringCoercionMode.Base, false, context);
|
||||||
|
|
||||||
|
if (pathStr.length === 0) {
|
||||||
|
if (context.size === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return mkStringWithContext("", context);
|
||||||
|
}
|
||||||
|
|
||||||
|
let last = pathStr.length - 1;
|
||||||
|
if (pathStr[last] === "/" && last > 0) {
|
||||||
|
last -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let pos = last;
|
||||||
|
while (pos >= 0 && pathStr[pos] !== "/") {
|
||||||
|
pos -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pos === -1) {
|
||||||
|
pos = 0;
|
||||||
|
} else {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = pathStr.substring(pos, last + 1);
|
||||||
|
|
||||||
|
// Preserve string context if present
|
||||||
|
if (context.size > 0) {
|
||||||
|
return mkStringWithContext(result, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.dirOf
|
||||||
|
* Get the directory part of a path or string
|
||||||
|
* TYPE-PRESERVING: path → path, string → string
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - dirOf ./foo/bar → ./foo (path)
|
||||||
|
* - dirOf "/foo/bar" → "/foo" (string)
|
||||||
|
* - dirOf "/" → "/" (same type as input)
|
||||||
|
*/
|
||||||
|
export const dirOf = (s: NixValue): NixPath | NixString => {
|
||||||
|
const forced = force(s);
|
||||||
|
|
||||||
|
// Path input → path output
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
const pathStr = forced.value;
|
||||||
|
const lastSlash = pathStr.lastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastSlash === -1) {
|
||||||
|
return mkPath(".");
|
||||||
|
}
|
||||||
|
if (lastSlash === 0) {
|
||||||
|
return mkPath("/");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkPath(pathStr.slice(0, lastSlash));
|
||||||
|
}
|
||||||
|
|
||||||
|
// String input → string output
|
||||||
|
const outContext: NixStringContext = new Set();
|
||||||
|
const pathStr = coerceToString(s, StringCoercionMode.Base, false, outContext);
|
||||||
|
|
||||||
|
const lastSlash = pathStr.lastIndexOf("/");
|
||||||
|
|
||||||
|
if (lastSlash === -1) {
|
||||||
|
return ".";
|
||||||
|
}
|
||||||
|
if (lastSlash === 0) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = pathStr.slice(0, lastSlash);
|
||||||
|
|
||||||
|
if (outContext.size > 0) {
|
||||||
|
return mkStringWithContext(result, outContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* builtins.toPath
|
||||||
|
* Convert a value to an absolute path string.
|
||||||
|
* DEPRECATED: Use `/. + "/path"` to convert a string into an absolute path.
|
||||||
|
*
|
||||||
|
* This validates that the input can be coerced to an absolute path.
|
||||||
|
* Returns a **string** (not a NixPath), with context preserved.
|
||||||
|
*
|
||||||
|
* Examples:
|
||||||
|
* - toPath "/foo" → "/foo" (string)
|
||||||
|
* - toPath "/foo/bar" → "/foo/bar" (string)
|
||||||
|
* - toPath "foo" → ERROR (not absolute)
|
||||||
|
* - toPath "" → ERROR (empty)
|
||||||
|
*/
|
||||||
|
export const toPath = (s: NixValue): NixString => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const pathStr = coerceToPath(s, context);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return pathStr;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkStringWithContext(pathStr, context);
|
||||||
|
};
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
import {
|
||||||
|
getStringContext,
|
||||||
|
getStringValue,
|
||||||
|
mkStringWithContext,
|
||||||
|
type NixStringContext,
|
||||||
|
} from "../string-context";
|
||||||
|
import { forceInt, forceList, forceString, forceStringValue } from "../type-assert";
|
||||||
|
import type { NixInt, NixString, NixValue } from "../types";
|
||||||
|
import { coerceToString, StringCoercionMode } from "./conversion";
|
||||||
|
|
||||||
|
export const stringLength = (e: NixValue): NixInt => BigInt(forceStringValue(e).length);
|
||||||
|
|
||||||
|
export const substring =
|
||||||
|
(start: NixValue) =>
|
||||||
|
(len: NixValue) =>
|
||||||
|
(s: NixValue): NixString => {
|
||||||
|
const startPos = Number(forceInt(start));
|
||||||
|
const length = Number(forceInt(len));
|
||||||
|
|
||||||
|
if (startPos < 0) {
|
||||||
|
throw new Error("negative start position in 'substring'");
|
||||||
|
}
|
||||||
|
|
||||||
|
const str = forceString(s);
|
||||||
|
const strValue = getStringValue(str);
|
||||||
|
const context = getStringContext(str);
|
||||||
|
|
||||||
|
if (length === 0) {
|
||||||
|
if (context.size === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return mkStringWithContext("", context);
|
||||||
|
}
|
||||||
|
|
||||||
|
const actualLength = length < 0 ? Number.MAX_SAFE_INTEGER : length;
|
||||||
|
const result = startPos >= strValue.length ? "" : strValue.substring(startPos, startPos + actualLength);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const concatStringsSep =
|
||||||
|
(sep: NixValue) =>
|
||||||
|
(list: NixValue): NixString => {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const separator = coerceToString(sep, StringCoercionMode.Interpolation, false, context);
|
||||||
|
|
||||||
|
const parts = forceList(list).map((elem) =>
|
||||||
|
coerceToString(elem, StringCoercionMode.Interpolation, false, context),
|
||||||
|
);
|
||||||
|
|
||||||
|
const result = parts.join(separator);
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const match =
|
||||||
|
(regex: NixValue) =>
|
||||||
|
(str: NixValue): NixValue => {
|
||||||
|
const regexStr = forceStringValue(regex);
|
||||||
|
const inputStr = forceStringValue(str);
|
||||||
|
|
||||||
|
const result = Deno.core.ops.op_match(regexStr, inputStr);
|
||||||
|
if (result === null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return result.map((g) => (g !== null ? g : null));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const split =
|
||||||
|
(regex: NixValue) =>
|
||||||
|
(str: NixValue): NixValue => {
|
||||||
|
const regexStr = forceStringValue(regex);
|
||||||
|
const inputStr = forceString(str);
|
||||||
|
const inputStrValue = getStringValue(inputStr);
|
||||||
|
|
||||||
|
const result = Deno.core.ops.op_split(regexStr, inputStrValue);
|
||||||
|
|
||||||
|
if (result.length === 1 && typeof result[0] === "string") {
|
||||||
|
return [inputStr];
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.map((item) => {
|
||||||
|
if (typeof item === "string") {
|
||||||
|
return item;
|
||||||
|
}
|
||||||
|
return item.map((g) => (g !== null ? g : null));
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -0,0 +1,67 @@
|
|||||||
|
import {
|
||||||
|
HAS_CONTEXT,
|
||||||
|
isNixPath,
|
||||||
|
isStringWithContext,
|
||||||
|
type NixAttrs,
|
||||||
|
type NixBool,
|
||||||
|
type NixFloat,
|
||||||
|
type NixFunction,
|
||||||
|
type NixInt,
|
||||||
|
type NixList,
|
||||||
|
type NixNull,
|
||||||
|
type NixPath,
|
||||||
|
type NixStrictValue,
|
||||||
|
type NixString,
|
||||||
|
} from "../types";
|
||||||
|
|
||||||
|
export const isNixString = (v: NixStrictValue): v is NixString => {
|
||||||
|
return typeof v === "string" || isStringWithContext(v);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isAttrs = (e: NixStrictValue): e is NixAttrs => {
|
||||||
|
const val = e;
|
||||||
|
return (
|
||||||
|
typeof val === "object" && !Array.isArray(val) && val !== null && !(HAS_CONTEXT in val) && !isPath(val)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isBool = (e: NixStrictValue): e is NixBool => typeof e === "boolean";
|
||||||
|
|
||||||
|
export const isFloat = (e: NixStrictValue): e is NixFloat => {
|
||||||
|
const val = e;
|
||||||
|
return typeof val === "number"; // Only number is float
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isFunction = (e: NixStrictValue): e is NixFunction => typeof e === "function";
|
||||||
|
|
||||||
|
export const isInt = (e: NixStrictValue): e is NixInt => {
|
||||||
|
const val = e;
|
||||||
|
return typeof val === "bigint"; // Only bigint is int
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isList = (e: NixStrictValue): e is NixList => Array.isArray(e);
|
||||||
|
|
||||||
|
export const isNull = (e: NixStrictValue): e is NixNull => e === null;
|
||||||
|
|
||||||
|
export const isPath = (e: NixStrictValue): e is NixPath => {
|
||||||
|
const val = e;
|
||||||
|
return isNixPath(val);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const isString = (e: NixStrictValue): e is NixString =>
|
||||||
|
typeof e === "string" || isStringWithContext(e);
|
||||||
|
|
||||||
|
export type NixType = "int" | "float" | "bool" | "string" | "path" | "null" | "list" | "lambda" | "set";
|
||||||
|
export const typeOf = (e: NixStrictValue): NixType => {
|
||||||
|
if (typeof e === "bigint") return "int";
|
||||||
|
if (typeof e === "number") return "float";
|
||||||
|
if (typeof e === "boolean") return "bool";
|
||||||
|
if (e === null) return "null";
|
||||||
|
if (isNixString(e)) return "string";
|
||||||
|
if (isNixPath(e)) return "path";
|
||||||
|
if (Array.isArray(e)) return "list";
|
||||||
|
if (typeof e === "object") return "set";
|
||||||
|
if (typeof e === "function") return "lambda";
|
||||||
|
|
||||||
|
throw new TypeError(`Unknown Nix type: ${typeof e}`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,328 @@
|
|||||||
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
|
import { isAttrs, typeOf } from "./builtins/type-check";
|
||||||
|
import { mkPath } from "./path";
|
||||||
|
import { isStringWithContext, mkStringWithContext, type NixStringContext } from "./string-context";
|
||||||
|
import { force } from "./thunk";
|
||||||
|
import { forceAttrs, forceBool, forceFunction, forceStringValue } from "./type-assert";
|
||||||
|
import type { NixAttrs, NixBool, NixPath, NixString, NixValue } from "./types";
|
||||||
|
import { CatchableError, isNixPath } from "./types";
|
||||||
|
|
||||||
|
interface StackFrame {
|
||||||
|
span: string;
|
||||||
|
message: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const callStack: StackFrame[] = [];
|
||||||
|
const MAX_STACK_DEPTH = 1000;
|
||||||
|
|
||||||
|
function enrichError(error: unknown): Error {
|
||||||
|
const err = error instanceof Error ? error : new Error(String(error));
|
||||||
|
|
||||||
|
if (callStack.length === 0) {
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const nixStackLines = callStack.map((frame) => {
|
||||||
|
return `NIX_STACK_FRAME:${frame.span}:${frame.message}`;
|
||||||
|
});
|
||||||
|
|
||||||
|
// Prepend stack frames to error stack
|
||||||
|
err.stack = `${nixStackLines.join("\n")}\n${err.stack || ""}`;
|
||||||
|
|
||||||
|
return err;
|
||||||
|
}
|
||||||
|
|
||||||
|
const pushContext = (message: string, span: string): void => {
|
||||||
|
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||||
|
callStack.shift();
|
||||||
|
}
|
||||||
|
callStack.push({ span, message });
|
||||||
|
};
|
||||||
|
|
||||||
|
const popContext = (): void => {
|
||||||
|
callStack.pop();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const withContext = <T>(message: string, span: string, fn: () => T): T => {
|
||||||
|
pushContext(message, span);
|
||||||
|
try {
|
||||||
|
return fn();
|
||||||
|
} catch (error) {
|
||||||
|
throw enrichError(error);
|
||||||
|
} finally {
|
||||||
|
popContext();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Concatenate multiple values into a string or path with context
|
||||||
|
* This is used for string interpolation like "hello ${world}"
|
||||||
|
*
|
||||||
|
* IMPORTANT: String context handling:
|
||||||
|
* - All contexts from interpolated values are merged into the result
|
||||||
|
* - Path mode: Store contexts are forbidden (will throw error)
|
||||||
|
* - String mode: All contexts are preserved and merged
|
||||||
|
*
|
||||||
|
* @param parts - Array of values to concatenate
|
||||||
|
* @param forceString - If true, result is always a string (paths are copied to store)
|
||||||
|
* @returns String or Path with merged contexts from all parts
|
||||||
|
*/
|
||||||
|
export const concatStringsWithContext = (parts: NixValue[], forceString: boolean): NixString | NixPath => {
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
const forced = parts.map(force);
|
||||||
|
|
||||||
|
const firstIsPath = !forceString && isNixPath(forced[0]);
|
||||||
|
|
||||||
|
if (firstIsPath) {
|
||||||
|
let result = (forced[0] as NixPath).value;
|
||||||
|
|
||||||
|
for (let i = 1; i < forced.length; i++) {
|
||||||
|
const part = forced[i];
|
||||||
|
|
||||||
|
if (isNixPath(part)) {
|
||||||
|
result += part.value;
|
||||||
|
} else if (typeof part === "string") {
|
||||||
|
result += part;
|
||||||
|
} else if (isStringWithContext(part)) {
|
||||||
|
if (part.context.size > 0) {
|
||||||
|
throw new TypeError("a string that refers to a store path cannot be appended to a path");
|
||||||
|
}
|
||||||
|
result += part.value;
|
||||||
|
} else {
|
||||||
|
const tempContext: NixStringContext = new Set();
|
||||||
|
const coerced = coerceToString(part, StringCoercionMode.Interpolation, false, tempContext);
|
||||||
|
|
||||||
|
if (tempContext.size > 0) {
|
||||||
|
throw new TypeError("a string that refers to a store path cannot be appended to a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
result += coerced;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkPath(result);
|
||||||
|
}
|
||||||
|
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const strParts: string[] = [];
|
||||||
|
|
||||||
|
for (const part of forced) {
|
||||||
|
if (isNixPath(part)) {
|
||||||
|
const str = coerceToString(part, StringCoercionMode.Interpolation, true, context);
|
||||||
|
strParts.push(str);
|
||||||
|
} else {
|
||||||
|
const str = coerceToString(part, StringCoercionMode.Interpolation, false, context);
|
||||||
|
strParts.push(str);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = strParts.join("");
|
||||||
|
|
||||||
|
if (context.size === 0) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkStringWithContext(value, context);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const resolvePath = (currentDir: string, path: NixValue): NixPath => {
|
||||||
|
const forced = force(path);
|
||||||
|
let pathStr: string;
|
||||||
|
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
pathStr = forced.value;
|
||||||
|
} else {
|
||||||
|
pathStr = forceStringValue(path);
|
||||||
|
}
|
||||||
|
|
||||||
|
const resolved = Deno.core.ops.op_resolve_path(currentDir, pathStr);
|
||||||
|
return mkPath(resolved);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const select = (obj: NixValue, attrpath: NixValue[], span?: string): NixValue => {
|
||||||
|
if (span) {
|
||||||
|
const pathStrings = attrpath.map((a) => forceStringValue(a));
|
||||||
|
const path = pathStrings.join(".");
|
||||||
|
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
|
||||||
|
|
||||||
|
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||||
|
callStack.shift();
|
||||||
|
}
|
||||||
|
callStack.push({ span, message });
|
||||||
|
try {
|
||||||
|
return selectImpl(obj, attrpath);
|
||||||
|
} catch (error) {
|
||||||
|
throw enrichError(error);
|
||||||
|
} finally {
|
||||||
|
callStack.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return selectImpl(obj, attrpath);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectImpl(obj: NixValue, attrpath: NixValue[]): NixValue {
|
||||||
|
let attrs = forceAttrs(obj);
|
||||||
|
|
||||||
|
for (const attr of attrpath.slice(0, -1)) {
|
||||||
|
const key = forceStringValue(attr);
|
||||||
|
if (!(key in attrs)) {
|
||||||
|
throw new Error(`Attribute '${key}' not found`);
|
||||||
|
}
|
||||||
|
const cur = forceAttrs(attrs[forceStringValue(attr)]);
|
||||||
|
attrs = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = forceStringValue(attrpath[attrpath.length - 1]);
|
||||||
|
if (!(last in attrs)) {
|
||||||
|
throw new Error(`Attribute '${last}' not found`);
|
||||||
|
}
|
||||||
|
return attrs[last];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const selectWithDefault = (
|
||||||
|
obj: NixValue,
|
||||||
|
attrpath: NixValue[],
|
||||||
|
defaultVal: NixValue,
|
||||||
|
span?: string,
|
||||||
|
): NixValue => {
|
||||||
|
if (span) {
|
||||||
|
const pathStrings = attrpath.map((a) => forceStringValue(a));
|
||||||
|
const path = pathStrings.join(".");
|
||||||
|
const message = path ? `while selecting attribute [${path}]` : "while selecting attribute";
|
||||||
|
|
||||||
|
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||||
|
callStack.shift();
|
||||||
|
}
|
||||||
|
callStack.push({ span, message });
|
||||||
|
try {
|
||||||
|
return selectWithDefaultImpl(obj, attrpath, defaultVal);
|
||||||
|
} catch (error) {
|
||||||
|
throw enrichError(error);
|
||||||
|
} finally {
|
||||||
|
callStack.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return selectWithDefaultImpl(obj, attrpath, defaultVal);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function selectWithDefaultImpl(obj: NixValue, attrpath: NixValue[], defaultVal: NixValue): NixValue {
|
||||||
|
let attrs = force(obj);
|
||||||
|
if (!isAttrs(attrs)) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const attr of attrpath.slice(0, -1)) {
|
||||||
|
const key = forceStringValue(attr);
|
||||||
|
if (!(key in attrs)) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
const cur = force(attrs[key]);
|
||||||
|
if (!isAttrs(cur)) {
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
attrs = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
const last = forceStringValue(attrpath[attrpath.length - 1]);
|
||||||
|
if (last in attrs) {
|
||||||
|
return attrs[last];
|
||||||
|
}
|
||||||
|
return defaultVal;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const hasAttr = (obj: NixValue, attrpath: NixValue[]): NixBool => {
|
||||||
|
const forced = force(obj);
|
||||||
|
if (!isAttrs(forced)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
let attrs = forced;
|
||||||
|
|
||||||
|
for (const attr of attrpath.slice(0, -1)) {
|
||||||
|
const cur = force(attrs[forceStringValue(attr)]);
|
||||||
|
if (!isAttrs(cur)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
attrs = cur;
|
||||||
|
}
|
||||||
|
|
||||||
|
return forceStringValue(attrpath[attrpath.length - 1]) in attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const call = (func: NixValue, arg: NixValue, span?: string): NixValue => {
|
||||||
|
if (span) {
|
||||||
|
if (callStack.length >= MAX_STACK_DEPTH) {
|
||||||
|
callStack.shift();
|
||||||
|
}
|
||||||
|
callStack.push({ span, message: "from call site" });
|
||||||
|
try {
|
||||||
|
return callImpl(func, arg);
|
||||||
|
} catch (error) {
|
||||||
|
throw enrichError(error);
|
||||||
|
} finally {
|
||||||
|
callStack.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return callImpl(func, arg);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
function callImpl(func: NixValue, arg: NixValue): NixValue {
|
||||||
|
const forcedFunc = force(func);
|
||||||
|
if (typeof forcedFunc === "function") {
|
||||||
|
forcedFunc.args?.check(arg);
|
||||||
|
return forcedFunc(arg);
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
typeof forcedFunc === "object" &&
|
||||||
|
!Array.isArray(forcedFunc) &&
|
||||||
|
forcedFunc !== null &&
|
||||||
|
"__functor" in forcedFunc
|
||||||
|
) {
|
||||||
|
const functor = forceFunction(forcedFunc.__functor);
|
||||||
|
return call(functor(forcedFunc), arg);
|
||||||
|
}
|
||||||
|
throw new Error(`attempt to call something which is not a function but ${typeOf(forcedFunc)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const assert = (assertion: NixValue, expr: NixValue, assertionRaw: string, span: string): NixValue => {
|
||||||
|
if (forceBool(assertion)) {
|
||||||
|
return expr;
|
||||||
|
}
|
||||||
|
withContext("while evaluating assertion", span, () => {
|
||||||
|
throw new CatchableError(`assertion '${assertionRaw}' failed`);
|
||||||
|
});
|
||||||
|
throw "unreachable";
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ifFunc = (cond: NixValue, consq: NixValue, alter: NixValue) => {
|
||||||
|
if (forceBool(cond)) {
|
||||||
|
return consq;
|
||||||
|
}
|
||||||
|
return alter;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mkPos = (span: string): NixAttrs => {
|
||||||
|
return Deno.core.ops.op_decode_span(span);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface WithScope {
|
||||||
|
env: NixValue;
|
||||||
|
last: WithScope | null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const lookupWith = (name: string, withScope: WithScope | null): NixValue => {
|
||||||
|
let current = withScope;
|
||||||
|
while (current !== null) {
|
||||||
|
const attrs = forceAttrs(current.env);
|
||||||
|
if (name in attrs) {
|
||||||
|
return attrs[name];
|
||||||
|
}
|
||||||
|
current = current.last;
|
||||||
|
}
|
||||||
|
throw new Error(`undefined variable '${name}'`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
/**
|
||||||
|
* nix-js Runtime Entry Point
|
||||||
|
*
|
||||||
|
* All functionality is exported via the global `Nix` object
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { builtins, PRIMOP_METADATA } from "./builtins";
|
||||||
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
|
import {
|
||||||
|
assert,
|
||||||
|
call,
|
||||||
|
concatStringsWithContext,
|
||||||
|
hasAttr,
|
||||||
|
lookupWith,
|
||||||
|
mkPos,
|
||||||
|
resolvePath,
|
||||||
|
select,
|
||||||
|
selectWithDefault,
|
||||||
|
withContext,
|
||||||
|
} from "./helpers";
|
||||||
|
import { op } from "./operators";
|
||||||
|
import { HAS_CONTEXT } from "./string-context";
|
||||||
|
import {
|
||||||
|
createThunk,
|
||||||
|
DEBUG_THUNKS,
|
||||||
|
force,
|
||||||
|
forceDeep,
|
||||||
|
forceShallow,
|
||||||
|
IS_CYCLE,
|
||||||
|
IS_THUNK,
|
||||||
|
isThunk,
|
||||||
|
} from "./thunk";
|
||||||
|
import { forceBool } from "./type-assert";
|
||||||
|
import { ATTR_POSITIONS, IS_PATH, mkAttrs, mkAttrsWithPos, mkFunction, type NixValue } from "./types";
|
||||||
|
|
||||||
|
export type NixRuntime = typeof Nix;
|
||||||
|
|
||||||
|
const replBindings: Record<string, NixValue> = {};
|
||||||
|
|
||||||
|
export const Nix = {
|
||||||
|
createThunk,
|
||||||
|
force,
|
||||||
|
forceShallow,
|
||||||
|
forceDeep,
|
||||||
|
forceBool,
|
||||||
|
isThunk,
|
||||||
|
IS_THUNK,
|
||||||
|
IS_CYCLE,
|
||||||
|
HAS_CONTEXT,
|
||||||
|
IS_PATH,
|
||||||
|
DEBUG_THUNKS,
|
||||||
|
|
||||||
|
assert,
|
||||||
|
call,
|
||||||
|
hasAttr,
|
||||||
|
select,
|
||||||
|
selectWithDefault,
|
||||||
|
lookupWith,
|
||||||
|
resolvePath,
|
||||||
|
coerceToString,
|
||||||
|
concatStringsWithContext,
|
||||||
|
StringCoercionMode,
|
||||||
|
mkAttrs,
|
||||||
|
mkAttrsWithPos,
|
||||||
|
mkFunction,
|
||||||
|
mkPos,
|
||||||
|
ATTR_POSITIONS,
|
||||||
|
|
||||||
|
withContext,
|
||||||
|
|
||||||
|
op,
|
||||||
|
builtins,
|
||||||
|
PRIMOP_METADATA,
|
||||||
|
|
||||||
|
replBindings,
|
||||||
|
setReplBinding: (name: string, value: NixValue) => {
|
||||||
|
replBindings[name] = value;
|
||||||
|
},
|
||||||
|
getReplBinding: (name: string) => replBindings[name],
|
||||||
|
};
|
||||||
|
|
||||||
|
globalThis.Nix = Nix;
|
||||||
@@ -0,0 +1,274 @@
|
|||||||
|
import { coerceToString, StringCoercionMode } from "./builtins/conversion";
|
||||||
|
import { isNixString, typeOf } from "./builtins/type-check";
|
||||||
|
import { mkPath } from "./path";
|
||||||
|
import {
|
||||||
|
getStringContext,
|
||||||
|
getStringValue,
|
||||||
|
mergeContexts,
|
||||||
|
mkStringWithContext,
|
||||||
|
type NixStringContext,
|
||||||
|
} from "./string-context";
|
||||||
|
import { force } from "./thunk";
|
||||||
|
import { coerceNumeric, forceAttrs, forceBool, forceList, forceNumeric } from "./type-assert";
|
||||||
|
import type { NixAttrs, NixList, NixPath, NixString, NixValue } from "./types";
|
||||||
|
import { isNixPath } from "./types";
|
||||||
|
|
||||||
|
const canCoerceToString = (v: NixValue): boolean => {
|
||||||
|
const forced = force(v);
|
||||||
|
if (isNixString(forced)) return true;
|
||||||
|
if (typeof forced === "object" && forced !== null && !Array.isArray(forced)) {
|
||||||
|
if ("outPath" in forced || "__toString" in forced) return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const compareValues = (a: NixValue, b: NixValue): -1 | 0 | 1 => {
|
||||||
|
const av = force(a);
|
||||||
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Handle float/int mixed comparisons
|
||||||
|
if (typeof av === "number" && typeof bv === "bigint") {
|
||||||
|
const cmp = av - Number(bv);
|
||||||
|
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
if (typeof av === "bigint" && typeof bv === "number") {
|
||||||
|
const cmp = Number(av) - bv;
|
||||||
|
return cmp < 0 ? -1 : cmp > 0 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeA = typeOf(av);
|
||||||
|
const typeB = typeOf(bv);
|
||||||
|
|
||||||
|
// Types must match (except float/int which is handled above)
|
||||||
|
if (typeA !== typeB) {
|
||||||
|
throw new TypeError(`cannot compare ${typeOf(av)} with ${typeOf(bv)}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeA === "int" || typeA === "float") {
|
||||||
|
return (av as never) < (bv as never) ? -1 : av === bv ? 0 : 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeA === "string") {
|
||||||
|
const strA = getStringValue(av as NixString);
|
||||||
|
const strB = getStringValue(bv as NixString);
|
||||||
|
return strA < strB ? -1 : strA > strB ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeA === "path") {
|
||||||
|
const aPath = av as NixPath;
|
||||||
|
const bPath = bv as NixPath;
|
||||||
|
return aPath.value < bPath.value ? -1 : aPath.value > bPath.value ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeA === "list") {
|
||||||
|
const aList = av as NixList;
|
||||||
|
const bList = bv as NixList;
|
||||||
|
for (let i = 0; ; i++) {
|
||||||
|
// Equal if same length, else aList > bList
|
||||||
|
if (i === bList.length) {
|
||||||
|
return i === aList.length ? 0 : 1;
|
||||||
|
} else if (i === aList.length) {
|
||||||
|
return -1; // aList < bList
|
||||||
|
} else if (!op.eq(aList[i], bList[i])) {
|
||||||
|
return compareValues(aList[i], bList[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other types are incomparable
|
||||||
|
throw new TypeError(
|
||||||
|
`cannot compare ${typeOf(av)} with ${typeOf(bv)}; values of that type are incomparable`,
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const op = {
|
||||||
|
add: (a: NixValue, b: NixValue): bigint | number | NixString | NixPath => {
|
||||||
|
const av = force(a);
|
||||||
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Path concatenation: path + string = path
|
||||||
|
if (isNixPath(av)) {
|
||||||
|
if (isNixString(bv)) {
|
||||||
|
const strB = getStringValue(bv);
|
||||||
|
const ctxB = getStringContext(bv);
|
||||||
|
|
||||||
|
if (ctxB.size > 0) {
|
||||||
|
throw new TypeError("a string that refers to a store path cannot be appended to a path");
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkPath(av.value + strB);
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: handle corepkgs
|
||||||
|
// path + path: concatenate
|
||||||
|
if (isNixPath(bv)) {
|
||||||
|
return mkPath(av.value + bv.value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// String + path: result is string (path coerces to string)
|
||||||
|
if (isNixString(av) && isNixPath(bv)) {
|
||||||
|
const strA = getStringValue(av);
|
||||||
|
const ctxA = getStringContext(av);
|
||||||
|
const result = strA + bv.value;
|
||||||
|
|
||||||
|
if (ctxA.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, ctxA);
|
||||||
|
}
|
||||||
|
|
||||||
|
// String concatenation
|
||||||
|
if (isNixString(av) && isNixString(bv)) {
|
||||||
|
// Merge string context
|
||||||
|
const strA = getStringValue(av);
|
||||||
|
const strB = getStringValue(bv);
|
||||||
|
const ctxA = getStringContext(av);
|
||||||
|
const ctxB = getStringContext(bv);
|
||||||
|
|
||||||
|
if (ctxA.size === 0 && ctxB.size === 0) {
|
||||||
|
return strA + strB;
|
||||||
|
}
|
||||||
|
|
||||||
|
return mkStringWithContext(strA + strB, mergeContexts(ctxA, ctxB));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-coerce to string if possible
|
||||||
|
if (canCoerceToString(a) && canCoerceToString(b)) {
|
||||||
|
const context: NixStringContext = new Set();
|
||||||
|
const strA = coerceToString(a, StringCoercionMode.Interpolation, false, context);
|
||||||
|
const strB = coerceToString(b, StringCoercionMode.Interpolation, false, context);
|
||||||
|
const result = strA + strB;
|
||||||
|
if (context.size === 0) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
return mkStringWithContext(result, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Perform numeric addition otherwise
|
||||||
|
const [numA, numB] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
return (numA as never) + (numB as never);
|
||||||
|
},
|
||||||
|
|
||||||
|
sub: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
return (av as never) - (bv as never);
|
||||||
|
},
|
||||||
|
|
||||||
|
mul: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
return (av as never) * (bv as never);
|
||||||
|
},
|
||||||
|
|
||||||
|
div: (a: NixValue, b: NixValue): bigint | number => {
|
||||||
|
const [av, bv] = coerceNumeric(forceNumeric(a), forceNumeric(b));
|
||||||
|
|
||||||
|
if (bv === 0 || bv === 0n) {
|
||||||
|
throw new RangeError("Division by zero");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (av as never) / (bv as never);
|
||||||
|
},
|
||||||
|
|
||||||
|
eq: (a: NixValue, b: NixValue): boolean => {
|
||||||
|
const av = force(a);
|
||||||
|
const bv = force(b);
|
||||||
|
|
||||||
|
// Pointer equality
|
||||||
|
if (av === bv) return true;
|
||||||
|
|
||||||
|
// Special case: int == float type compatibility
|
||||||
|
if (typeof av === "bigint" && typeof bv === "number") {
|
||||||
|
return Number(av) === bv;
|
||||||
|
}
|
||||||
|
if (typeof av === "number" && typeof bv === "bigint") {
|
||||||
|
return av === Number(bv);
|
||||||
|
}
|
||||||
|
|
||||||
|
const typeA = typeOf(av);
|
||||||
|
const typeB = typeOf(bv);
|
||||||
|
|
||||||
|
// All other types must match exactly
|
||||||
|
if (typeA !== typeB) return false;
|
||||||
|
|
||||||
|
if (typeA === "int" || typeA === "float" || typeA === "bool" || typeA === "null") {
|
||||||
|
return av === bv;
|
||||||
|
}
|
||||||
|
|
||||||
|
// String comparison (handles both plain strings and StringWithContext)
|
||||||
|
if (typeA === "string") {
|
||||||
|
return getStringValue(av as NixString) === getStringValue(bv as NixString);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Path comparison
|
||||||
|
if (typeA === "path") {
|
||||||
|
return (av as NixPath).value === (bv as NixPath).value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeA === "list") {
|
||||||
|
const aList = av as NixList;
|
||||||
|
const bList = bv as NixList;
|
||||||
|
if (aList.length !== bList.length) return false;
|
||||||
|
for (let i = 0; i < aList.length; i++) {
|
||||||
|
if (!op.eq(aList[i], bList[i])) return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeA === "set") {
|
||||||
|
const attrsA = av as NixAttrs;
|
||||||
|
const attrsB = bv as NixAttrs;
|
||||||
|
|
||||||
|
// Derivation comparison: compare outPaths only
|
||||||
|
// Safe to force 'type' because it's always a string literal, never a computed value
|
||||||
|
if ("type" in attrsA && "type" in attrsB) {
|
||||||
|
const typeValA = force(attrsA.type);
|
||||||
|
const typeValB = force(attrsB.type);
|
||||||
|
if (typeValA === "derivation" && typeValB === "derivation") {
|
||||||
|
if ("outPath" in attrsA && "outPath" in attrsB) {
|
||||||
|
return op.eq(attrsA.outPath, attrsB.outPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise, compare attributes one by one
|
||||||
|
const keysA = Object.keys(attrsA).sort();
|
||||||
|
const keysB = Object.keys(attrsB).sort();
|
||||||
|
|
||||||
|
if (keysA.length !== keysB.length) return false;
|
||||||
|
|
||||||
|
for (let i = 0; i < keysA.length; i++) {
|
||||||
|
if (keysA[i] !== keysB[i]) return false;
|
||||||
|
if (!op.eq(attrsA[keysA[i]], attrsB[keysB[i]])) return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Other types are incomparable
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
neq: (a: NixValue, b: NixValue): boolean => {
|
||||||
|
return !op.eq(a, b);
|
||||||
|
},
|
||||||
|
lt: (a: NixValue, b: NixValue): boolean => {
|
||||||
|
return compareValues(a, b) < 0;
|
||||||
|
},
|
||||||
|
lte: (a: NixValue, b: NixValue): boolean => {
|
||||||
|
return compareValues(a, b) <= 0;
|
||||||
|
},
|
||||||
|
gt: (a: NixValue, b: NixValue): boolean => {
|
||||||
|
return compareValues(a, b) > 0;
|
||||||
|
},
|
||||||
|
gte: (a: NixValue, b: NixValue): boolean => {
|
||||||
|
return compareValues(a, b) >= 0;
|
||||||
|
},
|
||||||
|
|
||||||
|
bnot: (a: NixValue): boolean => !forceBool(a),
|
||||||
|
|
||||||
|
concat: (a: NixValue, b: NixValue): NixList => {
|
||||||
|
return forceList(a).concat(forceList(b));
|
||||||
|
},
|
||||||
|
|
||||||
|
update: (a: NixValue, b: NixValue): NixAttrs => ({ ...forceAttrs(a), ...forceAttrs(b) }),
|
||||||
|
};
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import { IS_PATH, type NixPath } from "./types";
|
||||||
|
|
||||||
|
const canonicalizePath = (path: string): string => {
|
||||||
|
const parts: string[] = [];
|
||||||
|
let i = 0;
|
||||||
|
const len = path.length;
|
||||||
|
|
||||||
|
while (i < len) {
|
||||||
|
while (i < len && path[i] === "/") i++;
|
||||||
|
if (i >= len) break;
|
||||||
|
|
||||||
|
let j = i;
|
||||||
|
while (j < len && path[j] !== "/") j++;
|
||||||
|
const component = path.slice(i, j);
|
||||||
|
i = j;
|
||||||
|
|
||||||
|
if (component === ".") {
|
||||||
|
} else if (component === "..") {
|
||||||
|
if (parts.length > 0) {
|
||||||
|
parts.pop();
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
parts.push(component);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (parts.length === 0) {
|
||||||
|
return "/";
|
||||||
|
}
|
||||||
|
return `/${parts.join("/")}`;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mkPath = (value: string): NixPath => {
|
||||||
|
return { [IS_PATH]: true, value: canonicalizePath(value) };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getPathValue = (p: NixPath): string => {
|
||||||
|
return p.value;
|
||||||
|
};
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
import { getPrimopMetadata, isPrimop } from "./builtins/index";
|
||||||
|
import { isStringWithContext } from "./string-context";
|
||||||
|
import { IS_CYCLE, isThunk } from "./thunk";
|
||||||
|
import { isNixPath, type NixValue } from "./types";
|
||||||
|
|
||||||
|
export const printValue = (value: NixValue, seen: WeakSet<object> = new WeakSet()): string => {
|
||||||
|
if (isThunk(value)) {
|
||||||
|
return "«thunk»";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value === null) {
|
||||||
|
return "null";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "boolean") {
|
||||||
|
return value ? "true" : "false";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "bigint") {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "number") {
|
||||||
|
return value.toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "string") {
|
||||||
|
return printString(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "function") {
|
||||||
|
if (isPrimop(value)) {
|
||||||
|
const meta = getPrimopMetadata(value);
|
||||||
|
if (meta && meta.applied > 0) {
|
||||||
|
return "<PRIMOP-APP>";
|
||||||
|
}
|
||||||
|
return "<PRIMOP>";
|
||||||
|
}
|
||||||
|
return "<LAMBDA>";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof value === "object") {
|
||||||
|
if (IS_CYCLE in value && (value as Record<symbol, unknown>)[IS_CYCLE] === true) {
|
||||||
|
return "«repeated»";
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(value)) {
|
||||||
|
return "«repeated»";
|
||||||
|
}
|
||||||
|
seen.add(value);
|
||||||
|
|
||||||
|
if (isNixPath(value)) {
|
||||||
|
return value.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isStringWithContext(value)) {
|
||||||
|
return printString(value.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(value)) {
|
||||||
|
const items = value.map((v) => printValue(v, seen)).join(" ");
|
||||||
|
return `[ ${items} ]`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const entries = Object.entries(value)
|
||||||
|
.map(([k, v]) => `${printSymbol(k)} = ${printValue(v, seen)};`)
|
||||||
|
.join(" ");
|
||||||
|
return `{${entries ? ` ${entries} ` : " "}}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return "<unknown>";
|
||||||
|
};
|
||||||
|
|
||||||
|
const printString = (s: string): string => {
|
||||||
|
let result = '"';
|
||||||
|
for (const c of s) {
|
||||||
|
switch (c) {
|
||||||
|
case "\\":
|
||||||
|
result += "\\\\";
|
||||||
|
break;
|
||||||
|
case '"':
|
||||||
|
result += '\\"';
|
||||||
|
break;
|
||||||
|
case "\n":
|
||||||
|
result += "\\n";
|
||||||
|
break;
|
||||||
|
case "\r":
|
||||||
|
result += "\\r";
|
||||||
|
break;
|
||||||
|
case "\t":
|
||||||
|
result += "\\t";
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
result += c;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return `${result}"`;
|
||||||
|
};
|
||||||
|
|
||||||
|
const SYMBOL_REGEX = /^[a-zA-Z_][a-zA-Z0-9_'-]*$/;
|
||||||
|
|
||||||
|
const printSymbol = (s: string): string => {
|
||||||
|
if (SYMBOL_REGEX.test(s)) {
|
||||||
|
return s;
|
||||||
|
}
|
||||||
|
return printString(s);
|
||||||
|
};
|
||||||
@@ -0,0 +1,148 @@
|
|||||||
|
import type { NixStrictValue } from "./types";
|
||||||
|
|
||||||
|
export const HAS_CONTEXT = Symbol("HAS_CONTEXT");
|
||||||
|
|
||||||
|
export interface StringContextOpaque {
|
||||||
|
type: "opaque";
|
||||||
|
path: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringContextDrvDeep {
|
||||||
|
type: "drvDeep";
|
||||||
|
drvPath: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface StringContextBuilt {
|
||||||
|
type: "built";
|
||||||
|
drvPath: string;
|
||||||
|
output: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type StringContextElem = StringContextOpaque | StringContextDrvDeep | StringContextBuilt;
|
||||||
|
|
||||||
|
export type NixStringContext = Set<string>;
|
||||||
|
|
||||||
|
export interface StringWithContext {
|
||||||
|
readonly [HAS_CONTEXT]: true;
|
||||||
|
value: string;
|
||||||
|
context: NixStringContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isStringWithContext = (v: NixStrictValue): v is StringWithContext => {
|
||||||
|
return typeof v === "object" && v !== null && HAS_CONTEXT in v;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mkStringWithContext = (value: string, context: NixStringContext): StringWithContext => {
|
||||||
|
return { [HAS_CONTEXT]: true, value, context };
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mkPlainString = (value: string): string => value;
|
||||||
|
|
||||||
|
export const getStringValue = (s: string | StringWithContext): string => {
|
||||||
|
if (isStringWithContext(s)) {
|
||||||
|
return s.value;
|
||||||
|
}
|
||||||
|
return s;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getStringContext = (s: string | StringWithContext): NixStringContext => {
|
||||||
|
if (isStringWithContext(s)) {
|
||||||
|
return s.context;
|
||||||
|
}
|
||||||
|
return new Set();
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mergeContexts = (...contexts: NixStringContext[]): NixStringContext => {
|
||||||
|
const result = new Set<string>();
|
||||||
|
for (const ctx of contexts) {
|
||||||
|
for (const elem of ctx) {
|
||||||
|
result.add(elem);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const encodeContextElem = (elem: StringContextElem): string => {
|
||||||
|
switch (elem.type) {
|
||||||
|
case "opaque":
|
||||||
|
return elem.path;
|
||||||
|
case "drvDeep":
|
||||||
|
return `=${elem.drvPath}`;
|
||||||
|
case "built":
|
||||||
|
return `!${elem.output}!${elem.drvPath}`;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const decodeContextElem = (encoded: string): StringContextElem => {
|
||||||
|
if (encoded.startsWith("=")) {
|
||||||
|
return { type: "drvDeep", drvPath: encoded.slice(1) };
|
||||||
|
} else if (encoded.startsWith("!")) {
|
||||||
|
const secondBang = encoded.indexOf("!", 1);
|
||||||
|
if (secondBang === -1) {
|
||||||
|
throw new Error(`Invalid context element: ${encoded}`);
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
type: "built",
|
||||||
|
output: encoded.slice(1, secondBang),
|
||||||
|
drvPath: encoded.slice(secondBang + 1),
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
return { type: "opaque", path: encoded };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addContextElem = (context: NixStringContext, elem: StringContextElem): void => {
|
||||||
|
context.add(encodeContextElem(elem));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addOpaqueContext = (context: NixStringContext, path: string): void => {
|
||||||
|
context.add(path);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addDrvDeepContext = (context: NixStringContext, drvPath: string): void => {
|
||||||
|
context.add(`=${drvPath}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const addBuiltContext = (context: NixStringContext, drvPath: string, output: string): void => {
|
||||||
|
context.add(`!${output}!${drvPath}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface ParsedContextInfo {
|
||||||
|
path: boolean;
|
||||||
|
allOutputs: boolean;
|
||||||
|
outputs: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const parseContextToInfoMap = (context: NixStringContext): Map<string, ParsedContextInfo> => {
|
||||||
|
const result = new Map<string, ParsedContextInfo>();
|
||||||
|
|
||||||
|
const getOrCreate = (path: string): ParsedContextInfo => {
|
||||||
|
let info = result.get(path);
|
||||||
|
if (!info) {
|
||||||
|
info = { path: false, allOutputs: false, outputs: [] };
|
||||||
|
result.set(path, info);
|
||||||
|
}
|
||||||
|
return info;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const encoded of context) {
|
||||||
|
const elem = decodeContextElem(encoded);
|
||||||
|
switch (elem.type) {
|
||||||
|
case "opaque":
|
||||||
|
getOrCreate(elem.path).path = true;
|
||||||
|
break;
|
||||||
|
case "drvDeep":
|
||||||
|
getOrCreate(elem.drvPath).allOutputs = true;
|
||||||
|
break;
|
||||||
|
case "built":
|
||||||
|
getOrCreate(elem.drvPath).outputs.push(elem.output);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const info of result.values()) {
|
||||||
|
info.outputs.sort();
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
};
|
||||||
@@ -0,0 +1,192 @@
|
|||||||
|
import { isAttrs, isList } from "./builtins/type-check";
|
||||||
|
import { HAS_CONTEXT } from "./string-context";
|
||||||
|
import type { NixStrictValue, NixThunkInterface, NixValue } from "./types";
|
||||||
|
import { IS_PATH } from "./types";
|
||||||
|
|
||||||
|
export const IS_THUNK = Symbol("is_thunk");
|
||||||
|
|
||||||
|
const forceStack: NixThunk[] = [];
|
||||||
|
const MAX_FORCE_DEPTH = 1000;
|
||||||
|
|
||||||
|
export const DEBUG_THUNKS = { enabled: true };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* NixThunk class - represents a lazy, unevaluated expression
|
||||||
|
*
|
||||||
|
* A thunk wraps a function that produces a value when called.
|
||||||
|
* Once evaluated, the result is cached to avoid recomputation.
|
||||||
|
*
|
||||||
|
* Thunk states:
|
||||||
|
* - Unevaluated: func is defined, result is undefined
|
||||||
|
* - Evaluating (blackhole): func is undefined, result is undefined
|
||||||
|
* - Evaluated: func is undefined, result is defined
|
||||||
|
*/
|
||||||
|
export class NixThunk implements NixThunkInterface {
|
||||||
|
[key: symbol]: unknown;
|
||||||
|
readonly [IS_THUNK] = true as const;
|
||||||
|
func: (() => NixValue) | undefined;
|
||||||
|
result: NixStrictValue | undefined;
|
||||||
|
readonly label: string | undefined;
|
||||||
|
|
||||||
|
constructor(func: () => NixValue, label?: string) {
|
||||||
|
this.func = func;
|
||||||
|
this.result = undefined;
|
||||||
|
this.label = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
toString(): string {
|
||||||
|
if (this.label) {
|
||||||
|
return `«thunk ${this.label}»`;
|
||||||
|
}
|
||||||
|
return `«thunk»`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isThunk = (value: NixValue): value is NixThunkInterface => {
|
||||||
|
return value !== null && typeof value === "object" && IS_THUNK in value && value[IS_THUNK] === true;
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Force evaluation of a value
|
||||||
|
* If the value is a thunk, evaluate it and cache the result
|
||||||
|
* If already evaluated or not a thunk, return as-is
|
||||||
|
*
|
||||||
|
* Uses "blackhole" detection to catch infinite recursion:
|
||||||
|
* - Before evaluating, set func to undefined (entering blackhole state)
|
||||||
|
* - If we encounter a thunk with func=undefined and result=undefined, it's a blackhole
|
||||||
|
*
|
||||||
|
* @param value - Value to force (may be a thunk)
|
||||||
|
* @returns The forced/evaluated value
|
||||||
|
* @throws Error if infinite recursion is detected
|
||||||
|
*/
|
||||||
|
export const force = (value: NixValue): NixStrictValue => {
|
||||||
|
if (!isThunk(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.func === undefined) {
|
||||||
|
if (value.result === undefined) {
|
||||||
|
const thunk = value as NixThunk;
|
||||||
|
let msg = `infinite recursion encountered at ${thunk}\n`;
|
||||||
|
if (DEBUG_THUNKS.enabled) {
|
||||||
|
msg += "Force chain (most recent first):\n";
|
||||||
|
for (let i = forceStack.length - 1; i >= 0; i--) {
|
||||||
|
const t = forceStack[i];
|
||||||
|
msg += ` ${i + 1}. ${t}`;
|
||||||
|
msg += "\n";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
return value.result;
|
||||||
|
}
|
||||||
|
|
||||||
|
const thunk = value as NixThunk;
|
||||||
|
const func = thunk.func as () => NixValue;
|
||||||
|
thunk.func = undefined;
|
||||||
|
|
||||||
|
if (DEBUG_THUNKS.enabled) {
|
||||||
|
forceStack.push(thunk);
|
||||||
|
if (forceStack.length > MAX_FORCE_DEPTH) {
|
||||||
|
let msg = `force depth exceeded ${MAX_FORCE_DEPTH} at ${thunk}\n`;
|
||||||
|
msg += "Force chain (most recent first):\n";
|
||||||
|
for (let i = forceStack.length - 1; i >= Math.max(0, forceStack.length - 20); i--) {
|
||||||
|
const t = forceStack[i];
|
||||||
|
msg += ` ${i + 1}. ${t}`;
|
||||||
|
msg += "\n";
|
||||||
|
}
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const result = force(func());
|
||||||
|
thunk.result = result;
|
||||||
|
return result;
|
||||||
|
} catch (e) {
|
||||||
|
thunk.func = func;
|
||||||
|
throw e;
|
||||||
|
} finally {
|
||||||
|
if (DEBUG_THUNKS.enabled) {
|
||||||
|
forceStack.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const createThunk = (func: () => NixValue, label?: string): NixThunkInterface => {
|
||||||
|
return new NixThunk(func, label);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const IS_CYCLE = Symbol("is_cycle");
|
||||||
|
export const CYCLE_MARKER = { [IS_CYCLE]: true };
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deeply force a value, handling cycles by returning a special marker.
|
||||||
|
* Uses WeakSet to track seen objects and avoid infinite recursion.
|
||||||
|
* Returns a fully forced value where thunks are replaced with their results.
|
||||||
|
* Cyclic references are replaced with CYCLE_MARKER, preserving the container type.
|
||||||
|
*/
|
||||||
|
export const forceDeep = (value: NixValue, seen: WeakSet<object> = new WeakSet()): NixStrictValue => {
|
||||||
|
const forced = force(value);
|
||||||
|
|
||||||
|
if (forced === null || typeof forced !== "object") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (seen.has(forced)) {
|
||||||
|
if (Array.isArray(forced)) {
|
||||||
|
return [CYCLE_MARKER];
|
||||||
|
}
|
||||||
|
return CYCLE_MARKER;
|
||||||
|
}
|
||||||
|
if (isAttrs(forced) || isList(forced)) {
|
||||||
|
seen.add(forced);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HAS_CONTEXT in forced || IS_PATH in forced) {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(forced)) {
|
||||||
|
return forced.map((item) => forceDeep(item, seen));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof forced === "object") {
|
||||||
|
const result: Record<string, NixValue> = {};
|
||||||
|
for (const [key, val] of Object.entries(forced)) {
|
||||||
|
result[key] = forceDeep(val, seen);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return forced;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceShallow = (value: NixValue): NixStrictValue => {
|
||||||
|
const forced = force(value);
|
||||||
|
|
||||||
|
if (forced === null || typeof forced !== "object") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Array.isArray(forced)) {
|
||||||
|
return forced.map((item) => {
|
||||||
|
const forcedItem = force(item);
|
||||||
|
if (typeof forcedItem === "object" && forcedItem === forced) {
|
||||||
|
return CYCLE_MARKER;
|
||||||
|
} else {
|
||||||
|
return forcedItem;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isAttrs(forced)) {
|
||||||
|
const result: Record<string, NixValue> = {};
|
||||||
|
for (const [key, val] of Object.entries(forced)) {
|
||||||
|
const forcedVal = force(val);
|
||||||
|
result[key] = forcedVal === forced ? CYCLE_MARKER : forcedVal;
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
return forced;
|
||||||
|
};
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
import { isAttrs, isFunction, typeOf } from "./builtins/type-check";
|
||||||
|
import { force } from "./thunk";
|
||||||
|
import type {
|
||||||
|
NixAttrs,
|
||||||
|
NixFloat,
|
||||||
|
NixFunction,
|
||||||
|
NixInt,
|
||||||
|
NixList,
|
||||||
|
NixNumber,
|
||||||
|
NixPath,
|
||||||
|
NixString,
|
||||||
|
NixValue,
|
||||||
|
} from "./types";
|
||||||
|
import { isNixPath, isStringWithContext } from "./types";
|
||||||
|
|
||||||
|
export const forceList = (value: NixValue): NixList => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (!Array.isArray(forced)) {
|
||||||
|
throw new TypeError(`Expected list, got ${typeOf(forced)}`);
|
||||||
|
}
|
||||||
|
return forced;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceFunction = (value: NixValue): NixFunction => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (isFunction(forced)) {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
if (typeof forced === "object" && !Array.isArray(forced) && forced !== null && "__functor" in forced) {
|
||||||
|
const functorSet = forced as NixAttrs;
|
||||||
|
const functor = forceFunction(functorSet.__functor);
|
||||||
|
return (arg: NixValue) => forceFunction(functor(functorSet))(arg);
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected function, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceAttrs = (value: NixValue): NixAttrs => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (!isAttrs(forced)) {
|
||||||
|
throw new TypeError(`Expected attribute set, got ${typeOf(forced)}`);
|
||||||
|
}
|
||||||
|
return forced;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceStringValue = (value: NixValue): string => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced === "string") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
if (isStringWithContext(forced)) {
|
||||||
|
return forced.value;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceString = (value: NixValue): NixString => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced === "string") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
if (isStringWithContext(forced)) {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceStringNoCtx = (value: NixValue): string => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced === "string") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
if (isStringWithContext(forced)) {
|
||||||
|
throw new TypeError(`the string '${forced.value}' is not allowed to refer to a store path`);
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected string, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceBool = (value: NixValue): boolean => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced !== "boolean") {
|
||||||
|
throw new TypeError(`Expected boolean, got ${typeOf(forced)}`);
|
||||||
|
}
|
||||||
|
return forced;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceInt = (value: NixValue): NixInt => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced === "bigint") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected int, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceFloat = (value: NixValue): NixFloat => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced === "number") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected float, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceNumeric = (value: NixValue): NixNumber => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (typeof forced === "bigint" || typeof forced === "number") {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected numeric type, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const coerceNumeric = (a: NixNumber, b: NixNumber): [NixFloat, NixFloat] | [NixInt, NixInt] => {
|
||||||
|
const aIsInt = typeof a === "bigint";
|
||||||
|
const bIsInt = typeof b === "bigint";
|
||||||
|
|
||||||
|
if (!aIsInt || !bIsInt) {
|
||||||
|
return [Number(a), Number(b)];
|
||||||
|
}
|
||||||
|
|
||||||
|
return [a, b];
|
||||||
|
};
|
||||||
|
|
||||||
|
export const forceNixPath = (value: NixValue): NixPath => {
|
||||||
|
const forced = force(value);
|
||||||
|
if (isNixPath(forced)) {
|
||||||
|
return forced;
|
||||||
|
}
|
||||||
|
throw new TypeError(`Expected path, got ${typeOf(forced)}`);
|
||||||
|
};
|
||||||
@@ -0,0 +1,126 @@
|
|||||||
|
import { HAS_CONTEXT, isStringWithContext, type StringWithContext } from "./string-context";
|
||||||
|
import { force, IS_THUNK } from "./thunk";
|
||||||
|
import { forceAttrs, forceStringNoCtx } from "./type-assert";
|
||||||
|
export { HAS_CONTEXT, isStringWithContext };
|
||||||
|
export type { StringWithContext };
|
||||||
|
|
||||||
|
export const IS_PATH = Symbol("IS_PATH");
|
||||||
|
|
||||||
|
export interface NixPath {
|
||||||
|
readonly [IS_PATH]: true;
|
||||||
|
value: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isNixPath = (v: NixStrictValue): v is NixPath => {
|
||||||
|
return typeof v === "object" && v !== null && IS_PATH in v;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type NixInt = bigint;
|
||||||
|
export type NixFloat = number;
|
||||||
|
export type NixNumber = NixInt | NixFloat;
|
||||||
|
export type NixBool = boolean;
|
||||||
|
export type NixString = string | StringWithContext;
|
||||||
|
export type NixNull = null;
|
||||||
|
|
||||||
|
export type NixList = NixValue[];
|
||||||
|
// FIXME: reject contextful string
|
||||||
|
export type NixAttrs = { [key: string]: NixValue };
|
||||||
|
export type NixFunction = ((arg: NixValue) => NixValue) & { args?: NixArgs };
|
||||||
|
export class NixArgs {
|
||||||
|
required: string[];
|
||||||
|
optional: string[];
|
||||||
|
allowed: Set<string>;
|
||||||
|
ellipsis: boolean;
|
||||||
|
positions: Record<string, string>;
|
||||||
|
constructor(required: string[], optional: string[], positions: Record<string, string>, ellipsis: boolean) {
|
||||||
|
this.required = required;
|
||||||
|
this.optional = optional;
|
||||||
|
this.positions = positions;
|
||||||
|
this.ellipsis = ellipsis;
|
||||||
|
this.allowed = new Set(required.concat(optional));
|
||||||
|
}
|
||||||
|
check(arg: NixValue) {
|
||||||
|
const attrs = forceAttrs(arg);
|
||||||
|
|
||||||
|
for (const key of this.required) {
|
||||||
|
if (!Object.hasOwn(attrs, key)) {
|
||||||
|
throw new Error(`Function called without required argument '${key}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.ellipsis) {
|
||||||
|
for (const key in attrs) {
|
||||||
|
if (!this.allowed.has(key)) {
|
||||||
|
throw new Error(`Function called with unexpected argument '${key}'`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
export const mkFunction = (
|
||||||
|
f: (arg: NixValue) => NixValue,
|
||||||
|
required: string[],
|
||||||
|
optional: string[],
|
||||||
|
positions: Record<string, string>,
|
||||||
|
ellipsis: boolean,
|
||||||
|
): NixFunction => {
|
||||||
|
const func: NixFunction = f;
|
||||||
|
func.args = new NixArgs(required, optional, positions, ellipsis);
|
||||||
|
return func;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const mkAttrs = (attrs: NixAttrs, keys: NixValue[], values: NixValue[]): NixAttrs => {
|
||||||
|
const len = keys.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const key = force(keys[i]);
|
||||||
|
if (key === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const str = forceStringNoCtx(key);
|
||||||
|
attrs[str] = values[i];
|
||||||
|
}
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ATTR_POSITIONS = Symbol("attrPositions");
|
||||||
|
|
||||||
|
export const mkAttrsWithPos = (
|
||||||
|
attrs: NixAttrs,
|
||||||
|
positions: Record<string, string>,
|
||||||
|
dyns?: { dynKeys: NixValue[]; dynVals: NixValue[]; dynSpans: string[] },
|
||||||
|
): NixAttrs => {
|
||||||
|
if (dyns) {
|
||||||
|
const len = dyns.dynKeys.length;
|
||||||
|
for (let i = 0; i < len; i++) {
|
||||||
|
const key = force(dyns.dynKeys[i]);
|
||||||
|
if (key === null) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const str = forceStringNoCtx(key);
|
||||||
|
attrs[str] = dyns.dynVals[i];
|
||||||
|
positions[str] = dyns.dynSpans[i];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(positions).length > 0) {
|
||||||
|
Object.defineProperty(attrs, ATTR_POSITIONS, {
|
||||||
|
value: positions,
|
||||||
|
enumerable: false,
|
||||||
|
writable: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return attrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
export interface NixThunkInterface {
|
||||||
|
readonly [IS_THUNK]: true;
|
||||||
|
func: (() => NixValue) | undefined;
|
||||||
|
result: NixStrictValue | undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type NixPrimitive = NixNull | NixBool | NixInt | NixFloat | NixString;
|
||||||
|
export type NixValue = NixPrimitive | NixPath | NixList | NixAttrs | NixFunction | NixThunkInterface;
|
||||||
|
export type NixStrictValue = Exclude<NixValue, NixThunkInterface>;
|
||||||
|
|
||||||
|
export class CatchableError extends Error {}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user