Compare commits
81 Commits
86953dd9d3
...
new
| Author | SHA1 | Date | |
|---|---|---|---|
|
198d847151
|
|||
|
40d00a6c47
|
|||
|
0c9a391618
|
|||
|
7a7229d70e
|
|||
|
e4004ccb6d
|
|||
|
843ae6cfb4
|
|||
|
c24d6a8bb3
|
|||
|
d7351e907b
|
|||
|
550223a1d7
|
|||
|
53dbee3514
|
|||
| e1517c338e | |||
| 45096f5254 | |||
| b57fea3104 | |||
| 4380fa85c4 | |||
| 99045aa76c | |||
| 7eb3acf26f | |||
| b424f60f9f | |||
| 42031edac1 | |||
| 04dcadfd61 | |||
| c3c39bda0c | |||
| 782092b91e | |||
| ae5febd5dd | |||
| 3cc7c7be75 | |||
| f49634ccc0 | |||
| 4a885c18b8 | |||
| 37e395c0e3 | |||
| 16a8480d29 | |||
| f0a0593d4c | |||
| ce64a82da3 | |||
| 5c48e5cfdd | |||
| 7836f8c869 | |||
| e357678d70 | |||
| 2f2c690023 | |||
| cf4dd6c379 | |||
| 31c7a62311 | |||
| ad5d047c01 | |||
| 795742e3d8 | |||
| 60cd61d771 | |||
| d95a6e509c | |||
| 48a43bed55 | |||
| 6d8f1e79e6 | |||
| df4edaf5bb | |||
| 3aee3c67b9 | |||
|
a8f1c81b60
|
|||
|
249eaf3c11
|
|||
|
a79e20c417
|
|||
|
bd9eb638af
|
|||
|
d09b84676c
|
|||
|
1346ae5405
|
|||
|
6b46e466c2
|
|||
|
f154010120
|
|||
|
f5364ded1e
|
|||
|
26e7b74585
|
|||
|
e8a28a6d2f
|
|||
|
216930027d
|
|||
|
4d6fd6d614
|
|||
|
b7f4ece472
|
|||
|
a68681b4f5
|
|||
|
ba3e2ae3de
|
|||
|
c5aee21514
|
|||
|
8f01ce2eb4
|
|||
|
a08f0e78a3
|
|||
|
547f8f3828
|
|||
|
aa368cb12e
|
|||
|
0360bbe4aa
|
|||
|
db64763d77
|
|||
|
1aba28d97b
|
|||
|
6838f9a0cf
|
|||
|
cb539c52c3
|
|||
|
b8f8b5764d
|
|||
|
1cfa8223c6
|
|||
|
13874ca6ca
|
|||
|
5703329850
|
|||
|
f0812c9063
|
|||
|
97854afafa
|
|||
|
9545b0fcae
|
|||
|
aee46b0b49
|
|||
|
1f835e7b06
|
|||
|
9ee2dd5c08
|
|||
|
084968c08a
|
|||
|
058ef44259
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,3 +5,8 @@ target/
|
|||||||
# Profiling
|
# Profiling
|
||||||
flamegraph*.svg
|
flamegraph*.svg
|
||||||
perf.data*
|
perf.data*
|
||||||
|
profile.json.gz
|
||||||
|
prof.json
|
||||||
|
*.cpuprofile
|
||||||
|
*.cpuprofile.gz
|
||||||
|
*v8.log*
|
||||||
|
|||||||
12
.lazy.lua
12
.lazy.lua
@@ -3,13 +3,19 @@ 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 = {
|
|
||||||
"daemon"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
2273
Cargo.lock
generated
2273
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,10 @@
|
|||||||
[workspace]
|
[workspace]
|
||||||
resolver = "3"
|
resolver = "3"
|
||||||
members = [
|
members = [
|
||||||
"nix-js",
|
"fix",
|
||||||
"nix-js-macros"
|
"boxing",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[profile.profiling]
|
||||||
|
inherits = "release"
|
||||||
|
debug = true
|
||||||
|
|||||||
24
Justfile
24
Justfile
@@ -1,15 +1,31 @@
|
|||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@repl:
|
@repl:
|
||||||
RUST_LOG=none,nix_js=debug cargo run --bin repl
|
cargo run -- repl
|
||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@eval expr:
|
@eval expr:
|
||||||
RUST_LOG=none,nix_js=debug cargo run --bin eval -- '{{expr}}'
|
cargo run -- eval --expr '{{expr}}'
|
||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@replr:
|
@replr:
|
||||||
RUST_LOG=info cargo run --bin repl --release
|
cargo run --release -- repl
|
||||||
|
|
||||||
[no-exit-message]
|
[no-exit-message]
|
||||||
@evalr expr:
|
@evalr expr:
|
||||||
RUST_LOG=silent cargo run --bin eval --release -- '{{expr}}'
|
cargo run --release -- eval --expr '{{expr}}'
|
||||||
|
|
||||||
|
[no-exit-message]
|
||||||
|
@repli:
|
||||||
|
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 repl
|
||||||
|
|
||||||
|
[no-exit-message]
|
||||||
|
@evali expr:
|
||||||
|
cargo run --release --features inspector -- --inspect-brk 127.0.0.1:9229 eval --expr '{{expr}}'
|
||||||
|
|
||||||
|
[no-exit-message]
|
||||||
|
@replp:
|
||||||
|
cargo run --release --features prof -- repl
|
||||||
|
|
||||||
|
[no-exit-message]
|
||||||
|
@evalp expr:
|
||||||
|
cargo run --release --features prof -- eval --expr '{{expr}}'
|
||||||
|
|||||||
31
biome.json
31
biome.json
@@ -1,5 +1,5 @@
|
|||||||
{
|
{
|
||||||
"$schema": "https://biomejs.dev/schemas/2.3.9/schema.json",
|
"$schema": "https://biomejs.dev/schemas/2.3.14/schema.json",
|
||||||
"vcs": {
|
"vcs": {
|
||||||
"enabled": true,
|
"enabled": true,
|
||||||
"clientKind": "git",
|
"clientKind": "git",
|
||||||
@@ -20,10 +20,37 @@
|
|||||||
"linter": {
|
"linter": {
|
||||||
"rules": {
|
"rules": {
|
||||||
"style": {
|
"style": {
|
||||||
"useNamingConvention": "warn"
|
"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": {
|
"javascript": {
|
||||||
"formatter": {
|
"formatter": {
|
||||||
"arrowParentheses": "always",
|
"arrowParentheses": "always",
|
||||||
|
|||||||
8
boxing/Cargo.toml
Normal file
8
boxing/Cargo.toml
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
[package]
|
||||||
|
name = "boxing"
|
||||||
|
version = "0.1.3"
|
||||||
|
edition = "2021"
|
||||||
|
description = "NaN-boxing primitives (local fork with bool fix)"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
sptr = "0.3"
|
||||||
2
boxing/src/lib.rs
Normal file
2
boxing/src/lib.rs
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
pub mod nan;
|
||||||
|
mod utils;
|
||||||
7
boxing/src/nan.rs
Normal file
7
boxing/src/nan.rs
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
pub mod raw;
|
||||||
|
|
||||||
|
pub use raw::RawBox;
|
||||||
|
|
||||||
|
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
|
||||||
|
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
|
||||||
|
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
|
||||||
471
boxing/src/nan/raw.rs
Normal file
471
boxing/src/nan/raw.rs
Normal file
@@ -0,0 +1,471 @@
|
|||||||
|
use super::{NEG_QUIET_NAN, QUIET_NAN, SIGN_MASK};
|
||||||
|
use crate::utils::ArrayExt;
|
||||||
|
use sptr::Strict;
|
||||||
|
use std::fmt;
|
||||||
|
use std::mem::ManuallyDrop;
|
||||||
|
use std::num::NonZeroU8;
|
||||||
|
|
||||||
|
pub trait RawStore: Sized {
|
||||||
|
fn to_val(self, value: &mut Value);
|
||||||
|
fn from_val(value: &Value) -> Self;
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawStore for [u8; 6] {
|
||||||
|
#[inline]
|
||||||
|
fn to_val(self, value: &mut Value) {
|
||||||
|
value.set_data(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_val(value: &Value) -> Self {
|
||||||
|
*value.data()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawStore for bool {
|
||||||
|
#[inline]
|
||||||
|
fn to_val(self, value: &mut Value) {
|
||||||
|
value.set_data([u8::from(self)].truncate_to());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_val(value: &Value) -> Self {
|
||||||
|
value.data()[0] == 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! int_store {
|
||||||
|
($ty:ty) => {
|
||||||
|
impl RawStore for $ty {
|
||||||
|
#[inline]
|
||||||
|
fn to_val(self, value: &mut Value) {
|
||||||
|
let bytes = self.to_ne_bytes();
|
||||||
|
value.set_data(bytes.truncate_to());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn from_val(value: &Value) -> Self {
|
||||||
|
<$ty>::from_ne_bytes(value.data().truncate_to())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
int_store!(u8);
|
||||||
|
int_store!(u16);
|
||||||
|
int_store!(u32);
|
||||||
|
|
||||||
|
int_store!(i8);
|
||||||
|
int_store!(i16);
|
||||||
|
int_store!(i32);
|
||||||
|
|
||||||
|
fn store_ptr<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 {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(neg: bool, val: NonZeroU8) -> RawTag {
|
||||||
|
unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn new_checked(neg: bool, val: u8) -> Option<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 unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
|
||||||
|
RawTag(match (neg, val) {
|
||||||
|
(false, 1) => TagVal::_P1,
|
||||||
|
(false, 2) => TagVal::_P2,
|
||||||
|
(false, 3) => TagVal::_P3,
|
||||||
|
(false, 4) => TagVal::_P4,
|
||||||
|
(false, 5) => TagVal::_P5,
|
||||||
|
(false, 6) => TagVal::_P6,
|
||||||
|
(false, 7) => TagVal::_P7,
|
||||||
|
|
||||||
|
(true, 1) => TagVal::_N1,
|
||||||
|
(true, 2) => TagVal::_N2,
|
||||||
|
(true, 3) => TagVal::_N3,
|
||||||
|
(true, 4) => TagVal::_N4,
|
||||||
|
(true, 5) => TagVal::_N5,
|
||||||
|
(true, 6) => TagVal::_N6,
|
||||||
|
(true, 7) => TagVal::_N7,
|
||||||
|
|
||||||
|
_ => unsafe { core::hint::unreachable_unchecked() },
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_neg(self) -> bool {
|
||||||
|
matches!(self.0, |TagVal::_N1| TagVal::_N2
|
||||||
|
| TagVal::_N3
|
||||||
|
| TagVal::_N4
|
||||||
|
| TagVal::_N5
|
||||||
|
| TagVal::_N6
|
||||||
|
| TagVal::_N7)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn val(self) -> NonZeroU8 {
|
||||||
|
match self.0 {
|
||||||
|
TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN,
|
||||||
|
TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1),
|
||||||
|
TagVal::_P3 | TagVal::_N3 => NonZeroU8::MIN.saturating_add(2),
|
||||||
|
TagVal::_P4 | TagVal::_N4 => NonZeroU8::MIN.saturating_add(3),
|
||||||
|
TagVal::_P5 | TagVal::_N5 => NonZeroU8::MIN.saturating_add(4),
|
||||||
|
TagVal::_P6 | TagVal::_N6 => NonZeroU8::MIN.saturating_add(5),
|
||||||
|
TagVal::_P7 | TagVal::_N7 => NonZeroU8::MIN.saturating_add(6),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn neg_val(self) -> (bool, u8) {
|
||||||
|
match self.0 {
|
||||||
|
TagVal::_P1 => (false, 1),
|
||||||
|
TagVal::_P2 => (false, 2),
|
||||||
|
TagVal::_P3 => (false, 3),
|
||||||
|
TagVal::_P4 => (false, 4),
|
||||||
|
TagVal::_P5 => (false, 5),
|
||||||
|
TagVal::_P6 => (false, 6),
|
||||||
|
TagVal::_P7 => (false, 7),
|
||||||
|
TagVal::_N1 => (true, 1),
|
||||||
|
TagVal::_N2 => (true, 2),
|
||||||
|
TagVal::_N3 => (true, 3),
|
||||||
|
TagVal::_N4 => (true, 4),
|
||||||
|
TagVal::_N5 => (true, 5),
|
||||||
|
TagVal::_N6 => (true, 6),
|
||||||
|
TagVal::_N7 => (true, 7),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for RawTag {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
f.debug_struct("RawTag")
|
||||||
|
.field("neg", &self.is_neg())
|
||||||
|
.field("val", &self.val())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq)]
|
||||||
|
#[repr(transparent)]
|
||||||
|
struct Header(u16);
|
||||||
|
|
||||||
|
impl Header {
|
||||||
|
#[inline]
|
||||||
|
fn new(tag: RawTag) -> Header {
|
||||||
|
let (neg, val) = tag.neg_val();
|
||||||
|
Header(0x7FF8 | (u16::from(neg) << 15) | u16::from(val))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn tag(self) -> RawTag {
|
||||||
|
unsafe { RawTag::new_unchecked(self.get_sign(), self.get_tag()) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_sign(self) -> bool {
|
||||||
|
self.0 & 0x8000 != 0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn get_tag(self) -> u8 {
|
||||||
|
(self.0 & 0x0007) as u8
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn into_raw(self) -> u16 {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
|
#[repr(C, align(8))]
|
||||||
|
pub struct Value {
|
||||||
|
#[cfg(target_endian = "big")]
|
||||||
|
header: Header,
|
||||||
|
data: [u8; 6],
|
||||||
|
#[cfg(target_endian = "little")]
|
||||||
|
header: Header,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
#[inline]
|
||||||
|
pub fn new(tag: RawTag, data: [u8; 6]) -> Value {
|
||||||
|
Value {
|
||||||
|
header: Header::new(tag),
|
||||||
|
data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn empty(tag: RawTag) -> Value {
|
||||||
|
Value::new(tag, [0; 6])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
|
||||||
|
let mut v = Value::new(tag, [0; 6]);
|
||||||
|
T::to_val(val, &mut v);
|
||||||
|
v
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn load<T: RawStore>(self) -> T {
|
||||||
|
T::from_val(&self)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn tag(&self) -> RawTag {
|
||||||
|
self.header.tag()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn header(&self) -> &Header {
|
||||||
|
&self.header
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn set_data(&mut self, val: [u8; 6]) {
|
||||||
|
self.data = val;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn data(&self) -> &[u8; 6] {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn data_mut(&mut self) -> &mut [u8; 6] {
|
||||||
|
&mut self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub unsafe fn whole(&self) -> &[u8; 8] {
|
||||||
|
let ptr = (self as *const Value).cast::<[u8; 8]>();
|
||||||
|
unsafe { &*ptr }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
|
||||||
|
let ptr = (self as *mut Value).cast::<[u8; 8]>();
|
||||||
|
unsafe { &mut *ptr }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub union RawBox {
|
||||||
|
float: f64,
|
||||||
|
value: ManuallyDrop<Value>,
|
||||||
|
bits: u64,
|
||||||
|
#[cfg(target_pointer_width = "64")]
|
||||||
|
ptr: *const (),
|
||||||
|
#[cfg(target_pointer_width = "32")]
|
||||||
|
ptr: (u32, *const ()),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawBox {
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_float(val: f64) -> RawBox {
|
||||||
|
match (val.is_nan(), val.is_sign_positive()) {
|
||||||
|
(true, true) => RawBox {
|
||||||
|
float: f64::from_bits(QUIET_NAN),
|
||||||
|
},
|
||||||
|
(true, false) => RawBox {
|
||||||
|
float: f64::from_bits(NEG_QUIET_NAN),
|
||||||
|
},
|
||||||
|
(false, _) => RawBox { float: val },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn from_value(value: Value) -> RawBox {
|
||||||
|
RawBox {
|
||||||
|
value: ManuallyDrop::new(value),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn tag(&self) -> Option<RawTag> {
|
||||||
|
if self.is_value() {
|
||||||
|
Some(unsafe { self.value.tag() })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_float(&self) -> bool {
|
||||||
|
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn is_value(&self) -> bool {
|
||||||
|
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn float(&self) -> Option<&f64> {
|
||||||
|
if self.is_float() {
|
||||||
|
Some(unsafe { &self.float })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
#[must_use]
|
||||||
|
pub fn value(&self) -> Option<&Value> {
|
||||||
|
if self.is_value() {
|
||||||
|
Some(unsafe { &self.value })
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub fn into_float_unchecked(self) -> f64 {
|
||||||
|
unsafe { self.float }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for RawBox {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
RawBox {
|
||||||
|
ptr: unsafe { self.ptr },
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for RawBox {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.float() {
|
||||||
|
Some(val) => f.debug_tuple("RawBox::Float").field(val).finish(),
|
||||||
|
None => {
|
||||||
|
let val = self.value().expect("RawBox is neither float nor value");
|
||||||
|
|
||||||
|
f.debug_struct("RawBox::Data")
|
||||||
|
.field("tag", &val.tag())
|
||||||
|
.field("data", val.data())
|
||||||
|
.finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
16
boxing/src/utils.rs
Normal file
16
boxing/src/utils.rs
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
pub 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,6 +9,7 @@ let
|
|||||||
flake = (
|
flake = (
|
||||||
import flake-compat {
|
import flake-compat {
|
||||||
src = ./.;
|
src = ./.;
|
||||||
|
copySourceTreeToStore = false;
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
in
|
in
|
||||||
|
|||||||
106
fix/Cargo.toml
Normal file
106
fix/Cargo.toml
Normal file
@@ -0,0 +1,106 @@
|
|||||||
|
[package]
|
||||||
|
name = "fix"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2024"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
mimalloc = "0.1"
|
||||||
|
|
||||||
|
tokio = { version = "1.41", features = [
|
||||||
|
"rt-multi-thread",
|
||||||
|
"sync",
|
||||||
|
"net",
|
||||||
|
"io-util",
|
||||||
|
] }
|
||||||
|
nix-compat = { git = "https://git.snix.dev/snix/snix.git", version = "0.1.0", features = [
|
||||||
|
"wire",
|
||||||
|
"async",
|
||||||
|
] }
|
||||||
|
|
||||||
|
# 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"
|
||||||
|
bumpalo = { version = "3.20", features = [
|
||||||
|
"allocator-api2",
|
||||||
|
"boxed",
|
||||||
|
"collections",
|
||||||
|
] }
|
||||||
|
|
||||||
|
rust-embed = "8.11"
|
||||||
|
|
||||||
|
itertools = "0.14"
|
||||||
|
|
||||||
|
regex = "1.11"
|
||||||
|
|
||||||
|
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.14"
|
||||||
|
rowan = "0.16"
|
||||||
|
|
||||||
|
ere = "0.2.4"
|
||||||
|
num_enum = "0.7.5"
|
||||||
|
tap = "1.0.1"
|
||||||
|
|
||||||
|
ghost-cell = "0.2"
|
||||||
|
colored = "3.1"
|
||||||
|
boxing = { path = "../boxing" }
|
||||||
|
sealed = "0.6"
|
||||||
|
small-map = "0.1"
|
||||||
|
smallvec = "1.15"
|
||||||
|
|
||||||
|
[dependencies.gc-arena]
|
||||||
|
git = "https://github.com/kyren/gc-arena"
|
||||||
|
rev = "75671ae03f53718357b741ed4027560f14e90836"
|
||||||
|
features = ["allocator-api2", "hashbrown", "smallvec"]
|
||||||
|
|
||||||
|
[dev-dependencies]
|
||||||
|
criterion = { version = "0.8", features = ["html_reports"] }
|
||||||
|
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,6 +1,8 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use utils::eval;
|
use utils::eval;
|
||||||
|
|
||||||
fn bench_arithmetic(c: &mut Criterion) {
|
fn bench_arithmetic(c: &mut Criterion) {
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use utils::eval;
|
use utils::eval;
|
||||||
|
|
||||||
fn bench_builtin_math(c: &mut Criterion) {
|
fn bench_builtin_math(c: &mut Criterion) {
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
use criterion::{Criterion, black_box, criterion_group, criterion_main};
|
use std::hint::black_box;
|
||||||
|
|
||||||
|
use criterion::{Criterion, criterion_group, criterion_main};
|
||||||
use utils::eval;
|
use utils::eval;
|
||||||
|
|
||||||
fn bench_non_recursive(c: &mut Criterion) {
|
fn bench_non_recursive(c: &mut Criterion) {
|
||||||
18
fix/benches/utils.rs
Normal file
18
fix/benches/utils.rs
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use fix::error::{Result, Source};
|
||||||
|
use fix::runtime::Runtime;
|
||||||
|
use fix::value::Value;
|
||||||
|
|
||||||
|
pub fn eval(expr: &str) -> Value {
|
||||||
|
Runtime::new()
|
||||||
|
.unwrap()
|
||||||
|
.eval(Source::new_eval(expr.into()).unwrap())
|
||||||
|
.unwrap()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_result(expr: &str) -> Result<Value> {
|
||||||
|
Runtime::new()
|
||||||
|
.unwrap()
|
||||||
|
.eval(Source::new_eval(expr.into()).unwrap())
|
||||||
|
}
|
||||||
867
fix/src/codegen.rs
Normal file
867
fix/src/codegen.rs
Normal file
@@ -0,0 +1,867 @@
|
|||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use gc_arena::Collect;
|
||||||
|
use hashbrown::HashMap;
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
use rnix::TextRange;
|
||||||
|
use string_interner::Symbol as _;
|
||||||
|
|
||||||
|
use crate::ir::{ArgId, Attr, BinOpKind, Ir, Param, RawIrRef, StringId, ThunkId, UnOpKind};
|
||||||
|
|
||||||
|
pub(crate) struct InstructionPtr(pub usize);
|
||||||
|
|
||||||
|
#[derive(Collect)]
|
||||||
|
#[collect(require_static)]
|
||||||
|
pub struct Bytecode {
|
||||||
|
pub code: Box<[u8]>,
|
||||||
|
pub current_dir: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) trait BytecodeContext {
|
||||||
|
fn intern_string(&mut self, s: &str) -> StringId;
|
||||||
|
fn register_span(&mut self, range: TextRange) -> u32;
|
||||||
|
fn get_code(&self) -> &[u8];
|
||||||
|
fn get_code_mut(&mut self) -> &mut Vec<u8>;
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(u8)]
|
||||||
|
#[derive(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,
|
||||||
|
CallNoSpan,
|
||||||
|
|
||||||
|
MakeAttrs,
|
||||||
|
MakeAttrsDyn,
|
||||||
|
MakeEmptyAttrs,
|
||||||
|
Select,
|
||||||
|
SelectDefault,
|
||||||
|
HasAttr,
|
||||||
|
|
||||||
|
MakeList,
|
||||||
|
|
||||||
|
OpAdd,
|
||||||
|
OpSub,
|
||||||
|
OpMul,
|
||||||
|
OpDiv,
|
||||||
|
OpEq,
|
||||||
|
OpNeq,
|
||||||
|
OpLt,
|
||||||
|
OpGt,
|
||||||
|
OpLeq,
|
||||||
|
OpGeq,
|
||||||
|
OpConcat,
|
||||||
|
OpUpdate,
|
||||||
|
|
||||||
|
OpNeg,
|
||||||
|
OpNot,
|
||||||
|
|
||||||
|
ForceBool,
|
||||||
|
JumpIfFalse,
|
||||||
|
JumpIfTrue,
|
||||||
|
Jump,
|
||||||
|
|
||||||
|
ConcatStrings,
|
||||||
|
ResolvePath,
|
||||||
|
|
||||||
|
Assert,
|
||||||
|
|
||||||
|
PushWith,
|
||||||
|
PopWith,
|
||||||
|
WithLookup,
|
||||||
|
|
||||||
|
LoadBuiltins,
|
||||||
|
LoadBuiltin,
|
||||||
|
|
||||||
|
MkPos,
|
||||||
|
|
||||||
|
LoadReplBinding,
|
||||||
|
LoadScopedBinding,
|
||||||
|
|
||||||
|
Return,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScopeInfo {
|
||||||
|
depth: u16,
|
||||||
|
arg_id: Option<ArgId>,
|
||||||
|
thunk_map: HashMap<ThunkId, u32>,
|
||||||
|
}
|
||||||
|
|
||||||
|
struct BytecodeEmitter<'a, Ctx: BytecodeContext> {
|
||||||
|
ctx: &'a mut Ctx,
|
||||||
|
scope_stack: Vec<ScopeInfo>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn compile_bytecode(ir: RawIrRef<'_>, ctx: &mut impl BytecodeContext) -> InstructionPtr {
|
||||||
|
let ip = ctx.get_code().len();
|
||||||
|
let mut emitter = BytecodeEmitter::new(ctx);
|
||||||
|
emitter.emit_toplevel(ir);
|
||||||
|
InstructionPtr(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Ctx: BytecodeContext> BytecodeEmitter<'a, Ctx> {
|
||||||
|
fn new(ctx: &'a mut Ctx) -> Self {
|
||||||
|
Self {
|
||||||
|
ctx,
|
||||||
|
scope_stack: Vec::with_capacity(32),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
fn emit_op(&mut self, op: Op) {
|
||||||
|
self.ctx.get_code_mut().push(op as u8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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) -> u16 {
|
||||||
|
self.scope_stack.last().map_or(0, |s| s.depth)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn resolve_thunk(&self, id: ThunkId) -> (u16, 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 resolve_arg(&self, id: ArgId) -> (u16, u32) {
|
||||||
|
for scope in self.scope_stack.iter().rev() {
|
||||||
|
if scope.arg_id == Some(id) {
|
||||||
|
let layer = self.current_depth() - scope.depth;
|
||||||
|
return (layer, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
panic!("ArgId {:?} not found in any scope", id);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_load(&mut self, layer: u16, local: u32) {
|
||||||
|
if layer == 0 {
|
||||||
|
self.emit_op(Op::LoadLocal);
|
||||||
|
self.emit_u32(local);
|
||||||
|
} else {
|
||||||
|
self.emit_op(Op::LoadOuter);
|
||||||
|
self.emit_u8(layer as u8);
|
||||||
|
self.emit_u32(local);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn count_with_thunks(&self, ir: RawIrRef<'_>) -> usize {
|
||||||
|
match ir.deref() {
|
||||||
|
Ir::With { thunks, body, .. } => thunks.len() + self.count_with_thunks(*body),
|
||||||
|
Ir::TopLevel { thunks, body } => thunks.len() + self.count_with_thunks(*body),
|
||||||
|
Ir::If { cond, consq, alter } => {
|
||||||
|
self.count_with_thunks(*cond)
|
||||||
|
+ self.count_with_thunks(*consq)
|
||||||
|
+ self.count_with_thunks(*alter)
|
||||||
|
}
|
||||||
|
Ir::BinOp { lhs, rhs, .. } => {
|
||||||
|
self.count_with_thunks(*lhs) + self.count_with_thunks(*rhs)
|
||||||
|
}
|
||||||
|
Ir::UnOp { rhs, .. } => self.count_with_thunks(*rhs),
|
||||||
|
Ir::Call { func, arg, .. } => {
|
||||||
|
self.count_with_thunks(*func) + self.count_with_thunks(*arg)
|
||||||
|
}
|
||||||
|
Ir::Assert {
|
||||||
|
assertion, expr, ..
|
||||||
|
} => self.count_with_thunks(*assertion) + self.count_with_thunks(*expr),
|
||||||
|
Ir::Select { expr, default, .. } => {
|
||||||
|
self.count_with_thunks(*expr) + default.map_or(0, |d| self.count_with_thunks(d))
|
||||||
|
}
|
||||||
|
Ir::HasAttr { lhs, .. } => self.count_with_thunks(*lhs),
|
||||||
|
Ir::ConcatStrings { parts, .. } => {
|
||||||
|
parts.iter().map(|p| self.count_with_thunks(*p)).sum()
|
||||||
|
}
|
||||||
|
Ir::Path(p) => self.count_with_thunks(*p),
|
||||||
|
Ir::List { items } => items.iter().map(|item| self.count_with_thunks(*item)).sum(),
|
||||||
|
Ir::AttrSet { stcs, dyns } => {
|
||||||
|
stcs.iter()
|
||||||
|
.map(|(_, &(val, _))| self.count_with_thunks(val))
|
||||||
|
.sum::<usize>()
|
||||||
|
+ dyns
|
||||||
|
.iter()
|
||||||
|
.map(|&(k, v, _)| self.count_with_thunks(k) + self.count_with_thunks(v))
|
||||||
|
.sum::<usize>()
|
||||||
|
}
|
||||||
|
_ => 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_all_thunks<'ir>(
|
||||||
|
&self,
|
||||||
|
own_thunks: &[(ThunkId, RawIrRef<'ir>)],
|
||||||
|
body: RawIrRef<'ir>,
|
||||||
|
) -> Vec<(ThunkId, RawIrRef<'ir>)> {
|
||||||
|
let mut all = Vec::from(own_thunks);
|
||||||
|
self.collect_with_thunks_recursive(body, &mut all);
|
||||||
|
let mut i = 0;
|
||||||
|
while i < all.len() {
|
||||||
|
let thunk_body = all[i].1;
|
||||||
|
self.collect_with_thunks_recursive(thunk_body, &mut all);
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
all
|
||||||
|
}
|
||||||
|
|
||||||
|
fn collect_with_thunks_recursive<'ir>(
|
||||||
|
&self,
|
||||||
|
ir: RawIrRef<'ir>,
|
||||||
|
out: &mut Vec<(ThunkId, RawIrRef<'ir>)>,
|
||||||
|
) {
|
||||||
|
match ir.deref() {
|
||||||
|
Ir::With { thunks, body, .. } => {
|
||||||
|
for &(id, inner) in thunks.iter() {
|
||||||
|
out.push((id, inner));
|
||||||
|
}
|
||||||
|
self.collect_with_thunks_recursive(*body, out);
|
||||||
|
}
|
||||||
|
Ir::TopLevel { thunks, body } => {
|
||||||
|
for &(id, inner) in thunks.iter() {
|
||||||
|
out.push((id, inner));
|
||||||
|
}
|
||||||
|
self.collect_with_thunks_recursive(*body, out);
|
||||||
|
}
|
||||||
|
Ir::If { cond, consq, alter } => {
|
||||||
|
self.collect_with_thunks_recursive(*cond, out);
|
||||||
|
self.collect_with_thunks_recursive(*consq, out);
|
||||||
|
self.collect_with_thunks_recursive(*alter, out);
|
||||||
|
}
|
||||||
|
Ir::BinOp { lhs, rhs, .. } => {
|
||||||
|
self.collect_with_thunks_recursive(*lhs, out);
|
||||||
|
self.collect_with_thunks_recursive(*rhs, out);
|
||||||
|
}
|
||||||
|
Ir::UnOp { rhs, .. } => self.collect_with_thunks_recursive(*rhs, out),
|
||||||
|
Ir::Call { func, arg, .. } => {
|
||||||
|
self.collect_with_thunks_recursive(*func, out);
|
||||||
|
self.collect_with_thunks_recursive(*arg, out);
|
||||||
|
}
|
||||||
|
Ir::Assert {
|
||||||
|
assertion, expr, ..
|
||||||
|
} => {
|
||||||
|
self.collect_with_thunks_recursive(*assertion, out);
|
||||||
|
self.collect_with_thunks_recursive(*expr, out);
|
||||||
|
}
|
||||||
|
Ir::Select { expr, default, .. } => {
|
||||||
|
self.collect_with_thunks_recursive(*expr, out);
|
||||||
|
if let Some(d) = default {
|
||||||
|
self.collect_with_thunks_recursive(*d, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ir::HasAttr { lhs, .. } => self.collect_with_thunks_recursive(*lhs, out),
|
||||||
|
Ir::ConcatStrings { parts, .. } => {
|
||||||
|
for p in parts.iter() {
|
||||||
|
self.collect_with_thunks_recursive(*p, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ir::Path(p) => self.collect_with_thunks_recursive(*p, out),
|
||||||
|
Ir::List { items } => {
|
||||||
|
for item in items.iter() {
|
||||||
|
self.collect_with_thunks_recursive(*item, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ir::AttrSet { stcs, dyns } => {
|
||||||
|
for (_, &(val, _)) in stcs.iter() {
|
||||||
|
self.collect_with_thunks_recursive(val, out);
|
||||||
|
}
|
||||||
|
for &(key, val, _) in dyns.iter() {
|
||||||
|
self.collect_with_thunks_recursive(key, out);
|
||||||
|
self.collect_with_thunks_recursive(val, out);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn push_scope(&mut self, has_arg: bool, arg_id: Option<ArgId>, thunk_ids: &[ThunkId]) {
|
||||||
|
let depth = self.scope_stack.len() as u16;
|
||||||
|
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,
|
||||||
|
arg_id,
|
||||||
|
thunk_map,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn pop_scope(&mut self) {
|
||||||
|
self.scope_stack.pop();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_toplevel(&mut self, ir: RawIrRef<'_>) {
|
||||||
|
match ir.deref() {
|
||||||
|
Ir::TopLevel { body, thunks } => {
|
||||||
|
let with_thunk_count = self.count_with_thunks(*body);
|
||||||
|
let total_slots = thunks.len() + with_thunk_count;
|
||||||
|
|
||||||
|
let all_thunks = self.collect_all_thunks(thunks, *body);
|
||||||
|
let thunk_ids: Vec<ThunkId> = all_thunks.iter().map(|&(id, _)| id).collect();
|
||||||
|
|
||||||
|
self.push_scope(false, None, &thunk_ids);
|
||||||
|
|
||||||
|
if total_slots > 0 {
|
||||||
|
self.emit_op(Op::AllocLocals);
|
||||||
|
self.emit_u32(total_slots as u32);
|
||||||
|
}
|
||||||
|
|
||||||
|
self.emit_scope_thunks(thunks);
|
||||||
|
self.emit_expr(*body);
|
||||||
|
self.emit_op(Op::Return);
|
||||||
|
|
||||||
|
self.pop_scope();
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
self.push_scope(false, None, &[]);
|
||||||
|
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 label = format!("e{}", id.0);
|
||||||
|
let label_idx = self.ctx.intern_string(&label);
|
||||||
|
|
||||||
|
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);
|
||||||
|
self.emit_str_id(label_idx);
|
||||||
|
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.deref() {
|
||||||
|
&Ir::Int(x) => {
|
||||||
|
if x <= i32::MAX as i64 {
|
||||||
|
self.emit_op(Op::PushSmi);
|
||||||
|
self.emit_i32(x as i32);
|
||||||
|
} else {
|
||||||
|
self.emit_op(Op::PushBigInt);
|
||||||
|
self.emit_i64(x);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&Ir::Float(x) => {
|
||||||
|
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(s) => {
|
||||||
|
let idx = self.ctx.intern_string(s.deref());
|
||||||
|
self.emit_op(Op::PushString);
|
||||||
|
self.emit_str_id(idx);
|
||||||
|
}
|
||||||
|
&Ir::Path(p) => {
|
||||||
|
self.emit_expr(p);
|
||||||
|
self.emit_op(Op::ResolvePath);
|
||||||
|
}
|
||||||
|
&Ir::If { cond, consq, alter } => {
|
||||||
|
self.emit_expr(cond);
|
||||||
|
self.emit_op(Op::ForceBool);
|
||||||
|
|
||||||
|
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,
|
||||||
|
arg,
|
||||||
|
ref thunks,
|
||||||
|
} => {
|
||||||
|
self.emit_func(arg, thunks, param, body);
|
||||||
|
}
|
||||||
|
Ir::AttrSet { stcs, dyns } => {
|
||||||
|
self.emit_attrset(stcs, dyns);
|
||||||
|
}
|
||||||
|
Ir::List { items } => {
|
||||||
|
for &item in items.iter() {
|
||||||
|
self.emit_expr(item);
|
||||||
|
}
|
||||||
|
self.emit_op(Op::MakeList);
|
||||||
|
self.emit_u32(items.len() as u32);
|
||||||
|
}
|
||||||
|
&Ir::Call { func, arg, span } => {
|
||||||
|
self.emit_expr(func);
|
||||||
|
self.emit_expr(arg);
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::Call);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
&Ir::Arg(id) => {
|
||||||
|
let (layer, local) = self.resolve_arg(id);
|
||||||
|
self.emit_load(layer, local);
|
||||||
|
}
|
||||||
|
&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::Thunk(id) => {
|
||||||
|
let (layer, local) = self.resolve_thunk(id);
|
||||||
|
self.emit_load(layer, local);
|
||||||
|
}
|
||||||
|
Ir::Builtins => {
|
||||||
|
self.emit_op(Op::LoadBuiltins);
|
||||||
|
}
|
||||||
|
&Ir::Builtin(name) => {
|
||||||
|
self.emit_op(Op::LoadBuiltin);
|
||||||
|
self.emit_u32(name.0.to_usize() as u32);
|
||||||
|
}
|
||||||
|
&Ir::ConcatStrings {
|
||||||
|
ref parts,
|
||||||
|
force_string,
|
||||||
|
} => {
|
||||||
|
for &part in parts.iter() {
|
||||||
|
self.emit_expr(part);
|
||||||
|
}
|
||||||
|
self.emit_op(Op::ConcatStrings);
|
||||||
|
self.emit_u16(parts.len() as u16);
|
||||||
|
self.emit_u8(if force_string { 1 } else { 0 });
|
||||||
|
}
|
||||||
|
&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_expr(*expr);
|
||||||
|
self.emit_op(Op::Assert);
|
||||||
|
self.emit_str_id(raw_idx);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
&Ir::CurPos(span) => {
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::MkPos);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
&Ir::ReplBinding(name) => {
|
||||||
|
self.emit_op(Op::LoadReplBinding);
|
||||||
|
self.emit_str_id(name);
|
||||||
|
}
|
||||||
|
&Ir::ScopedImportBinding(name) => {
|
||||||
|
self.emit_op(Op::LoadScopedBinding);
|
||||||
|
self.emit_str_id(name);
|
||||||
|
}
|
||||||
|
&Ir::With {
|
||||||
|
namespace,
|
||||||
|
body,
|
||||||
|
ref thunks,
|
||||||
|
} => {
|
||||||
|
self.emit_with(namespace, body, thunks);
|
||||||
|
}
|
||||||
|
&Ir::WithLookup(name) => {
|
||||||
|
self.emit_op(Op::WithLookup);
|
||||||
|
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::ForceBool);
|
||||||
|
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::ForceBool);
|
||||||
|
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::ForceBool);
|
||||||
|
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::ForceBool);
|
||||||
|
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::ForceBool);
|
||||||
|
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::ForceBool);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
PipeL => {
|
||||||
|
self.emit_expr(rhs);
|
||||||
|
self.emit_expr(lhs);
|
||||||
|
self.emit_op(Op::CallNoSpan);
|
||||||
|
}
|
||||||
|
PipeR => {
|
||||||
|
self.emit_expr(lhs);
|
||||||
|
self.emit_expr(rhs);
|
||||||
|
self.emit_op(Op::CallNoSpan);
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
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(
|
||||||
|
&mut self,
|
||||||
|
arg: ArgId,
|
||||||
|
thunks: &[(ThunkId, RawIrRef<'_>)],
|
||||||
|
param: &Option<Param<'_>>,
|
||||||
|
body: RawIrRef<'_>,
|
||||||
|
) {
|
||||||
|
let with_thunk_count = self.count_with_thunks(body);
|
||||||
|
let total_slots = thunks.len() + with_thunk_count;
|
||||||
|
|
||||||
|
let all_thunks = self.collect_all_thunks(thunks, body);
|
||||||
|
let thunk_ids: Vec<ThunkId> = all_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, Some(arg), &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(total_slots as u32);
|
||||||
|
self.emit_u16(required.len() as u16);
|
||||||
|
self.emit_u16(optional.len() as u16);
|
||||||
|
self.emit_u8(if *ellipsis { 1 } else { 0 });
|
||||||
|
|
||||||
|
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(total_slots as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_attrset(
|
||||||
|
&mut self,
|
||||||
|
stcs: &crate::ir::HashMap<'_, StringId, (RawIrRef<'_>, TextRange)>,
|
||||||
|
dyns: &[(RawIrRef<'_>, RawIrRef<'_>, TextRange)],
|
||||||
|
) {
|
||||||
|
if stcs.is_empty() && dyns.is_empty() {
|
||||||
|
self.emit_op(Op::MakeEmptyAttrs);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if !dyns.is_empty() {
|
||||||
|
for (&sym, &(val, _)) in stcs.iter() {
|
||||||
|
self.emit_op(Op::PushString);
|
||||||
|
self.emit_str_id(sym);
|
||||||
|
self.emit_expr(val);
|
||||||
|
}
|
||||||
|
for (_, &(_, span)) in stcs.iter() {
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::PushSmi);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
for &(key, val, span) in dyns.iter() {
|
||||||
|
self.emit_expr(key);
|
||||||
|
self.emit_expr(val);
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::PushSmi);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
self.emit_op(Op::MakeAttrsDyn);
|
||||||
|
self.emit_u32(stcs.len() as u32);
|
||||||
|
self.emit_u32(dyns.len() as u32);
|
||||||
|
} else {
|
||||||
|
for (&sym, &(val, _)) in stcs.iter() {
|
||||||
|
self.emit_op(Op::PushString);
|
||||||
|
self.emit_str_id(sym);
|
||||||
|
self.emit_expr(val);
|
||||||
|
}
|
||||||
|
for (_, &(_, span)) in stcs.iter() {
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::PushSmi);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
self.emit_op(Op::MakeAttrs);
|
||||||
|
self.emit_u32(stcs.len() as u32);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_select(
|
||||||
|
&mut self,
|
||||||
|
expr: RawIrRef<'_>,
|
||||||
|
attrpath: &[Attr<RawIrRef<'_>>],
|
||||||
|
default: Option<RawIrRef<'_>>,
|
||||||
|
span: TextRange,
|
||||||
|
) {
|
||||||
|
self.emit_expr(expr);
|
||||||
|
for attr in attrpath.iter() {
|
||||||
|
match *attr {
|
||||||
|
Attr::Str(sym, _) => {
|
||||||
|
self.emit_op(Op::PushString);
|
||||||
|
self.emit_str_id(sym);
|
||||||
|
}
|
||||||
|
Attr::Dynamic(expr, _) => {
|
||||||
|
self.emit_expr(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(default) = default {
|
||||||
|
self.emit_expr(default);
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::SelectDefault);
|
||||||
|
self.emit_u16(attrpath.len() as u16);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
} else {
|
||||||
|
let span_id = self.ctx.register_span(span);
|
||||||
|
self.emit_op(Op::Select);
|
||||||
|
self.emit_u16(attrpath.len() as u16);
|
||||||
|
self.emit_u32(span_id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_has_attr(&mut self, lhs: RawIrRef<'_>, rhs: &[Attr<RawIrRef<'_>>]) {
|
||||||
|
self.emit_expr(lhs);
|
||||||
|
for attr in rhs.iter() {
|
||||||
|
match *attr {
|
||||||
|
Attr::Str(sym, _) => {
|
||||||
|
self.emit_op(Op::PushString);
|
||||||
|
self.emit_str_id(sym);
|
||||||
|
}
|
||||||
|
Attr::Dynamic(expr, _) => {
|
||||||
|
self.emit_expr(expr);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.emit_op(Op::HasAttr);
|
||||||
|
self.emit_u16(rhs.len() as u16);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_with(
|
||||||
|
&mut self,
|
||||||
|
namespace: RawIrRef<'_>,
|
||||||
|
body: RawIrRef<'_>,
|
||||||
|
thunks: &[(ThunkId, RawIrRef<'_>)],
|
||||||
|
) {
|
||||||
|
self.emit_expr(namespace);
|
||||||
|
self.emit_op(Op::PushWith);
|
||||||
|
self.emit_scope_thunks(thunks);
|
||||||
|
self.emit_expr(body);
|
||||||
|
self.emit_op(Op::PopWith);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn emit_toplevel_inner(&mut self, body: RawIrRef<'_>, thunks: &[(ThunkId, RawIrRef<'_>)]) {
|
||||||
|
self.emit_scope_thunks(thunks);
|
||||||
|
self.emit_expr(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
142
fix/src/derivation.rs
Normal file
142
fix/src/derivation.rs
Normal file
@@ -0,0 +1,142 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet};
|
||||||
|
|
||||||
|
pub struct OutputInfo {
|
||||||
|
pub path: String,
|
||||||
|
pub hash_algo: String,
|
||||||
|
pub hash: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct DerivationData {
|
||||||
|
pub name: String,
|
||||||
|
pub outputs: BTreeMap<String, OutputInfo>,
|
||||||
|
pub input_drvs: BTreeMap<String, BTreeSet<String>>,
|
||||||
|
pub input_srcs: BTreeSet<String>,
|
||||||
|
pub platform: String,
|
||||||
|
pub builder: String,
|
||||||
|
pub args: Vec<String>,
|
||||||
|
pub env: BTreeMap<String, String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_string(s: &str) -> String {
|
||||||
|
let mut result = String::with_capacity(s.len() + 2);
|
||||||
|
result.push('"');
|
||||||
|
for c in s.chars() {
|
||||||
|
match c {
|
||||||
|
'"' => result.push_str("\\\""),
|
||||||
|
'\\' => result.push_str("\\\\"),
|
||||||
|
'\n' => result.push_str("\\n"),
|
||||||
|
'\r' => result.push_str("\\r"),
|
||||||
|
'\t' => result.push_str("\\t"),
|
||||||
|
_ => result.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
result.push('"');
|
||||||
|
result
|
||||||
|
}
|
||||||
|
|
||||||
|
fn quote_string(s: &str) -> String {
|
||||||
|
format!("\"{}\"", s)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl DerivationData {
|
||||||
|
pub fn generate_aterm(&self) -> String {
|
||||||
|
let mut output_entries = Vec::new();
|
||||||
|
for (name, info) in &self.outputs {
|
||||||
|
output_entries.push(format!(
|
||||||
|
"({},{},{},{})",
|
||||||
|
quote_string(name),
|
||||||
|
quote_string(&info.path),
|
||||||
|
quote_string(&info.hash_algo),
|
||||||
|
quote_string(&info.hash),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let outputs = output_entries.join(",");
|
||||||
|
|
||||||
|
let mut input_drv_entries = Vec::new();
|
||||||
|
for (drv_path, output_names) in &self.input_drvs {
|
||||||
|
let sorted_outs: Vec<String> = output_names.iter().map(|s| quote_string(s)).collect();
|
||||||
|
let out_list = format!("[{}]", sorted_outs.join(","));
|
||||||
|
input_drv_entries.push(format!("({},{})", quote_string(drv_path), out_list));
|
||||||
|
}
|
||||||
|
let input_drvs = input_drv_entries.join(",");
|
||||||
|
|
||||||
|
let input_srcs: Vec<String> = self.input_srcs.iter().map(|s| quote_string(s)).collect();
|
||||||
|
let input_srcs = input_srcs.join(",");
|
||||||
|
|
||||||
|
let args: Vec<String> = self.args.iter().map(|s| escape_string(s)).collect();
|
||||||
|
let args = args.join(",");
|
||||||
|
|
||||||
|
let mut env_entries: Vec<String> = Vec::new();
|
||||||
|
for (k, v) in &self.env {
|
||||||
|
env_entries.push(format!("({},{})", escape_string(k), escape_string(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Derive([{}],[{}],[{}],{},{},[{}],[{}])",
|
||||||
|
outputs,
|
||||||
|
input_drvs,
|
||||||
|
input_srcs,
|
||||||
|
quote_string(&self.platform),
|
||||||
|
escape_string(&self.builder),
|
||||||
|
args,
|
||||||
|
env_entries.join(","),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn generate_aterm_modulo(&self, input_drv_hashes: &BTreeMap<String, String>) -> String {
|
||||||
|
let mut output_entries = Vec::new();
|
||||||
|
for (name, info) in &self.outputs {
|
||||||
|
output_entries.push(format!(
|
||||||
|
"({},{},{},{})",
|
||||||
|
quote_string(name),
|
||||||
|
quote_string(&info.path),
|
||||||
|
quote_string(&info.hash_algo),
|
||||||
|
quote_string(&info.hash),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
let outputs = output_entries.join(",");
|
||||||
|
|
||||||
|
let mut input_drv_entries = Vec::new();
|
||||||
|
for (drv_hash, outputs_csv) in input_drv_hashes {
|
||||||
|
let mut sorted_outs: Vec<&str> = outputs_csv.split(',').collect();
|
||||||
|
sorted_outs.sort();
|
||||||
|
let out_list: Vec<String> = sorted_outs.iter().map(|s| quote_string(s)).collect();
|
||||||
|
let out_list = format!("[{}]", out_list.join(","));
|
||||||
|
input_drv_entries.push(format!("({},{})", quote_string(drv_hash), out_list));
|
||||||
|
}
|
||||||
|
let input_drvs = input_drv_entries.join(",");
|
||||||
|
|
||||||
|
let input_srcs: Vec<String> = self.input_srcs.iter().map(|s| quote_string(s)).collect();
|
||||||
|
let input_srcs = input_srcs.join(",");
|
||||||
|
|
||||||
|
let args: Vec<String> = self.args.iter().map(|s| escape_string(s)).collect();
|
||||||
|
let args = args.join(",");
|
||||||
|
|
||||||
|
let mut env_entries: Vec<String> = Vec::new();
|
||||||
|
for (k, v) in &self.env {
|
||||||
|
env_entries.push(format!("({},{})", escape_string(k), escape_string(v)));
|
||||||
|
}
|
||||||
|
|
||||||
|
format!(
|
||||||
|
"Derive([{}],[{}],[{}],{},{},[{}],[{}])",
|
||||||
|
outputs,
|
||||||
|
input_drvs,
|
||||||
|
input_srcs,
|
||||||
|
quote_string(&self.platform),
|
||||||
|
escape_string(&self.builder),
|
||||||
|
args,
|
||||||
|
env_entries.join(","),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn collect_references(&self) -> Vec<String> {
|
||||||
|
let mut refs = BTreeSet::new();
|
||||||
|
for src in &self.input_srcs {
|
||||||
|
refs.insert(src.clone());
|
||||||
|
}
|
||||||
|
for drv_path in self.input_drvs.keys() {
|
||||||
|
refs.insert(drv_path.clone());
|
||||||
|
}
|
||||||
|
refs.into_iter().collect()
|
||||||
|
}
|
||||||
|
}
|
||||||
372
fix/src/disassembler.rs
Normal file
372
fix/src/disassembler.rs
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
use std::fmt::Write;
|
||||||
|
|
||||||
|
use colored::Colorize;
|
||||||
|
use num_enum::TryFromPrimitive;
|
||||||
|
|
||||||
|
use crate::codegen::{Bytecode, Op};
|
||||||
|
|
||||||
|
pub(crate) trait DisassemblerContext {
|
||||||
|
fn lookup_string(&self, id: u32) -> &str;
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) struct Disassembler<'a, Ctx> {
|
||||||
|
code: &'a [u8],
|
||||||
|
ctx: &'a Ctx,
|
||||||
|
pos: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a, Ctx: DisassemblerContext> Disassembler<'a, Ctx> {
|
||||||
|
pub fn new(bytecode: &'a Bytecode, ctx: &'a Ctx) -> Self {
|
||||||
|
Self {
|
||||||
|
code: &bytecode.code,
|
||||||
|
ctx,
|
||||||
|
pos: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u8(&mut self) -> u8 {
|
||||||
|
let b = self.code[self.pos];
|
||||||
|
self.pos += 1;
|
||||||
|
b
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u16(&mut self) -> u16 {
|
||||||
|
let bytes = self.code[self.pos..self.pos + 2]
|
||||||
|
.try_into()
|
||||||
|
.expect("no enough bytes");
|
||||||
|
self.pos += 2;
|
||||||
|
u16::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_u32(&mut self) -> u32 {
|
||||||
|
let bytes = self.code[self.pos..self.pos + 4]
|
||||||
|
.try_into()
|
||||||
|
.expect("no enough bytes");
|
||||||
|
self.pos += 4;
|
||||||
|
u32::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_i32(&mut self) -> i32 {
|
||||||
|
let bytes = self.code[self.pos..self.pos + 4]
|
||||||
|
.try_into()
|
||||||
|
.expect("no enough bytes");
|
||||||
|
self.pos += 4;
|
||||||
|
i32::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_i64(&mut self) -> i64 {
|
||||||
|
let bytes = self.code[self.pos..self.pos + 8]
|
||||||
|
.try_into()
|
||||||
|
.expect("no enough bytes");
|
||||||
|
self.pos += 8;
|
||||||
|
i64::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn read_f64(&mut self) -> f64 {
|
||||||
|
let bytes = self.code[self.pos..self.pos + 8]
|
||||||
|
.try_into()
|
||||||
|
.expect("no enough bytes");
|
||||||
|
self.pos += 8;
|
||||||
|
f64::from_le_bytes(bytes)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn disassemble(&mut self) -> String {
|
||||||
|
self.disassemble_impl(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
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.pos < self.code.len() {
|
||||||
|
let start_pos = self.pos;
|
||||||
|
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.pos];
|
||||||
|
|
||||||
|
for (i, chunk) in bytes_slice.chunks(4).enumerate() {
|
||||||
|
let bytes_str = {
|
||||||
|
let mut temp = String::new();
|
||||||
|
if i == 0 {
|
||||||
|
let _ = write!(&mut temp, "{:02x}", self.code[start_pos]);
|
||||||
|
} else {
|
||||||
|
let _ = write!(&mut temp, " ");
|
||||||
|
}
|
||||||
|
for b in chunk.iter() {
|
||||||
|
let _ = write!(&mut temp, " {:02x}", b);
|
||||||
|
}
|
||||||
|
temp
|
||||||
|
};
|
||||||
|
|
||||||
|
if i == 0 {
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let extra_width = start_pos.ilog2() >> 4;
|
||||||
|
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.lookup_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();
|
||||||
|
let label_idx = self.read_u32();
|
||||||
|
let label = self.ctx.lookup_string(label_idx);
|
||||||
|
("MakeThunk", format!("-> {:04x} label={}", offset, label))
|
||||||
|
}
|
||||||
|
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.lookup_string(idx)));
|
||||||
|
}
|
||||||
|
for _ in 0..opt_count {
|
||||||
|
let idx = self.read_u32();
|
||||||
|
arg_str.push_str(&format!("Opt({}) ", self.ctx.lookup_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 => {
|
||||||
|
let span_id = self.read_u32();
|
||||||
|
("Call", format!("span={}", span_id))
|
||||||
|
}
|
||||||
|
Op::CallNoSpan => ("CallNoSpan", String::new()),
|
||||||
|
|
||||||
|
Op::MakeAttrs => {
|
||||||
|
let count = self.read_u32();
|
||||||
|
("MakeAttrs", format!("size={}", count))
|
||||||
|
}
|
||||||
|
Op::MakeAttrsDyn => {
|
||||||
|
let static_count = self.read_u32();
|
||||||
|
let dyn_count = self.read_u32();
|
||||||
|
(
|
||||||
|
"MakeAttrsDyn",
|
||||||
|
format!("static={} dyn={}", static_count, dyn_count),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Op::MakeEmptyAttrs => ("MakeEmptyAttrs", String::new()),
|
||||||
|
|
||||||
|
Op::Select => {
|
||||||
|
let path_len = self.read_u16();
|
||||||
|
let span_id = self.read_u32();
|
||||||
|
("Select", format!("path_len={} span={}", path_len, span_id))
|
||||||
|
}
|
||||||
|
Op::SelectDefault => {
|
||||||
|
let path_len = self.read_u16();
|
||||||
|
let span_id = self.read_u32();
|
||||||
|
(
|
||||||
|
"SelectDefault",
|
||||||
|
format!("path_len={} span={}", path_len, span_id),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Op::HasAttr => {
|
||||||
|
let path_len = self.read_u16();
|
||||||
|
("HasAttr", format!("path_len={}", path_len))
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::MakeList => {
|
||||||
|
let count = self.read_u32();
|
||||||
|
("MakeList", format!("size={}", count))
|
||||||
|
}
|
||||||
|
|
||||||
|
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::ForceBool => ("ForceBool", 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::ResolvePath => ("ResolvePath", String::new()),
|
||||||
|
Op::Assert => {
|
||||||
|
let raw_idx = self.read_u32();
|
||||||
|
let span_id = self.read_u32();
|
||||||
|
("Assert", format!("text_id={} span={}", raw_idx, span_id))
|
||||||
|
}
|
||||||
|
Op::PushWith => ("PushWith", String::new()),
|
||||||
|
Op::PopWith => ("PopWith", String::new()),
|
||||||
|
Op::WithLookup => {
|
||||||
|
let idx = self.read_u32();
|
||||||
|
let name = self.ctx.lookup_string(idx);
|
||||||
|
("WithLookup", format!("{:?}", name))
|
||||||
|
}
|
||||||
|
|
||||||
|
Op::LoadBuiltins => ("LoadBuiltins", String::new()),
|
||||||
|
Op::LoadBuiltin => {
|
||||||
|
let idx = self.read_u32();
|
||||||
|
let name = self.ctx.lookup_string(idx);
|
||||||
|
("LoadBuiltin", format!("{:?}", name))
|
||||||
|
}
|
||||||
|
Op::MkPos => {
|
||||||
|
let span_id = self.read_u32();
|
||||||
|
("MkPos", format!("id={}", span_id))
|
||||||
|
}
|
||||||
|
Op::LoadReplBinding => {
|
||||||
|
let idx = self.read_u32();
|
||||||
|
let name = self.ctx.lookup_string(idx);
|
||||||
|
("LoadReplBinding", format!("{:?}", name))
|
||||||
|
}
|
||||||
|
Op::LoadScopedBinding => {
|
||||||
|
let idx = self.read_u32();
|
||||||
|
let name = self.ctx.lookup_string(idx);
|
||||||
|
("LoadScopedBinding", format!("{:?}", name))
|
||||||
|
}
|
||||||
|
Op::Return => ("Return", String::new()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
1249
fix/src/downgrade.rs
Normal file
1249
fix/src/downgrade.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,16 +1,9 @@
|
|||||||
#![allow(unused_assignments)]
|
|
||||||
|
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
use deno_core::error::JsError;
|
|
||||||
use deno_error::JsErrorClass as _;
|
|
||||||
use itertools::Itertools as _;
|
|
||||||
use miette::{Diagnostic, NamedSource, SourceSpan};
|
use miette::{Diagnostic, NamedSource, SourceSpan};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::runtime::RuntimeContext;
|
|
||||||
|
|
||||||
pub type Result<T> = core::result::Result<T, Box<Error>>;
|
pub type Result<T> = core::result::Result<T, Box<Error>>;
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -21,6 +14,8 @@ pub enum SourceType {
|
|||||||
Repl(Arc<PathBuf>),
|
Repl(Arc<PathBuf>),
|
||||||
/// file
|
/// file
|
||||||
File(Arc<PathBuf>),
|
File(Arc<PathBuf>),
|
||||||
|
/// virtual (name, no path)
|
||||||
|
Virtual(Arc<str>),
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug)]
|
#[derive(Clone, Debug)]
|
||||||
@@ -44,6 +39,13 @@ impl From<Source> for NamedSource<Arc<str>> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Source {
|
impl Source {
|
||||||
|
pub fn new_file(path: PathBuf) -> std::io::Result<Self> {
|
||||||
|
Ok(Source {
|
||||||
|
src: std::fs::read_to_string(&path)?.into(),
|
||||||
|
ty: crate::error::SourceType::File(Arc::new(path)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
pub fn new_eval(src: String) -> Result<Self> {
|
pub fn new_eval(src: String) -> Result<Self> {
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
ty: std::env::current_dir()
|
ty: std::env::current_dir()
|
||||||
@@ -64,6 +66,13 @@ impl Source {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_virtual(name: Arc<str>, src: String) -> Self {
|
||||||
|
Self {
|
||||||
|
ty: SourceType::Virtual(name),
|
||||||
|
src: src.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_dir(&self) -> &Path {
|
pub fn get_dir(&self) -> &Path {
|
||||||
use SourceType::*;
|
use SourceType::*;
|
||||||
match &self.ty {
|
match &self.ty {
|
||||||
@@ -72,6 +81,7 @@ impl Source {
|
|||||||
.as_path()
|
.as_path()
|
||||||
.parent()
|
.parent()
|
||||||
.expect("source file must have a parent dir"),
|
.expect("source file must have a parent dir"),
|
||||||
|
Virtual(_) => Path::new("/"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -80,6 +90,7 @@ impl Source {
|
|||||||
SourceType::Eval(_) => "«eval»".into(),
|
SourceType::Eval(_) => "«eval»".into(),
|
||||||
SourceType::Repl(_) => "«repl»".into(),
|
SourceType::Repl(_) => "«repl»".into(),
|
||||||
SourceType::File(path) => path.as_os_str().to_string_lossy().to_string(),
|
SourceType::File(path) => path.as_os_str().to_string_lossy().to_string(),
|
||||||
|
SourceType::Virtual(name) => name.to_string(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -213,138 +224,3 @@ pub struct StackFrame {
|
|||||||
#[source_code]
|
#[source_code]
|
||||||
pub src: NamedSource<Arc<str>>,
|
pub src: NamedSource<Arc<str>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
const MAX_STACK_FRAMES: usize = 20;
|
|
||||||
const FRAMES_AT_START: usize = 15;
|
|
||||||
const FRAMES_AT_END: usize = 5;
|
|
||||||
|
|
||||||
pub(crate) fn parse_js_error(error: Box<JsError>, ctx: &impl RuntimeContext) -> Error {
|
|
||||||
let (span, src, frames) = if let Some(stack) = &error.stack {
|
|
||||||
let mut frames = parse_frames(stack, ctx);
|
|
||||||
|
|
||||||
if let Some(last_frame) = frames.pop() {
|
|
||||||
(
|
|
||||||
Some(text_range_to_source_span(last_frame.span)),
|
|
||||||
Some(last_frame.src.into()),
|
|
||||||
frames,
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
(None, None, frames)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(None, None, Vec::new())
|
|
||||||
};
|
|
||||||
let stack_trace = if std::env::var("NIX_JS_STACK_TRACE").is_ok() {
|
|
||||||
truncate_stack_trace(frames)
|
|
||||||
} else {
|
|
||||||
Vec::new()
|
|
||||||
};
|
|
||||||
let message = error.get_message().to_string();
|
|
||||||
let js_backtrace = error.stack.map(|stack| {
|
|
||||||
stack
|
|
||||||
.lines()
|
|
||||||
.filter(|line| !line.starts_with("NIX_STACK_FRAME:"))
|
|
||||||
.join("\n")
|
|
||||||
});
|
|
||||||
|
|
||||||
Error::EvalError {
|
|
||||||
src,
|
|
||||||
span,
|
|
||||||
message,
|
|
||||||
js_backtrace,
|
|
||||||
stack_trace,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct NixStackFrame {
|
|
||||||
span: rnix::TextRange,
|
|
||||||
message: String,
|
|
||||||
src: Source,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<NixStackFrame> for StackFrame {
|
|
||||||
fn from(NixStackFrame { span, message, src }: NixStackFrame) -> Self {
|
|
||||||
StackFrame {
|
|
||||||
span: text_range_to_source_span(span),
|
|
||||||
message,
|
|
||||||
src: src.into(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
fn parse_frames(stack: &str, ctx: &impl RuntimeContext) -> Vec<NixStackFrame> {
|
|
||||||
let mut frames = Vec::new();
|
|
||||||
|
|
||||||
for line in stack.lines() {
|
|
||||||
// Format: NIX_STACK_FRAME:source_id:start:end[:extra_data]
|
|
||||||
let Some(rest) = line.strip_prefix("NIX_STACK_FRAME:") else {
|
|
||||||
continue;
|
|
||||||
};
|
|
||||||
let parts: Vec<&str> = rest.splitn(4, ':').collect();
|
|
||||||
|
|
||||||
if parts.len() < 3 {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
let src = match parts[0].parse() {
|
|
||||||
Ok(id) => ctx.get_source(id),
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let start: u32 = match parts[1].parse() {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
let end: u32 = match parts[2].parse() {
|
|
||||||
Ok(v) => v,
|
|
||||||
Err(_) => continue,
|
|
||||||
};
|
|
||||||
|
|
||||||
let span = rnix::TextRange::new(rnix::TextSize::from(start), rnix::TextSize::from(end));
|
|
||||||
|
|
||||||
let message = {
|
|
||||||
if parts.len() == 4 {
|
|
||||||
parts[3].to_string()
|
|
||||||
} else {
|
|
||||||
String::new()
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
frames.push(NixStackFrame { span, message, src });
|
|
||||||
}
|
|
||||||
|
|
||||||
// Deduplicate consecutive identical frames
|
|
||||||
frames.dedup_by(|a, b| a.span == b.span && a.message == b.message);
|
|
||||||
|
|
||||||
frames
|
|
||||||
}
|
|
||||||
|
|
||||||
fn truncate_stack_trace(frames: Vec<NixStackFrame>) -> Vec<StackFrame> {
|
|
||||||
let reversed: Vec<_> = frames.into_iter().rev().collect();
|
|
||||||
let total = reversed.len();
|
|
||||||
|
|
||||||
if total <= MAX_STACK_FRAMES {
|
|
||||||
return reversed.into_iter().map(Into::into).collect();
|
|
||||||
}
|
|
||||||
|
|
||||||
let omitted_count = total - FRAMES_AT_START - FRAMES_AT_END;
|
|
||||||
|
|
||||||
reversed
|
|
||||||
.into_iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(i, frame)| {
|
|
||||||
if i < FRAMES_AT_START {
|
|
||||||
Some(frame.into())
|
|
||||||
} else if i == FRAMES_AT_START {
|
|
||||||
Some(StackFrame {
|
|
||||||
span: text_range_to_source_span(frame.span),
|
|
||||||
message: format!("... ({} more frames omitted)", omitted_count),
|
|
||||||
src: frame.src.into(),
|
|
||||||
})
|
|
||||||
} else if i >= total - FRAMES_AT_END {
|
|
||||||
Some(frame.into())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.collect()
|
|
||||||
}
|
|
||||||
@@ -1,16 +1,16 @@
|
|||||||
use deno_core::OpState;
|
use deno_core::OpState;
|
||||||
|
use deno_core::ToV8;
|
||||||
use deno_core::op2;
|
use deno_core::op2;
|
||||||
use serde::Serialize;
|
use nix_compat::nixhash::HashAlgo;
|
||||||
|
use nix_compat::nixhash::NixHash;
|
||||||
use tracing::{debug, info, warn};
|
use tracing::{debug, info, warn};
|
||||||
|
|
||||||
use crate::runtime::OpStateExt;
|
use crate::store::Store as _;
|
||||||
use crate::runtime::RuntimeContext;
|
|
||||||
|
|
||||||
mod archive;
|
mod archive;
|
||||||
pub(crate) mod cache;
|
pub(crate) mod cache;
|
||||||
mod download;
|
mod download;
|
||||||
mod git;
|
mod git;
|
||||||
mod hg;
|
|
||||||
mod metadata_cache;
|
mod metadata_cache;
|
||||||
|
|
||||||
pub use cache::FetcherCache;
|
pub use cache::FetcherCache;
|
||||||
@@ -18,22 +18,20 @@ pub use download::Downloader;
|
|||||||
pub use metadata_cache::MetadataCache;
|
pub use metadata_cache::MetadataCache;
|
||||||
|
|
||||||
use crate::nar;
|
use crate::nar;
|
||||||
use crate::runtime::NixError;
|
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(ToV8)]
|
||||||
pub struct FetchUrlResult {
|
pub struct FetchUrlResult {
|
||||||
pub store_path: String,
|
pub store_path: String,
|
||||||
pub hash: String,
|
pub hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(ToV8)]
|
||||||
pub struct FetchTarballResult {
|
pub struct FetchTarballResult {
|
||||||
pub store_path: String,
|
pub store_path: String,
|
||||||
pub hash: String,
|
|
||||||
pub nar_hash: String,
|
pub nar_hash: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
#[derive(ToV8)]
|
||||||
pub struct FetchGitResult {
|
pub struct FetchGitResult {
|
||||||
pub out_path: String,
|
pub out_path: String,
|
||||||
pub rev: String,
|
pub rev: String,
|
||||||
@@ -45,31 +43,22 @@ pub struct FetchGitResult {
|
|||||||
pub nar_hash: Option<String>,
|
pub nar_hash: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Serialize)]
|
|
||||||
pub struct FetchHgResult {
|
|
||||||
pub out_path: String,
|
|
||||||
pub branch: String,
|
|
||||||
pub rev: String,
|
|
||||||
pub short_rev: String,
|
|
||||||
pub rev_count: u64,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
#[serde]
|
|
||||||
pub fn op_fetch_url<Ctx: RuntimeContext>(
|
pub fn op_fetch_url<Ctx: RuntimeContext>(
|
||||||
state: &mut OpState,
|
state: &mut OpState,
|
||||||
#[string] url: String,
|
#[string] url: String,
|
||||||
#[string] expected_hash: Option<String>,
|
#[string] expected_hash: Option<String>,
|
||||||
#[string] name: Option<String>,
|
#[string] name: Option<String>,
|
||||||
executable: bool,
|
executable: bool,
|
||||||
) -> Result<FetchUrlResult, NixError> {
|
) -> Result<FetchUrlResult, NixRuntimeError> {
|
||||||
let _span = tracing::info_span!("op_fetch_url", url = %url).entered();
|
let _span = tracing::info_span!("op_fetch_url", url = %url).entered();
|
||||||
info!("fetchurl started");
|
info!("fetchurl started");
|
||||||
|
|
||||||
let file_name =
|
let file_name =
|
||||||
name.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string());
|
name.unwrap_or_else(|| url.rsplit('/').next().unwrap_or("download").to_string());
|
||||||
|
|
||||||
let metadata_cache = MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?;
|
let metadata_cache =
|
||||||
|
MetadataCache::new(3600).map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
let input = serde_json::json!({
|
let input = serde_json::json!({
|
||||||
"type": "file",
|
"type": "file",
|
||||||
@@ -80,7 +69,7 @@ pub fn op_fetch_url<Ctx: RuntimeContext>(
|
|||||||
|
|
||||||
if let Some(cached_entry) = metadata_cache
|
if let Some(cached_entry) = metadata_cache
|
||||||
.lookup(&input)
|
.lookup(&input)
|
||||||
.map_err(|e| NixError::from(e.to_string()))?
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?
|
||||||
{
|
{
|
||||||
let cached_hash = cached_entry
|
let cached_hash = cached_entry
|
||||||
.info
|
.info
|
||||||
@@ -112,16 +101,16 @@ pub fn op_fetch_url<Ctx: RuntimeContext>(
|
|||||||
let downloader = Downloader::new();
|
let downloader = Downloader::new();
|
||||||
let data = downloader
|
let data = downloader
|
||||||
.download(&url)
|
.download(&url)
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
info!(bytes = data.len(), "Download complete");
|
info!(bytes = data.len(), "Download complete");
|
||||||
|
|
||||||
let hash = crate::nix_hash::sha256_hex(&String::from_utf8_lossy(&data));
|
let hash = crate::nix_utils::sha256_hex(&data);
|
||||||
|
|
||||||
if let Some(ref expected) = expected_hash {
|
if let Some(ref expected) = expected_hash {
|
||||||
let normalized_expected = normalize_hash(expected);
|
let normalized_expected = normalize_hash(expected);
|
||||||
if hash != normalized_expected {
|
if hash != normalized_expected {
|
||||||
return Err(NixError::from(format!(
|
return Err(NixRuntimeError::from(format!(
|
||||||
"hash mismatch for '{}': expected {}, got {}",
|
"hash mismatch for '{}': expected {}, got {}",
|
||||||
url, normalized_expected, hash
|
url, normalized_expected, hash
|
||||||
)));
|
)));
|
||||||
@@ -132,7 +121,7 @@ pub fn op_fetch_url<Ctx: RuntimeContext>(
|
|||||||
let store = ctx.get_store();
|
let store = ctx.get_store();
|
||||||
let store_path = store
|
let store_path = store
|
||||||
.add_to_store(&file_name, &data, false, vec![])
|
.add_to_store(&file_name, &data, false, vec![])
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
info!(store_path = %store_path, "Added to store");
|
info!(store_path = %store_path, "Added to store");
|
||||||
|
|
||||||
@@ -153,25 +142,24 @@ pub fn op_fetch_url<Ctx: RuntimeContext>(
|
|||||||
|
|
||||||
metadata_cache
|
metadata_cache
|
||||||
.add(&input, &info, &store_path, true)
|
.add(&input, &info, &store_path, true)
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
Ok(FetchUrlResult { store_path, hash })
|
Ok(FetchUrlResult { store_path, hash })
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
#[serde]
|
|
||||||
pub fn op_fetch_tarball<Ctx: RuntimeContext>(
|
pub fn op_fetch_tarball<Ctx: RuntimeContext>(
|
||||||
state: &mut OpState,
|
state: &mut OpState,
|
||||||
#[string] url: String,
|
#[string] url: String,
|
||||||
#[string] expected_hash: Option<String>,
|
|
||||||
#[string] expected_nar_hash: Option<String>,
|
|
||||||
#[string] name: Option<String>,
|
#[string] name: Option<String>,
|
||||||
) -> Result<FetchTarballResult, NixError> {
|
#[string] sha256: Option<String>,
|
||||||
|
) -> Result<FetchTarballResult, NixRuntimeError> {
|
||||||
let _span = tracing::info_span!("op_fetch_tarball", url = %url).entered();
|
let _span = tracing::info_span!("op_fetch_tarball", url = %url).entered();
|
||||||
info!("fetchTarball started");
|
info!("fetchTarball started");
|
||||||
|
|
||||||
let dir_name = name.unwrap_or_else(|| "source".to_string());
|
let dir_name = name.unwrap_or_else(|| "source".to_string());
|
||||||
let metadata_cache = MetadataCache::new(3600).map_err(|e| NixError::from(e.to_string()))?;
|
let metadata_cache =
|
||||||
|
MetadataCache::new(3600).map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
let input = serde_json::json!({
|
let input = serde_json::json!({
|
||||||
"type": "tarball",
|
"type": "tarball",
|
||||||
@@ -179,28 +167,31 @@ pub fn op_fetch_tarball<Ctx: RuntimeContext>(
|
|||||||
"name": dir_name,
|
"name": dir_name,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
let expected_sha256 = sha256
|
||||||
|
.map(
|
||||||
|
|ref sha256| match NixHash::from_str(sha256, Some(HashAlgo::Sha256)) {
|
||||||
|
Ok(NixHash::Sha256(digest)) => Ok(digest),
|
||||||
|
_ => Err(format!("fetchTarball: invalid sha256 '{sha256}'")),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.transpose()?;
|
||||||
|
let expected_hex = expected_sha256.map(hex::encode);
|
||||||
|
|
||||||
if let Some(cached_entry) = metadata_cache
|
if let Some(cached_entry) = metadata_cache
|
||||||
.lookup(&input)
|
.lookup(&input)
|
||||||
.map_err(|e| NixError::from(e.to_string()))?
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?
|
||||||
{
|
{
|
||||||
let cached_nar_hash = cached_entry
|
let cached_nar_hash = cached_entry
|
||||||
.info
|
.info
|
||||||
.get("nar_hash")
|
.get("nar_hash")
|
||||||
.and_then(|v| v.as_str())
|
.and_then(|v| v.as_str())
|
||||||
.unwrap_or("");
|
.unwrap_or("");
|
||||||
let cached_tarball_hash = cached_entry
|
|
||||||
.info
|
|
||||||
.get("tarball_hash")
|
|
||||||
.and_then(|v| v.as_str())
|
|
||||||
.unwrap_or("");
|
|
||||||
|
|
||||||
if let Some(ref expected_nar) = expected_nar_hash {
|
if let Some(ref hex) = expected_hex {
|
||||||
let normalized_expected = normalize_hash(expected_nar);
|
if cached_nar_hash == hex {
|
||||||
if cached_nar_hash == normalized_expected {
|
|
||||||
info!("Cache hit");
|
info!("Cache hit");
|
||||||
return Ok(FetchTarballResult {
|
return Ok(FetchTarballResult {
|
||||||
store_path: cached_entry.store_path.clone(),
|
store_path: cached_entry.store_path.clone(),
|
||||||
hash: cached_tarball_hash.to_string(),
|
|
||||||
nar_hash: cached_nar_hash.to_string(),
|
nar_hash: cached_nar_hash.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -208,7 +199,6 @@ pub fn op_fetch_tarball<Ctx: RuntimeContext>(
|
|||||||
info!("Cache hit (no hash check)");
|
info!("Cache hit (no hash check)");
|
||||||
return Ok(FetchTarballResult {
|
return Ok(FetchTarballResult {
|
||||||
store_path: cached_entry.store_path.clone(),
|
store_path: cached_entry.store_path.clone(),
|
||||||
hash: cached_tarball_hash.to_string(),
|
|
||||||
nar_hash: cached_nar_hash.to_string(),
|
nar_hash: cached_nar_hash.to_string(),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -218,77 +208,59 @@ pub fn op_fetch_tarball<Ctx: RuntimeContext>(
|
|||||||
let downloader = Downloader::new();
|
let downloader = Downloader::new();
|
||||||
let data = downloader
|
let data = downloader
|
||||||
.download(&url)
|
.download(&url)
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
info!(bytes = data.len(), "Download complete");
|
info!(bytes = data.len(), "Download complete");
|
||||||
|
|
||||||
let tarball_hash = crate::nix_hash::sha256_hex(&String::from_utf8_lossy(&data));
|
|
||||||
|
|
||||||
if let Some(ref expected) = expected_hash {
|
|
||||||
let normalized_expected = normalize_hash(expected);
|
|
||||||
if tarball_hash != normalized_expected {
|
|
||||||
return Err(NixError::from(format!(
|
|
||||||
"Tarball hash mismatch for '{}': expected {}, got {}",
|
|
||||||
url, normalized_expected, tarball_hash
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
info!("Extracting tarball");
|
info!("Extracting tarball");
|
||||||
let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?;
|
let (extracted_path, _temp_dir) = archive::extract_tarball_to_temp(&data)
|
||||||
let (extracted_path, _temp_dir) = cache
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
.extract_tarball_to_temp(&data)
|
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
|
||||||
|
|
||||||
info!("Computing NAR hash");
|
info!("Computing NAR hash");
|
||||||
let nar_hash =
|
let nar_hash =
|
||||||
nar::compute_nar_hash(&extracted_path).map_err(|e| NixError::from(e.to_string()))?;
|
nar::compute_nar_hash(&extracted_path).map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
let nar_hash_hex = hex::encode(nar_hash);
|
||||||
|
|
||||||
debug!(
|
debug!(
|
||||||
tarball_hash = %tarball_hash,
|
nar_hash = %nar_hash_hex,
|
||||||
nar_hash = %nar_hash,
|
|
||||||
"Hash computation complete"
|
"Hash computation complete"
|
||||||
);
|
);
|
||||||
|
|
||||||
if let Some(ref expected) = expected_nar_hash {
|
if let Some(ref expected) = expected_hex
|
||||||
let normalized_expected = normalize_hash(expected);
|
&& nar_hash_hex != *expected
|
||||||
if nar_hash != normalized_expected {
|
{
|
||||||
return Err(NixError::from(format!(
|
return Err(NixRuntimeError::from(format!(
|
||||||
"NAR hash mismatch for '{}': expected {}, got {}",
|
"Tarball hash mismatch for '{}': expected {}, got {}",
|
||||||
url, normalized_expected, nar_hash
|
url, expected, nar_hash_hex
|
||||||
)));
|
)));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
info!("Adding to store");
|
info!("Adding to store");
|
||||||
let ctx: &Ctx = state.get_ctx();
|
let ctx: &Ctx = state.get_ctx();
|
||||||
let store = ctx.get_store();
|
let store = ctx.get_store();
|
||||||
let store_path = store
|
let store_path = store
|
||||||
.add_to_store_from_path(&dir_name, &extracted_path, vec![])
|
.add_to_store_from_path(&dir_name, &extracted_path, vec![])
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
info!(store_path = %store_path, "Added to store");
|
info!(store_path = %store_path, "Added to store");
|
||||||
|
|
||||||
let info = serde_json::json!({
|
let info = serde_json::json!({
|
||||||
"tarball_hash": tarball_hash,
|
"nar_hash": nar_hash_hex,
|
||||||
"nar_hash": nar_hash,
|
|
||||||
"url": url,
|
"url": url,
|
||||||
});
|
});
|
||||||
|
|
||||||
let immutable = expected_nar_hash.is_some();
|
let immutable = expected_sha256.is_some();
|
||||||
metadata_cache
|
metadata_cache
|
||||||
.add(&input, &info, &store_path, immutable)
|
.add(&input, &info, &store_path, immutable)
|
||||||
.map_err(|e| NixError::from(e.to_string()))?;
|
.map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
|
|
||||||
Ok(FetchTarballResult {
|
Ok(FetchTarballResult {
|
||||||
store_path,
|
store_path,
|
||||||
hash: tarball_hash,
|
nar_hash: nar_hash_hex,
|
||||||
nar_hash,
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[op2]
|
#[op2]
|
||||||
#[serde]
|
|
||||||
pub fn op_fetch_git<Ctx: RuntimeContext>(
|
pub fn op_fetch_git<Ctx: RuntimeContext>(
|
||||||
state: &mut OpState,
|
state: &mut OpState,
|
||||||
#[string] url: String,
|
#[string] url: String,
|
||||||
@@ -298,10 +270,10 @@ pub fn op_fetch_git<Ctx: RuntimeContext>(
|
|||||||
submodules: bool,
|
submodules: bool,
|
||||||
all_refs: bool,
|
all_refs: bool,
|
||||||
#[string] name: Option<String>,
|
#[string] name: Option<String>,
|
||||||
) -> Result<FetchGitResult, NixError> {
|
) -> Result<FetchGitResult, NixRuntimeError> {
|
||||||
let _span = tracing::info_span!("op_fetch_git", url = %url).entered();
|
let _span = tracing::info_span!("op_fetch_git", url = %url).entered();
|
||||||
info!("fetchGit started");
|
info!("fetchGit started");
|
||||||
let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?;
|
let cache = FetcherCache::new().map_err(|e| NixRuntimeError::from(e.to_string()))?;
|
||||||
let dir_name = name.unwrap_or_else(|| "source".to_string());
|
let dir_name = name.unwrap_or_else(|| "source".to_string());
|
||||||
|
|
||||||
let ctx: &Ctx = state.get_ctx();
|
let ctx: &Ctx = state.get_ctx();
|
||||||
@@ -318,65 +290,16 @@ pub fn op_fetch_git<Ctx: RuntimeContext>(
|
|||||||
all_refs,
|
all_refs,
|
||||||
&dir_name,
|
&dir_name,
|
||||||
)
|
)
|
||||||
.map_err(|e| NixError::from(e.to_string()))
|
.map_err(|e| NixRuntimeError::from(e.to_string()))
|
||||||
}
|
|
||||||
|
|
||||||
#[op2]
|
|
||||||
#[serde]
|
|
||||||
pub fn op_fetch_hg(
|
|
||||||
#[string] url: String,
|
|
||||||
#[string] rev: Option<String>,
|
|
||||||
#[string] name: Option<String>,
|
|
||||||
) -> Result<FetchHgResult, NixError> {
|
|
||||||
let cache = FetcherCache::new().map_err(|e| NixError::from(e.to_string()))?;
|
|
||||||
let dir_name = name.unwrap_or_else(|| "source".to_string());
|
|
||||||
|
|
||||||
hg::fetch_hg(&cache, &url, rev.as_deref(), &dir_name).map_err(|e| NixError::from(e.to_string()))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn normalize_hash(hash: &str) -> String {
|
fn normalize_hash(hash: &str) -> String {
|
||||||
|
use base64::prelude::*;
|
||||||
if hash.starts_with("sha256-")
|
if hash.starts_with("sha256-")
|
||||||
&& let Some(b64) = hash.strip_prefix("sha256-")
|
&& let Some(b64) = hash.strip_prefix("sha256-")
|
||||||
&& let Ok(bytes) = base64_decode(b64)
|
&& let Ok(bytes) = BASE64_STANDARD.decode(b64)
|
||||||
{
|
{
|
||||||
return hex::encode(bytes);
|
return hex::encode(bytes);
|
||||||
}
|
}
|
||||||
hash.to_string()
|
hash.to_string()
|
||||||
}
|
}
|
||||||
|
|
||||||
fn base64_decode(input: &str) -> Result<Vec<u8>, String> {
|
|
||||||
const ALPHABET: &[u8] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
||||||
|
|
||||||
let input = input.trim_end_matches('=');
|
|
||||||
let mut output = Vec::with_capacity(input.len() * 3 / 4);
|
|
||||||
|
|
||||||
let mut buffer = 0u32;
|
|
||||||
let mut bits = 0;
|
|
||||||
|
|
||||||
for c in input.bytes() {
|
|
||||||
let value = ALPHABET
|
|
||||||
.iter()
|
|
||||||
.position(|&x| x == c)
|
|
||||||
.ok_or_else(|| format!("Invalid base64 character: {}", c as char))?;
|
|
||||||
|
|
||||||
buffer = (buffer << 6) | (value as u32);
|
|
||||||
bits += 6;
|
|
||||||
|
|
||||||
if bits >= 8 {
|
|
||||||
bits -= 8;
|
|
||||||
output.push((buffer >> bits) as u8);
|
|
||||||
buffer &= (1 << bits) - 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(output)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn register_ops<Ctx: RuntimeContext>() -> Vec<deno_core::OpDecl> {
|
|
||||||
vec![
|
|
||||||
op_fetch_url::<Ctx>(),
|
|
||||||
op_fetch_tarball::<Ctx>(),
|
|
||||||
op_fetch_git::<Ctx>(),
|
|
||||||
op_fetch_hg(),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::fs::{self, File};
|
use std::fs;
|
||||||
use std::io::Cursor;
|
use std::io::Cursor;
|
||||||
|
use std::os::unix::ffi::OsStrExt;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
|
|
||||||
use flate2::read::GzDecoder;
|
use flate2::read::GzDecoder;
|
||||||
@@ -10,7 +11,6 @@ pub enum ArchiveFormat {
|
|||||||
TarXz,
|
TarXz,
|
||||||
TarBz2,
|
TarBz2,
|
||||||
Tar,
|
Tar,
|
||||||
Zip,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ArchiveFormat {
|
impl ArchiveFormat {
|
||||||
@@ -27,9 +27,6 @@ impl ArchiveFormat {
|
|||||||
if url.ends_with(".tar") {
|
if url.ends_with(".tar") {
|
||||||
return ArchiveFormat::Tar;
|
return ArchiveFormat::Tar;
|
||||||
}
|
}
|
||||||
if url.ends_with(".zip") {
|
|
||||||
return ArchiveFormat::Zip;
|
|
||||||
}
|
|
||||||
|
|
||||||
if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
|
if data.len() >= 2 && data[0] == 0x1f && data[1] == 0x8b {
|
||||||
return ArchiveFormat::TarGz;
|
return ArchiveFormat::TarGz;
|
||||||
@@ -40,15 +37,12 @@ impl ArchiveFormat {
|
|||||||
if data.len() >= 3 && &data[0..3] == b"BZh" {
|
if data.len() >= 3 && &data[0..3] == b"BZh" {
|
||||||
return ArchiveFormat::TarBz2;
|
return ArchiveFormat::TarBz2;
|
||||||
}
|
}
|
||||||
if data.len() >= 4 && &data[0..4] == b"PK\x03\x04" {
|
|
||||||
return ArchiveFormat::Zip;
|
|
||||||
}
|
|
||||||
|
|
||||||
ArchiveFormat::TarGz
|
ArchiveFormat::TarGz
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn extract_archive(data: &[u8], dest: &Path) -> Result<PathBuf, ArchiveError> {
|
pub fn extract_tarball(data: &[u8], dest: &Path) -> Result<PathBuf, ArchiveError> {
|
||||||
let format = ArchiveFormat::detect("", data);
|
let format = ArchiveFormat::detect("", data);
|
||||||
|
|
||||||
let temp_dir = dest.join("_extract_temp");
|
let temp_dir = dest.join("_extract_temp");
|
||||||
@@ -59,7 +53,6 @@ pub fn extract_archive(data: &[u8], dest: &Path) -> Result<PathBuf, ArchiveError
|
|||||||
ArchiveFormat::TarXz => extract_tar_xz(data, &temp_dir)?,
|
ArchiveFormat::TarXz => extract_tar_xz(data, &temp_dir)?,
|
||||||
ArchiveFormat::TarBz2 => extract_tar_bz2(data, &temp_dir)?,
|
ArchiveFormat::TarBz2 => extract_tar_bz2(data, &temp_dir)?,
|
||||||
ArchiveFormat::Tar => extract_tar(data, &temp_dir)?,
|
ArchiveFormat::Tar => extract_tar(data, &temp_dir)?,
|
||||||
ArchiveFormat::Zip => extract_zip(data, &temp_dir)?,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
strip_single_toplevel(&temp_dir, dest)
|
strip_single_toplevel(&temp_dir, dest)
|
||||||
@@ -92,40 +85,10 @@ fn extract_tar(data: &[u8], dest: &Path) -> Result<(), ArchiveError> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn extract_zip(data: &[u8], dest: &Path) -> Result<(), ArchiveError> {
|
|
||||||
let cursor = Cursor::new(data);
|
|
||||||
let mut archive = zip::ZipArchive::new(cursor)?;
|
|
||||||
|
|
||||||
for i in 0..archive.len() {
|
|
||||||
let mut file = archive.by_index(i)?;
|
|
||||||
let outpath = dest.join(file.mangled_name());
|
|
||||||
|
|
||||||
if file.is_dir() {
|
|
||||||
fs::create_dir_all(&outpath)?;
|
|
||||||
} else {
|
|
||||||
if let Some(parent) = outpath.parent() {
|
|
||||||
fs::create_dir_all(parent)?;
|
|
||||||
}
|
|
||||||
let mut outfile = File::create(&outpath)?;
|
|
||||||
std::io::copy(&mut file, &mut outfile)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(unix)]
|
|
||||||
{
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
|
||||||
if let Some(mode) = file.unix_mode() {
|
|
||||||
fs::set_permissions(&outpath, fs::Permissions::from_mode(mode))?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn strip_single_toplevel(temp_dir: &Path, dest: &Path) -> Result<PathBuf, ArchiveError> {
|
fn strip_single_toplevel(temp_dir: &Path, dest: &Path) -> Result<PathBuf, ArchiveError> {
|
||||||
let entries: Vec<_> = fs::read_dir(temp_dir)?
|
let entries: Vec<_> = fs::read_dir(temp_dir)?
|
||||||
.filter_map(|e| e.ok())
|
.filter_map(|e| e.ok())
|
||||||
.filter(|e| !e.file_name().to_string_lossy().starts_with('.'))
|
.filter(|e| e.file_name().as_os_str().as_bytes()[0] != b'.')
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let source_dir = if entries.len() == 1 && entries[0].file_type()?.is_dir() {
|
let source_dir = if entries.len() == 1 && entries[0].file_type()?.is_dir() {
|
||||||
@@ -182,17 +145,21 @@ fn copy_dir_recursive(src: &Path, dst: &Path) -> Result<(), std::io::Error> {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn extract_tarball_to_temp(data: &[u8]) -> Result<(PathBuf, tempfile::TempDir), ArchiveError> {
|
||||||
|
let temp_dir = tempfile::tempdir()?;
|
||||||
|
let extracted_path = extract_tarball(data, temp_dir.path())?;
|
||||||
|
Ok((extracted_path, temp_dir))
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum ArchiveError {
|
pub enum ArchiveError {
|
||||||
IoError(std::io::Error),
|
IoError(std::io::Error),
|
||||||
ZipError(zip::result::ZipError),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl std::fmt::Display for ArchiveError {
|
impl std::fmt::Display for ArchiveError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
match self {
|
match self {
|
||||||
ArchiveError::IoError(e) => write!(f, "I/O error: {}", e),
|
ArchiveError::IoError(e) => write!(f, "I/O error: {}", e),
|
||||||
ArchiveError::ZipError(e) => write!(f, "ZIP error: {}", e),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -204,9 +171,3 @@ impl From<std::io::Error> for ArchiveError {
|
|||||||
ArchiveError::IoError(e)
|
ArchiveError::IoError(e)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<zip::result::ZipError> for ArchiveError {
|
|
||||||
fn from(e: zip::result::ZipError) -> Self {
|
|
||||||
ArchiveError::ZipError(e)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
29
fix/src/fetcher/cache.rs
Normal file
29
fix/src/fetcher/cache.rs
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
use std::fs;
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct FetcherCache {
|
||||||
|
base_dir: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl FetcherCache {
|
||||||
|
pub fn new() -> Result<Self, std::io::Error> {
|
||||||
|
let base_dir = dirs::cache_dir()
|
||||||
|
.unwrap_or_else(|| PathBuf::from("/tmp"))
|
||||||
|
.join("fix")
|
||||||
|
.join("fetchers");
|
||||||
|
|
||||||
|
fs::create_dir_all(&base_dir)?;
|
||||||
|
|
||||||
|
Ok(Self { base_dir })
|
||||||
|
}
|
||||||
|
|
||||||
|
fn git_cache_dir(&self) -> PathBuf {
|
||||||
|
self.base_dir.join("git")
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn get_git_bare(&self, url: &str) -> PathBuf {
|
||||||
|
let key = crate::nix_utils::sha256_hex(url.as_bytes());
|
||||||
|
self.git_cache_dir().join(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
use reqwest::blocking::Client;
|
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
|
|
||||||
|
use reqwest::blocking::Client;
|
||||||
|
|
||||||
pub struct Downloader {
|
pub struct Downloader {
|
||||||
client: Client,
|
client: Client,
|
||||||
}
|
}
|
||||||
@@ -31,8 +31,10 @@ pub fn fetch_git(
|
|||||||
let temp_dir = tempfile::tempdir()?;
|
let temp_dir = tempfile::tempdir()?;
|
||||||
let checkout_dir = checkout_rev_to_temp(&bare_repo, &target_rev, submodules, temp_dir.path())?;
|
let checkout_dir = checkout_rev_to_temp(&bare_repo, &target_rev, submodules, temp_dir.path())?;
|
||||||
|
|
||||||
let nar_hash = crate::nar::compute_nar_hash(&checkout_dir)
|
let nar_hash = hex::encode(
|
||||||
.map_err(|e| GitError::NarHashError(e.to_string()))?;
|
crate::nar::compute_nar_hash(&checkout_dir)
|
||||||
|
.map_err(|e| GitError::NarHashError(e.to_string()))?,
|
||||||
|
);
|
||||||
|
|
||||||
let store_path = store
|
let store_path = store
|
||||||
.add_to_store_from_path(name, &checkout_dir, vec![])
|
.add_to_store_from_path(name, &checkout_dir, vec![])
|
||||||
@@ -1,9 +1,10 @@
|
|||||||
#![allow(dead_code)]
|
#![allow(dead_code)]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
use std::time::{SystemTime, UNIX_EPOCH};
|
||||||
|
|
||||||
use rusqlite::{Connection, OptionalExtension, params};
|
use rusqlite::{Connection, OptionalExtension, params};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::path::PathBuf;
|
|
||||||
use std::time::{SystemTime, UNIX_EPOCH};
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub enum CacheError {
|
pub enum CacheError {
|
||||||
688
fix/src/ir.rs
Normal file
688
fix/src/ir.rs
Normal file
@@ -0,0 +1,688 @@
|
|||||||
|
use std::{
|
||||||
|
hash::{Hash, Hasher},
|
||||||
|
ops::Deref,
|
||||||
|
};
|
||||||
|
|
||||||
|
use bumpalo::{Bump, boxed::Box, collections::Vec};
|
||||||
|
use gc_arena::Collect;
|
||||||
|
use ghost_cell::{GhostCell, GhostToken};
|
||||||
|
use rnix::{TextRange, ast};
|
||||||
|
use string_interner::symbol::SymbolU32;
|
||||||
|
|
||||||
|
pub type HashMap<'ir, K, V> = hashbrown::HashMap<K, V, hashbrown::DefaultHashBuilder, &'ir Bump>;
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct IrRef<'id, 'ir>(&'ir GhostCell<'id, Ir<'ir, Self>>);
|
||||||
|
|
||||||
|
impl<'id, 'ir> IrRef<'id, 'ir> {
|
||||||
|
pub fn new(ir: &'ir GhostCell<'id, Ir<'ir, Self>>) -> Self {
|
||||||
|
Self(ir)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn alloc(bump: &'ir Bump, ir: Ir<'ir, Self>) -> Self {
|
||||||
|
Self(bump.alloc(GhostCell::new(ir)))
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn borrow<'a>(&'a self, token: &'a GhostToken<'id>) -> &'a Ir<'ir, Self> {
|
||||||
|
self.0.borrow(token)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Freeze a mutable IR reference into a read-only one, consuming the
|
||||||
|
/// `GhostToken` to prevent any further mutation.
|
||||||
|
///
|
||||||
|
/// # Safety
|
||||||
|
/// The transmute is sound because:
|
||||||
|
/// - `GhostCell<'id, T>` is `#[repr(transparent)]` over `T`
|
||||||
|
/// - `IrRef<'id, 'ir>` is `#[repr(transparent)]` over
|
||||||
|
/// `&'ir GhostCell<'id, Ir<'ir, Self>>`
|
||||||
|
/// - `RawIrRef<'ir>` is `#[repr(transparent)]` over `&'ir Ir<'ir, Self>`
|
||||||
|
/// - `Ir<'ir, Ref>` is `#[repr(C)]` and both ref types are pointer-sized
|
||||||
|
///
|
||||||
|
/// Consuming the `GhostToken` guarantees no `borrow_mut` calls can occur
|
||||||
|
/// afterwards, so the shared `&Ir` references from `RawIrRef::Deref` can
|
||||||
|
/// never alias with mutable references.
|
||||||
|
pub fn freeze(self, _token: GhostToken<'id>) -> RawIrRef<'ir> {
|
||||||
|
unsafe { std::mem::transmute(self) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub struct RawIrRef<'ir>(&'ir Ir<'ir, Self>);
|
||||||
|
|
||||||
|
impl<'ir> Deref for RawIrRef<'ir> {
|
||||||
|
type Target = Ir<'ir, RawIrRef<'ir>>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(C)]
|
||||||
|
pub enum Ir<'ir, Ref> {
|
||||||
|
Int(i64),
|
||||||
|
Float(f64),
|
||||||
|
Bool(bool),
|
||||||
|
Null,
|
||||||
|
Str(Box<'ir, String>),
|
||||||
|
AttrSet {
|
||||||
|
stcs: HashMap<'ir, StringId, (Ref, TextRange)>,
|
||||||
|
dyns: Vec<'ir, (Ref, Ref, TextRange)>,
|
||||||
|
},
|
||||||
|
List {
|
||||||
|
items: Vec<'ir, Ref>,
|
||||||
|
},
|
||||||
|
Path(Ref),
|
||||||
|
ConcatStrings {
|
||||||
|
parts: Vec<'ir, Ref>,
|
||||||
|
force_string: bool,
|
||||||
|
},
|
||||||
|
|
||||||
|
// OPs
|
||||||
|
UnOp {
|
||||||
|
rhs: Ref,
|
||||||
|
kind: UnOpKind,
|
||||||
|
},
|
||||||
|
BinOp {
|
||||||
|
lhs: Ref,
|
||||||
|
rhs: Ref,
|
||||||
|
kind: BinOpKind,
|
||||||
|
},
|
||||||
|
HasAttr {
|
||||||
|
lhs: Ref,
|
||||||
|
rhs: Vec<'ir, Attr<Ref>>,
|
||||||
|
},
|
||||||
|
Select {
|
||||||
|
expr: Ref,
|
||||||
|
attrpath: Vec<'ir, Attr<Ref>>,
|
||||||
|
default: Option<Ref>,
|
||||||
|
span: TextRange,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Conditionals
|
||||||
|
If {
|
||||||
|
cond: Ref,
|
||||||
|
consq: Ref,
|
||||||
|
alter: Ref,
|
||||||
|
},
|
||||||
|
Assert {
|
||||||
|
assertion: Ref,
|
||||||
|
expr: Ref,
|
||||||
|
assertion_raw: String,
|
||||||
|
span: TextRange,
|
||||||
|
},
|
||||||
|
|
||||||
|
With {
|
||||||
|
namespace: Ref,
|
||||||
|
body: Ref,
|
||||||
|
thunks: Vec<'ir, (ThunkId, Ref)>,
|
||||||
|
},
|
||||||
|
WithLookup(StringId),
|
||||||
|
|
||||||
|
// Function related
|
||||||
|
Func {
|
||||||
|
body: Ref,
|
||||||
|
param: Option<Param<'ir>>,
|
||||||
|
arg: ArgId,
|
||||||
|
thunks: Vec<'ir, (ThunkId, Ref)>,
|
||||||
|
},
|
||||||
|
Arg(ArgId),
|
||||||
|
Call {
|
||||||
|
func: Ref,
|
||||||
|
arg: Ref,
|
||||||
|
span: TextRange,
|
||||||
|
},
|
||||||
|
|
||||||
|
// Builtins
|
||||||
|
Builtins,
|
||||||
|
Builtin(StringId),
|
||||||
|
|
||||||
|
// Misc
|
||||||
|
TopLevel {
|
||||||
|
body: Ref,
|
||||||
|
thunks: Vec<'ir, (ThunkId, Ref)>,
|
||||||
|
},
|
||||||
|
Thunk(ThunkId),
|
||||||
|
CurPos(TextRange),
|
||||||
|
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, Collect)]
|
||||||
|
#[collect(require_static)]
|
||||||
|
pub struct StringId(pub SymbolU32);
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct ArgId(pub u32);
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||||
|
pub struct SpanId(pub u32);
|
||||||
|
|
||||||
|
/// Represents a key in an attribute path.
|
||||||
|
#[allow(unused)]
|
||||||
|
#[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 (`//`)
|
||||||
|
|
||||||
|
// Not standard, but part of rnix AST
|
||||||
|
PipeL,
|
||||||
|
PipeR,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl From<ast::BinOpKind> for BinOpKind {
|
||||||
|
fn from(op: ast::BinOpKind) -> Self {
|
||||||
|
use BinOpKind::*;
|
||||||
|
use ast::BinOpKind as kind;
|
||||||
|
match op {
|
||||||
|
kind::Concat => Con,
|
||||||
|
kind::Update => Upd,
|
||||||
|
kind::Add => Add,
|
||||||
|
kind::Sub => Sub,
|
||||||
|
kind::Mul => Mul,
|
||||||
|
kind::Div => Div,
|
||||||
|
kind::And => And,
|
||||||
|
kind::Equal => Eq,
|
||||||
|
kind::Implication => Impl,
|
||||||
|
kind::Less => Lt,
|
||||||
|
kind::LessOrEq => Leq,
|
||||||
|
kind::More => Gt,
|
||||||
|
kind::MoreOrEq => Geq,
|
||||||
|
kind::NotEqual => Neq,
|
||||||
|
kind::Or => Or,
|
||||||
|
kind::PipeLeft => PipeL,
|
||||||
|
kind::PipeRight => PipeR,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 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,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy)]
|
||||||
|
pub(crate) struct IrKey<'id, 'ir, 'a>(pub IrRef<'id, 'ir>, pub &'a GhostToken<'id>);
|
||||||
|
|
||||||
|
impl std::hash::Hash for IrKey<'_, '_, '_> {
|
||||||
|
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||||
|
ir_content_hash(self.0, self.1, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl PartialEq for IrKey<'_, '_, '_> {
|
||||||
|
fn eq(&self, other: &Self) -> bool {
|
||||||
|
ir_content_eq(self.0, other.0, self.1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Eq for IrKey<'_, '_, '_> {}
|
||||||
|
|
||||||
|
fn attr_content_hash<'id>(
|
||||||
|
attr: &Attr<IrRef<'id, '_>>,
|
||||||
|
token: &GhostToken<'id>,
|
||||||
|
state: &mut impl Hasher,
|
||||||
|
) {
|
||||||
|
core::mem::discriminant(attr).hash(state);
|
||||||
|
match attr {
|
||||||
|
Attr::Dynamic(expr, _) => ir_content_hash(*expr, token, state),
|
||||||
|
Attr::Str(sym, _) => sym.hash(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn attr_content_eq<'id, 'ir>(
|
||||||
|
a: &Attr<IrRef<'id, 'ir>>,
|
||||||
|
b: &Attr<IrRef<'id, 'ir>>,
|
||||||
|
token: &GhostToken<'id>,
|
||||||
|
) -> bool {
|
||||||
|
match (a, b) {
|
||||||
|
(Attr::Dynamic(ae, _), Attr::Dynamic(be, _)) => ir_content_eq(*ae, *be, token),
|
||||||
|
(Attr::Str(a, _), Attr::Str(b, _)) => a == b,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn param_content_hash(param: &Param<'_>, state: &mut impl Hasher) {
|
||||||
|
param.required.len().hash(state);
|
||||||
|
for (sym, _) in param.required.iter() {
|
||||||
|
sym.hash(state);
|
||||||
|
}
|
||||||
|
param.optional.len().hash(state);
|
||||||
|
for (sym, _) in param.optional.iter() {
|
||||||
|
sym.hash(state);
|
||||||
|
}
|
||||||
|
param.ellipsis.hash(state);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn param_content_eq(a: &Param<'_>, b: &Param<'_>) -> bool {
|
||||||
|
a.ellipsis == b.ellipsis
|
||||||
|
&& a.required.len() == b.required.len()
|
||||||
|
&& a.optional.len() == b.optional.len()
|
||||||
|
&& a.required
|
||||||
|
.iter()
|
||||||
|
.zip(b.required.iter())
|
||||||
|
.all(|((a, _), (b, _))| a == b)
|
||||||
|
&& a.optional
|
||||||
|
.iter()
|
||||||
|
.zip(b.optional.iter())
|
||||||
|
.all(|((a, _), (b, _))| a == b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thunks_content_hash<'id>(
|
||||||
|
thunks: &[(ThunkId, IrRef<'id, '_>)],
|
||||||
|
token: &GhostToken<'id>,
|
||||||
|
state: &mut impl Hasher,
|
||||||
|
) {
|
||||||
|
thunks.len().hash(state);
|
||||||
|
for &(id, ir) in thunks {
|
||||||
|
id.hash(state);
|
||||||
|
ir_content_hash(ir, token, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn thunks_content_eq<'id, 'ir>(
|
||||||
|
a: &[(ThunkId, IrRef<'id, 'ir>)],
|
||||||
|
b: &[(ThunkId, IrRef<'id, 'ir>)],
|
||||||
|
token: &GhostToken<'id>,
|
||||||
|
) -> bool {
|
||||||
|
a.len() == b.len()
|
||||||
|
&& a.iter()
|
||||||
|
.zip(b.iter())
|
||||||
|
.all(|(&(ai, ae), &(bi, be))| ai == bi && ir_content_eq(ae, be, token))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn ir_content_hash<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>, state: &mut impl Hasher) {
|
||||||
|
let ir = ir.borrow(token);
|
||||||
|
core::mem::discriminant(ir).hash(state);
|
||||||
|
match ir {
|
||||||
|
Ir::Int(x) => x.hash(state),
|
||||||
|
Ir::Float(x) => x.to_bits().hash(state),
|
||||||
|
Ir::Bool(x) => x.hash(state),
|
||||||
|
Ir::Null => {}
|
||||||
|
Ir::Str(x) => x.hash(state),
|
||||||
|
Ir::AttrSet { stcs, dyns } => {
|
||||||
|
stcs.len().hash(state);
|
||||||
|
let mut combined: u64 = 0;
|
||||||
|
for (&key, &(val, _)) in stcs.iter() {
|
||||||
|
let mut h = std::hash::DefaultHasher::new();
|
||||||
|
key.hash(&mut h);
|
||||||
|
ir_content_hash(val, token, &mut h);
|
||||||
|
combined = combined.wrapping_add(h.finish());
|
||||||
|
}
|
||||||
|
combined.hash(state);
|
||||||
|
dyns.len().hash(state);
|
||||||
|
for &(k, v, _) in dyns.iter() {
|
||||||
|
ir_content_hash(k, token, state);
|
||||||
|
ir_content_hash(v, token, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ir::List { items } => {
|
||||||
|
items.len().hash(state);
|
||||||
|
for &item in items.iter() {
|
||||||
|
ir_content_hash(item, token, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ir::HasAttr { lhs, rhs } => {
|
||||||
|
ir_content_hash(*lhs, token, state);
|
||||||
|
rhs.len().hash(state);
|
||||||
|
for attr in rhs.iter() {
|
||||||
|
attr_content_hash(attr, token, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&Ir::BinOp { lhs, rhs, kind } => {
|
||||||
|
ir_content_hash(lhs, token, state);
|
||||||
|
ir_content_hash(rhs, token, state);
|
||||||
|
kind.hash(state);
|
||||||
|
}
|
||||||
|
&Ir::UnOp { rhs, kind } => {
|
||||||
|
ir_content_hash(rhs, token, state);
|
||||||
|
kind.hash(state);
|
||||||
|
}
|
||||||
|
Ir::Select {
|
||||||
|
expr,
|
||||||
|
attrpath,
|
||||||
|
default,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
ir_content_hash(*expr, token, state);
|
||||||
|
attrpath.len().hash(state);
|
||||||
|
for attr in attrpath.iter() {
|
||||||
|
attr_content_hash(attr, token, state);
|
||||||
|
}
|
||||||
|
default.is_some().hash(state);
|
||||||
|
if let Some(d) = default {
|
||||||
|
ir_content_hash(*d, token, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&Ir::If { cond, consq, alter } => {
|
||||||
|
ir_content_hash(cond, token, state);
|
||||||
|
ir_content_hash(consq, token, state);
|
||||||
|
ir_content_hash(alter, token, state);
|
||||||
|
}
|
||||||
|
&Ir::Call { func, arg, .. } => {
|
||||||
|
ir_content_hash(func, token, state);
|
||||||
|
ir_content_hash(arg, token, state);
|
||||||
|
}
|
||||||
|
Ir::Assert {
|
||||||
|
assertion,
|
||||||
|
expr,
|
||||||
|
assertion_raw,
|
||||||
|
..
|
||||||
|
} => {
|
||||||
|
ir_content_hash(*assertion, token, state);
|
||||||
|
ir_content_hash(*expr, token, state);
|
||||||
|
assertion_raw.hash(state);
|
||||||
|
}
|
||||||
|
Ir::ConcatStrings {
|
||||||
|
force_string,
|
||||||
|
parts,
|
||||||
|
} => {
|
||||||
|
force_string.hash(state);
|
||||||
|
parts.len().hash(state);
|
||||||
|
for &part in parts.iter() {
|
||||||
|
ir_content_hash(part, token, state);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&Ir::Path(expr) => ir_content_hash(expr, token, state),
|
||||||
|
Ir::Func {
|
||||||
|
body,
|
||||||
|
arg,
|
||||||
|
param,
|
||||||
|
thunks,
|
||||||
|
} => {
|
||||||
|
ir_content_hash(*body, token, state);
|
||||||
|
arg.hash(state);
|
||||||
|
param.is_some().hash(state);
|
||||||
|
if let Some(p) = param {
|
||||||
|
param_content_hash(p, state);
|
||||||
|
}
|
||||||
|
thunks_content_hash(thunks, token, state);
|
||||||
|
}
|
||||||
|
Ir::TopLevel { body, thunks } => {
|
||||||
|
ir_content_hash(*body, token, state);
|
||||||
|
thunks_content_hash(thunks, token, state);
|
||||||
|
}
|
||||||
|
Ir::Arg(x) => x.hash(state),
|
||||||
|
Ir::Thunk(x) => x.hash(state),
|
||||||
|
Ir::Builtins => {}
|
||||||
|
Ir::Builtin(x) => x.hash(state),
|
||||||
|
Ir::CurPos(x) => x.hash(state),
|
||||||
|
Ir::ReplBinding(x) => x.hash(state),
|
||||||
|
Ir::ScopedImportBinding(x) => x.hash(state),
|
||||||
|
&Ir::With {
|
||||||
|
namespace,
|
||||||
|
body,
|
||||||
|
ref thunks,
|
||||||
|
} => {
|
||||||
|
ir_content_hash(namespace, token, state);
|
||||||
|
ir_content_hash(body, token, state);
|
||||||
|
thunks_content_hash(thunks, token, state);
|
||||||
|
}
|
||||||
|
Ir::WithLookup(x) => x.hash(state),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn ir_content_eq<'id, 'ir>(
|
||||||
|
a: IrRef<'id, 'ir>,
|
||||||
|
b: IrRef<'id, 'ir>,
|
||||||
|
token: &GhostToken<'id>,
|
||||||
|
) -> bool {
|
||||||
|
std::ptr::eq(a.0, b.0)
|
||||||
|
|| match (a.borrow(token), b.borrow(token)) {
|
||||||
|
(Ir::Int(a), Ir::Int(b)) => a == b,
|
||||||
|
(Ir::Float(a), Ir::Float(b)) => a.to_bits() == b.to_bits(),
|
||||||
|
(Ir::Bool(a), Ir::Bool(b)) => a == b,
|
||||||
|
(Ir::Null, Ir::Null) => true,
|
||||||
|
(Ir::Str(a), Ir::Str(b)) => **a == **b,
|
||||||
|
(
|
||||||
|
Ir::AttrSet {
|
||||||
|
stcs: a_stcs,
|
||||||
|
dyns: a_dyns,
|
||||||
|
},
|
||||||
|
Ir::AttrSet {
|
||||||
|
stcs: b_stcs,
|
||||||
|
dyns: b_dyns,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
a_stcs.len() == b_stcs.len()
|
||||||
|
&& a_dyns.len() == b_dyns.len()
|
||||||
|
&& a_stcs.iter().all(|(&k, &(av, _))| {
|
||||||
|
b_stcs
|
||||||
|
.get(&k)
|
||||||
|
.is_some_and(|&(bv, _)| ir_content_eq(av, bv, token))
|
||||||
|
})
|
||||||
|
&& a_dyns
|
||||||
|
.iter()
|
||||||
|
.zip(b_dyns.iter())
|
||||||
|
.all(|(&(ak, av, _), &(bk, bv, _))| {
|
||||||
|
ir_content_eq(ak, bk, token) && ir_content_eq(av, bv, token)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
(Ir::List { items: a }, Ir::List { items: b }) => {
|
||||||
|
a.len() == b.len()
|
||||||
|
&& a.iter()
|
||||||
|
.zip(b.iter())
|
||||||
|
.all(|(&a, &b)| ir_content_eq(a, b, token))
|
||||||
|
}
|
||||||
|
(Ir::HasAttr { lhs: al, rhs: ar }, Ir::HasAttr { lhs: bl, rhs: br }) => {
|
||||||
|
ir_content_eq(*al, *bl, token)
|
||||||
|
&& ar.len() == br.len()
|
||||||
|
&& ar
|
||||||
|
.iter()
|
||||||
|
.zip(br.iter())
|
||||||
|
.all(|(a, b)| attr_content_eq(a, b, token))
|
||||||
|
}
|
||||||
|
(
|
||||||
|
&Ir::BinOp {
|
||||||
|
lhs: al,
|
||||||
|
rhs: ar,
|
||||||
|
kind: ak,
|
||||||
|
},
|
||||||
|
&Ir::BinOp {
|
||||||
|
lhs: bl,
|
||||||
|
rhs: br,
|
||||||
|
kind: bk,
|
||||||
|
},
|
||||||
|
) => ak == bk && ir_content_eq(al, bl, token) && ir_content_eq(ar, br, token),
|
||||||
|
(&Ir::UnOp { rhs: ar, kind: ak }, &Ir::UnOp { rhs: br, kind: bk }) => {
|
||||||
|
ak == bk && ir_content_eq(ar, br, token)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Ir::Select {
|
||||||
|
expr: ae,
|
||||||
|
attrpath: aa,
|
||||||
|
default: ad,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
Ir::Select {
|
||||||
|
expr: be,
|
||||||
|
attrpath: ba,
|
||||||
|
default: bd,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
ir_content_eq(*ae, *be, token)
|
||||||
|
&& aa.len() == ba.len()
|
||||||
|
&& aa
|
||||||
|
.iter()
|
||||||
|
.zip(ba.iter())
|
||||||
|
.all(|(a, b)| attr_content_eq(a, b, token))
|
||||||
|
&& match (ad, bd) {
|
||||||
|
(Some(a), Some(b)) => ir_content_eq(*a, *b, token),
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(
|
||||||
|
&Ir::If {
|
||||||
|
cond: ac,
|
||||||
|
consq: acs,
|
||||||
|
alter: aa,
|
||||||
|
},
|
||||||
|
&Ir::If {
|
||||||
|
cond: bc,
|
||||||
|
consq: bcs,
|
||||||
|
alter: ba,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
ir_content_eq(ac, bc, token)
|
||||||
|
&& ir_content_eq(acs, bcs, token)
|
||||||
|
&& ir_content_eq(aa, ba, token)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
&Ir::Call {
|
||||||
|
func: af, arg: aa, ..
|
||||||
|
},
|
||||||
|
&Ir::Call {
|
||||||
|
func: bf, arg: ba, ..
|
||||||
|
},
|
||||||
|
) => ir_content_eq(af, bf, token) && ir_content_eq(aa, ba, token),
|
||||||
|
(
|
||||||
|
Ir::Assert {
|
||||||
|
assertion: aa,
|
||||||
|
expr: ae,
|
||||||
|
assertion_raw: ar,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
Ir::Assert {
|
||||||
|
assertion: ba,
|
||||||
|
expr: be,
|
||||||
|
assertion_raw: br,
|
||||||
|
..
|
||||||
|
},
|
||||||
|
) => ar == br && ir_content_eq(*aa, *ba, token) && ir_content_eq(*ae, *be, token),
|
||||||
|
(
|
||||||
|
Ir::ConcatStrings {
|
||||||
|
force_string: af,
|
||||||
|
parts: ap,
|
||||||
|
},
|
||||||
|
Ir::ConcatStrings {
|
||||||
|
force_string: bf,
|
||||||
|
parts: bp,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
af == bf
|
||||||
|
&& ap.len() == bp.len()
|
||||||
|
&& ap
|
||||||
|
.iter()
|
||||||
|
.zip(bp.iter())
|
||||||
|
.all(|(&a, &b)| ir_content_eq(a, b, token))
|
||||||
|
}
|
||||||
|
(&Ir::Path(a), &Ir::Path(b)) => ir_content_eq(a, b, token),
|
||||||
|
(
|
||||||
|
Ir::Func {
|
||||||
|
body: ab,
|
||||||
|
arg: aa,
|
||||||
|
param: ap,
|
||||||
|
thunks: at,
|
||||||
|
},
|
||||||
|
Ir::Func {
|
||||||
|
body: bb,
|
||||||
|
arg: ba,
|
||||||
|
param: bp,
|
||||||
|
thunks: bt,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
ir_content_eq(*ab, *bb, token)
|
||||||
|
&& aa == ba
|
||||||
|
&& match (ap, bp) {
|
||||||
|
(Some(a), Some(b)) => param_content_eq(a, b),
|
||||||
|
(None, None) => true,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
&& thunks_content_eq(at, bt, token)
|
||||||
|
}
|
||||||
|
(
|
||||||
|
Ir::TopLevel {
|
||||||
|
body: ab,
|
||||||
|
thunks: at,
|
||||||
|
},
|
||||||
|
Ir::TopLevel {
|
||||||
|
body: bb,
|
||||||
|
thunks: bt,
|
||||||
|
},
|
||||||
|
) => ir_content_eq(*ab, *bb, token) && thunks_content_eq(at, bt, token),
|
||||||
|
(Ir::Arg(a), Ir::Arg(b)) => a == b,
|
||||||
|
(Ir::Thunk(a), Ir::Thunk(b)) => a == b,
|
||||||
|
(Ir::Builtins, Ir::Builtins) => true,
|
||||||
|
(Ir::Builtin(a), Ir::Builtin(b)) => a == b,
|
||||||
|
(Ir::CurPos(a), Ir::CurPos(b)) => a == b,
|
||||||
|
(Ir::ReplBinding(a), Ir::ReplBinding(b)) => a == b,
|
||||||
|
(Ir::ScopedImportBinding(a), Ir::ScopedImportBinding(b)) => a == b,
|
||||||
|
(
|
||||||
|
Ir::With {
|
||||||
|
namespace: a_ns,
|
||||||
|
body: a_body,
|
||||||
|
thunks: a_thunks,
|
||||||
|
},
|
||||||
|
Ir::With {
|
||||||
|
namespace: b_ns,
|
||||||
|
body: b_body,
|
||||||
|
thunks: b_thunks,
|
||||||
|
},
|
||||||
|
) => {
|
||||||
|
ir_content_eq(*a_ns, *b_ns, token)
|
||||||
|
&& ir_content_eq(*a_body, *b_body, token)
|
||||||
|
&& thunks_content_eq(a_thunks, b_thunks, token)
|
||||||
|
}
|
||||||
|
(Ir::WithLookup(a), Ir::WithLookup(b)) => a == b,
|
||||||
|
_ => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,17 +1,21 @@
|
|||||||
#![warn(clippy::unwrap_used)]
|
#![warn(clippy::unwrap_used)]
|
||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
pub mod context;
|
|
||||||
pub mod error;
|
pub mod error;
|
||||||
pub mod logging;
|
pub mod logging;
|
||||||
|
pub mod runtime;
|
||||||
pub mod value;
|
pub mod value;
|
||||||
|
|
||||||
mod codegen;
|
mod codegen;
|
||||||
mod fetcher;
|
mod derivation;
|
||||||
|
mod disassembler;
|
||||||
|
mod downgrade;
|
||||||
|
// mod fetcher;
|
||||||
mod ir;
|
mod ir;
|
||||||
mod nar;
|
mod nar;
|
||||||
mod nix_hash;
|
mod nix_utils;
|
||||||
mod runtime;
|
|
||||||
mod store;
|
mod store;
|
||||||
|
mod string_context;
|
||||||
|
|
||||||
#[global_allocator]
|
#[global_allocator]
|
||||||
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
static GLOBAL: mimalloc::MiMalloc = mimalloc::MiMalloc;
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
use std::env;
|
use std::env;
|
||||||
use std::io::IsTerminal;
|
use std::io::IsTerminal;
|
||||||
|
|
||||||
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
use tracing_subscriber::{EnvFilter, Layer, fmt, layer::SubscriberExt, util::SubscriberInitExt};
|
||||||
|
|
||||||
pub fn init_logging() {
|
pub fn init_logging() {
|
||||||
152
fix/src/main.rs
Normal file
152
fix/src/main.rs
Normal file
@@ -0,0 +1,152 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
use std::process::exit;
|
||||||
|
|
||||||
|
use anyhow::Result;
|
||||||
|
use clap::{Args, Parser, Subcommand};
|
||||||
|
use fix::error::Source;
|
||||||
|
use fix::runtime::Runtime;
|
||||||
|
use hashbrown::HashSet;
|
||||||
|
use rustyline::DefaultEditor;
|
||||||
|
use rustyline::error::ReadlineError;
|
||||||
|
|
||||||
|
#[derive(Parser)]
|
||||||
|
#[command(name = "nix-js", about = "Nix expression evaluator")]
|
||||||
|
struct Cli {
|
||||||
|
#[command(subcommand)]
|
||||||
|
command: Command,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Subcommand)]
|
||||||
|
enum Command {
|
||||||
|
Compile {
|
||||||
|
#[clap(flatten)]
|
||||||
|
source: ExprSource,
|
||||||
|
#[arg(long)]
|
||||||
|
silent: bool,
|
||||||
|
},
|
||||||
|
Eval {
|
||||||
|
#[clap(flatten)]
|
||||||
|
source: ExprSource,
|
||||||
|
},
|
||||||
|
Repl,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Args)]
|
||||||
|
#[group(required = true, multiple = false)]
|
||||||
|
struct ExprSource {
|
||||||
|
#[clap(short, long)]
|
||||||
|
expr: Option<String>,
|
||||||
|
#[clap(short, long)]
|
||||||
|
file: Option<PathBuf>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
|
||||||
|
let src = if let Some(expr) = src.expr {
|
||||||
|
Source::new_eval(expr)?
|
||||||
|
} else if let Some(file) = src.file {
|
||||||
|
Source::new_file(file)?
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
todo!()
|
||||||
|
/* match runtime.compile_bytecode(src) {
|
||||||
|
Ok(compiled) => {
|
||||||
|
if !silent {
|
||||||
|
println!("{}", runtime.disassemble_colored(&compiled));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{:?}", miette::Report::new(*err));
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(()) */
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_eval(runtime: &mut Runtime, src: ExprSource) -> Result<()> {
|
||||||
|
let src = if let Some(expr) = src.expr {
|
||||||
|
Source::new_eval(expr)?
|
||||||
|
} else if let Some(file) = src.file {
|
||||||
|
Source::new_file(file)?
|
||||||
|
} else {
|
||||||
|
unreachable!()
|
||||||
|
};
|
||||||
|
match runtime.eval_deep(src) {
|
||||||
|
Ok(value) => {
|
||||||
|
println!("{}", value.display_compat());
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("{:?}", miette::Report::new(*err));
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_repl(runtime: &mut Runtime) -> Result<()> {
|
||||||
|
let mut rl = DefaultEditor::new()?;
|
||||||
|
let mut scope = HashSet::new();
|
||||||
|
const RE: ere::Regex<3> = ere::compile_regex!("^[ \t]*([a-zA-Z_][a-zA-Z0-9_'-]*)[ \t]*(.*)$");
|
||||||
|
loop {
|
||||||
|
let readline = rl.readline("nix-js-repl> ");
|
||||||
|
match readline {
|
||||||
|
Ok(line) => {
|
||||||
|
if line.trim().is_empty() {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
let _ = rl.add_history_entry(line.as_str());
|
||||||
|
if let Some([Some(_), Some(ident), Some(rest)]) = RE.exec(&line) {
|
||||||
|
if let Some(expr) = rest.strip_prefix('=') {
|
||||||
|
let expr = expr.trim_start();
|
||||||
|
if expr.is_empty() {
|
||||||
|
eprintln!("Error: missing expression after '='");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
match runtime.add_binding(ident, expr, &mut scope) {
|
||||||
|
Ok(value) => println!("{} = {}", ident, value),
|
||||||
|
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let src = Source::new_repl(line)?;
|
||||||
|
match runtime.eval_repl(src, &scope) {
|
||||||
|
Ok(value) => println!("{value}"),
|
||||||
|
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
let src = Source::new_repl(line)?;
|
||||||
|
match runtime.eval_repl(src, &scope) {
|
||||||
|
Ok(value) => println!("{value}"),
|
||||||
|
Err(err) => eprintln!("{:?}", miette::Report::new(*err)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(ReadlineError::Interrupted) => {
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
Err(ReadlineError::Eof) => {
|
||||||
|
println!("CTRL-D");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
Err(err) => {
|
||||||
|
eprintln!("Error: {err:?}");
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() -> Result<()> {
|
||||||
|
fix::logging::init_logging();
|
||||||
|
|
||||||
|
let cli = Cli::parse();
|
||||||
|
|
||||||
|
let mut runtime = Runtime::new()?;
|
||||||
|
|
||||||
|
match cli.command {
|
||||||
|
Command::Compile { source, silent } => run_compile(&mut runtime, source, silent),
|
||||||
|
Command::Eval { source } => run_eval(&mut runtime, source),
|
||||||
|
Command::Repl => run_repl(&mut runtime),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,18 +1,19 @@
|
|||||||
use nix_nar::Encoder;
|
|
||||||
use sha2::{Digest, Sha256};
|
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
|
use nix_nar::Encoder;
|
||||||
|
use sha2::{Digest, Sha256};
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
pub fn compute_nar_hash(path: &Path) -> Result<String> {
|
pub fn compute_nar_hash(path: &Path) -> Result<[u8; 32]> {
|
||||||
let mut hasher = Sha256::new();
|
let mut hasher = Sha256::new();
|
||||||
std::io::copy(
|
std::io::copy(
|
||||||
&mut Encoder::new(path).map_err(|err| Error::internal(err.to_string()))?,
|
&mut Encoder::new(path).map_err(|err| Error::internal(err.to_string()))?,
|
||||||
&mut hasher,
|
&mut hasher,
|
||||||
)
|
)
|
||||||
.map_err(|err| Error::internal(err.to_string()))?;
|
.map_err(|err| Error::internal(err.to_string()))?;
|
||||||
Ok(hex::encode(hasher.finalize()))
|
Ok(hasher.finalize().into())
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn pack_nar(path: &Path) -> Result<Vec<u8>> {
|
pub fn pack_nar(path: &Path) -> Result<Vec<u8>> {
|
||||||
@@ -27,17 +28,19 @@ pub fn pack_nar(path: &Path) -> Result<Vec<u8>> {
|
|||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
#[allow(clippy::unwrap_used)]
|
#[allow(clippy::unwrap_used)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
|
||||||
use std::fs;
|
use std::fs;
|
||||||
|
|
||||||
use tempfile::TempDir;
|
use tempfile::TempDir;
|
||||||
|
|
||||||
#[test]
|
use super::*;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
fn test_simple_file() {
|
fn test_simple_file() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
let file_path = temp.path().join("test.txt");
|
let file_path = temp.path().join("test.txt");
|
||||||
fs::write(&file_path, "hello").unwrap();
|
fs::write(&file_path, "hello").unwrap();
|
||||||
|
|
||||||
let hash = compute_nar_hash(&file_path).unwrap();
|
let hash = hex::encode(compute_nar_hash(&file_path).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hash,
|
hash,
|
||||||
"0a430879c266f8b57f4092a0f935cf3facd48bbccde5760d4748ca405171e969"
|
"0a430879c266f8b57f4092a0f935cf3facd48bbccde5760d4748ca405171e969"
|
||||||
@@ -46,13 +49,13 @@ mod tests {
|
|||||||
assert_eq!(hash.len(), 64);
|
assert_eq!(hash.len(), 64);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn test_directory() {
|
fn test_directory() {
|
||||||
let temp = TempDir::new().unwrap();
|
let temp = TempDir::new().unwrap();
|
||||||
fs::write(temp.path().join("a.txt"), "aaa").unwrap();
|
fs::write(temp.path().join("a.txt"), "aaa").unwrap();
|
||||||
fs::write(temp.path().join("b.txt"), "bbb").unwrap();
|
fs::write(temp.path().join("b.txt"), "bbb").unwrap();
|
||||||
|
|
||||||
let hash = compute_nar_hash(temp.path()).unwrap();
|
let hash = hex::encode(compute_nar_hash(temp.path()).unwrap());
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
hash,
|
hash,
|
||||||
"0036c14209749bc9b9631e2077b108b701c322ab53853cd26f2746268a86fc0f"
|
"0036c14209749bc9b9631e2077b108b701c322ab53853cd26f2746268a86fc0f"
|
||||||
21
fix/src/nix_utils.rs
Normal file
21
fix/src/nix_utils.rs
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
use nix_compat::store_path::compress_hash;
|
||||||
|
use sha2::{Digest as _, Sha256};
|
||||||
|
|
||||||
|
pub fn sha256_hex(data: &[u8]) -> String {
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(data);
|
||||||
|
hex::encode(hasher.finalize())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn make_store_path(store_dir: &str, ty: &str, hash_hex: &str, name: &str) -> String {
|
||||||
|
let s = format!("{}:sha256:{}:{}:{}", ty, hash_hex, store_dir, name);
|
||||||
|
|
||||||
|
let mut hasher = Sha256::new();
|
||||||
|
hasher.update(s.as_bytes());
|
||||||
|
let hash: [u8; 32] = hasher.finalize().into();
|
||||||
|
|
||||||
|
let compressed = compress_hash::<20>(&hash);
|
||||||
|
let encoded = nix_compat::nixbase32::encode(&compressed);
|
||||||
|
|
||||||
|
format!("{}/{}-{}", store_dir, encoded, name)
|
||||||
|
}
|
||||||
531
fix/src/runtime.rs
Normal file
531
fix/src/runtime.rs
Normal file
@@ -0,0 +1,531 @@
|
|||||||
|
use std::hash::BuildHasher;
|
||||||
|
|
||||||
|
use bumpalo::Bump;
|
||||||
|
use gc_arena::{Arena, Rootable, arena::CollectionPhase};
|
||||||
|
use ghost_cell::{GhostCell, GhostToken};
|
||||||
|
use hashbrown::{DefaultHashBuilder, HashMap, HashSet, HashTable};
|
||||||
|
use rnix::TextRange;
|
||||||
|
use string_interner::DefaultStringInterner;
|
||||||
|
|
||||||
|
use crate::codegen::{BytecodeContext, InstructionPtr};
|
||||||
|
use crate::downgrade::{Downgrade as _, DowngradeContext};
|
||||||
|
use crate::error::{Error, Result, Source};
|
||||||
|
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq};
|
||||||
|
use crate::runtime::builtins::new_builtins_env;
|
||||||
|
use crate::store::{DaemonStore, StoreConfig};
|
||||||
|
use crate::value::Symbol;
|
||||||
|
|
||||||
|
mod builtins;
|
||||||
|
mod stack;
|
||||||
|
mod value;
|
||||||
|
mod vm;
|
||||||
|
use vm::{Action, VM};
|
||||||
|
|
||||||
|
pub struct Runtime {
|
||||||
|
bytecode: Vec<u8>,
|
||||||
|
global_env: HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
|
||||||
|
sources: Vec<Source>,
|
||||||
|
store: DaemonStore,
|
||||||
|
spans: Vec<(usize, TextRange)>,
|
||||||
|
thunk_count: usize,
|
||||||
|
strings: DefaultStringInterner,
|
||||||
|
arena: Arena<Rootable![VM<'_>]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Runtime {
|
||||||
|
const COLLECTOR_GRANULARITY: f64 = 1024.0;
|
||||||
|
|
||||||
|
pub fn new() -> Result<Self> {
|
||||||
|
let mut strings = DefaultStringInterner::new();
|
||||||
|
let global_env = new_builtins_env(&mut strings);
|
||||||
|
|
||||||
|
let config = StoreConfig::from_env();
|
||||||
|
let store = DaemonStore::connect(&config.daemon_socket)?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
global_env,
|
||||||
|
store,
|
||||||
|
strings,
|
||||||
|
thunk_count: 0,
|
||||||
|
bytecode: Vec::new(),
|
||||||
|
sources: Vec::new(),
|
||||||
|
spans: Vec::new(),
|
||||||
|
arena: Arena::new(|mc| VM::new(mc)),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval(&mut self, source: Source) -> Result<crate::value::Value> {
|
||||||
|
let root = self.downgrade(source, None)?;
|
||||||
|
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
|
||||||
|
self.run(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_shallow(&mut self, _source: Source) -> Result<crate::value::Value> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_deep(&mut self, source: Source) -> Result<crate::value::Value> {
|
||||||
|
// FIXME: deep
|
||||||
|
let root = self.downgrade(source, None)?;
|
||||||
|
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
|
||||||
|
self.run(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn eval_repl(
|
||||||
|
&mut self,
|
||||||
|
source: Source,
|
||||||
|
scope: &HashSet<StringId>,
|
||||||
|
) -> Result<crate::value::Value> {
|
||||||
|
// FIXME: shallow
|
||||||
|
let root = self.downgrade(source, Some(Scope::Repl(scope)))?;
|
||||||
|
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
|
||||||
|
self.run(ip)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn add_binding(
|
||||||
|
&mut self,
|
||||||
|
_ident: &str,
|
||||||
|
_expr: &str,
|
||||||
|
_scope: &mut HashSet<StringId>,
|
||||||
|
) -> Result<crate::value::Value> {
|
||||||
|
todo!()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downgrade_ctx<'a, 'bump, 'id>(
|
||||||
|
&'a mut self,
|
||||||
|
bump: &'bump Bump,
|
||||||
|
token: GhostToken<'id>,
|
||||||
|
extra_scope: Option<Scope<'a>>,
|
||||||
|
) -> DowngradeCtx<'a, 'id, 'bump> {
|
||||||
|
let Runtime {
|
||||||
|
global_env,
|
||||||
|
sources,
|
||||||
|
thunk_count,
|
||||||
|
strings,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
DowngradeCtx {
|
||||||
|
bump,
|
||||||
|
token,
|
||||||
|
strings,
|
||||||
|
source: sources.last().unwrap().clone(),
|
||||||
|
scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope.into_iter()).collect(),
|
||||||
|
with_scope_count: 0,
|
||||||
|
arg_count: 0,
|
||||||
|
thunk_count,
|
||||||
|
thunk_scopes: vec![ThunkScope::new_in(bump)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn downgrade<'a>(&'a mut self, source: Source, extra_scope: Option<Scope<'a>>) -> Result<OwnedIr> {
|
||||||
|
tracing::debug!("Parsing Nix expression");
|
||||||
|
|
||||||
|
self.sources.push(source.clone());
|
||||||
|
|
||||||
|
let root = rnix::Root::parse(&source.src);
|
||||||
|
handle_parse_error(root.errors(), source).map_or(Ok(()), Err)?;
|
||||||
|
|
||||||
|
tracing::debug!("Downgrading Nix expression");
|
||||||
|
let expr = root
|
||||||
|
.tree()
|
||||||
|
.expr()
|
||||||
|
.ok_or_else(|| Error::parse_error("unexpected EOF".into()))?;
|
||||||
|
let bump = Bump::new();
|
||||||
|
GhostToken::new(|token| {
|
||||||
|
let ir = self
|
||||||
|
.downgrade_ctx(&bump, token, extra_scope)
|
||||||
|
.downgrade_toplevel(expr)?;
|
||||||
|
let ir = unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) };
|
||||||
|
Ok(OwnedIr { _bump: bump, ir })
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run(&mut self, ip: InstructionPtr) -> Result<crate::value::Value> {
|
||||||
|
let mut pc = ip.0;
|
||||||
|
loop {
|
||||||
|
let Runtime {
|
||||||
|
bytecode,
|
||||||
|
strings,
|
||||||
|
arena,
|
||||||
|
..
|
||||||
|
} = self;
|
||||||
|
let action =
|
||||||
|
arena.mutate_root(|mc, root| root.run_batch(bytecode, &mut pc, mc, strings));
|
||||||
|
match action {
|
||||||
|
Action::NeedGc => {
|
||||||
|
if self.arena.collection_phase() == CollectionPhase::Sweeping {
|
||||||
|
self.arena.collect_debt();
|
||||||
|
} else if let Some(marked) = self.arena.mark_debt() {
|
||||||
|
marked.start_sweeping();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Action::Done(done) => {
|
||||||
|
break done;
|
||||||
|
}
|
||||||
|
Action::Continue => (),
|
||||||
|
Action::IoRequest(_) => todo!(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_error_span(error: &rnix::ParseError) -> Option<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> {
|
||||||
|
bump: &'ir Bump,
|
||||||
|
token: GhostToken<'id>,
|
||||||
|
strings: &'ctx mut DefaultStringInterner,
|
||||||
|
source: Source,
|
||||||
|
scopes: Vec<Scope<'ctx>>,
|
||||||
|
with_scope_count: u32,
|
||||||
|
arg_count: u32,
|
||||||
|
thunk_count: &'ctx mut usize,
|
||||||
|
thunk_scopes: Vec<ThunkScope<'id, 'ir>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn should_thunk<'id>(ir: IrRef<'id, '_>, token: &GhostToken<'id>) -> bool {
|
||||||
|
!matches!(
|
||||||
|
ir.borrow(token),
|
||||||
|
Ir::Builtin(_)
|
||||||
|
| Ir::Builtins
|
||||||
|
| Ir::Int(_)
|
||||||
|
| Ir::Float(_)
|
||||||
|
| Ir::Bool(_)
|
||||||
|
| Ir::Null
|
||||||
|
| Ir::Str(_)
|
||||||
|
| Ir::Thunk(_)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx, 'id, 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
|
||||||
|
fn new(
|
||||||
|
bump: &'ir Bump,
|
||||||
|
token: GhostToken<'id>,
|
||||||
|
symbols: &'ctx mut DefaultStringInterner,
|
||||||
|
global: &'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>,
|
||||||
|
extra_scope: Option<Scope<'ctx>>,
|
||||||
|
thunk_count: &'ctx mut usize,
|
||||||
|
source: Source,
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
bump,
|
||||||
|
token,
|
||||||
|
strings: symbols,
|
||||||
|
source,
|
||||||
|
scopes: std::iter::once(Scope::Global(global))
|
||||||
|
.chain(extra_scope)
|
||||||
|
.collect(),
|
||||||
|
thunk_count,
|
||||||
|
arg_count: 0,
|
||||||
|
with_scope_count: 0,
|
||||||
|
thunk_scopes: vec![ThunkScope::new_in(bump)],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'ctx: 'ir, 'id, 'ir> DowngradeContext<'id, 'ir> for DowngradeCtx<'ctx, 'id, 'ir> {
|
||||||
|
fn new_expr(&self, expr: Ir<'ir, IrRef<'id, 'ir>>) -> IrRef<'id, 'ir> {
|
||||||
|
IrRef::new(self.bump.alloc(GhostCell::new(expr)))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_arg(&mut self) -> ArgId {
|
||||||
|
self.arg_count += 1;
|
||||||
|
ArgId(self.arg_count - 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn maybe_thunk(&mut self, ir: IrRef<'id, 'ir>) -> IrRef<'id, 'ir> {
|
||||||
|
if !should_thunk(ir, &self.token) {
|
||||||
|
return ir;
|
||||||
|
}
|
||||||
|
|
||||||
|
let cached = self
|
||||||
|
.thunk_scopes
|
||||||
|
.last()
|
||||||
|
.expect("no active cache scope")
|
||||||
|
.lookup_cache(ir, &self.token);
|
||||||
|
|
||||||
|
if let Some(id) = cached {
|
||||||
|
return IrRef::alloc(self.bump, Ir::Thunk(id));
|
||||||
|
}
|
||||||
|
|
||||||
|
let id = ThunkId(*self.thunk_count);
|
||||||
|
*self.thunk_count = self.thunk_count.checked_add(1).expect("thunk id overflow");
|
||||||
|
self.thunk_scopes
|
||||||
|
.last_mut()
|
||||||
|
.expect("no active cache scope")
|
||||||
|
.add_binding(id, ir, &self.token);
|
||||||
|
IrRef::alloc(self.bump, Ir::Thunk(id))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new_sym(&mut self, sym: String) -> StringId {
|
||||||
|
StringId(self.strings.get_or_intern(sym))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_sym(&self, id: StringId) -> Symbol<'_> {
|
||||||
|
self.strings.resolve(id.0).expect("no symbol found").into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup(&self, sym: StringId, span: TextRange) -> Result<IrRef<'id, 'ir>> {
|
||||||
|
for scope in self.scopes.iter().rev() {
|
||||||
|
match scope {
|
||||||
|
&Scope::Global(global_scope) => {
|
||||||
|
if let Some(expr) = global_scope.get(&sym) {
|
||||||
|
let ir = match expr {
|
||||||
|
Ir::Builtins => Ir::Builtins,
|
||||||
|
Ir::Builtin(s) => Ir::Builtin(*s),
|
||||||
|
Ir::Bool(b) => Ir::Bool(*b),
|
||||||
|
Ir::Null => Ir::Null,
|
||||||
|
_ => unreachable!("globals should only contain leaf IR nodes"),
|
||||||
|
};
|
||||||
|
return Ok(self.new_expr(ir));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&Scope::Repl(repl_bindings) => {
|
||||||
|
if repl_bindings.contains(&sym) {
|
||||||
|
return Ok(self.new_expr(Ir::ReplBinding(sym)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scope::ScopedImport(scoped_bindings) => {
|
||||||
|
if scoped_bindings.contains(&sym) {
|
||||||
|
return Ok(self.new_expr(Ir::ScopedImportBinding(sym)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Scope::Let(let_scope) => {
|
||||||
|
if let Some(&expr) = let_scope.get(&sym) {
|
||||||
|
return Ok(self.new_expr(Ir::Thunk(expr)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
&Scope::Param(param_sym, id) => {
|
||||||
|
if param_sym == sym {
|
||||||
|
return Ok(self.new_expr(Ir::Arg(id)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.with_scope_count > 0 {
|
||||||
|
Ok(self.new_expr(Ir::WithLookup(sym)))
|
||||||
|
} else {
|
||||||
|
Err(Error::downgrade_error(
|
||||||
|
format!("'{}' not found", self.get_sym(sym)),
|
||||||
|
self.get_current_source(),
|
||||||
|
span,
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_current_source(&self) -> Source {
|
||||||
|
self.source.clone()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_let_scope<F, R>(&mut self, keys: &[StringId], f: F) -> Result<R>
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> Result<(bumpalo::collections::Vec<'ir, IrRef<'id, 'ir>>, R)>,
|
||||||
|
{
|
||||||
|
let base = *self.thunk_count;
|
||||||
|
*self.thunk_count = self
|
||||||
|
.thunk_count
|
||||||
|
.checked_add(keys.len())
|
||||||
|
.expect("thunk id overflow");
|
||||||
|
let iter = keys.iter().enumerate().map(|(offset, &key)| {
|
||||||
|
(
|
||||||
|
key,
|
||||||
|
ThunkId(unsafe { base.checked_add(offset).unwrap_unchecked() }),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
self.scopes.push(Scope::Let(iter.collect()));
|
||||||
|
let (vals, ret) = {
|
||||||
|
let mut guard = ScopeGuard { ctx: self };
|
||||||
|
f(guard.as_ctx())?
|
||||||
|
};
|
||||||
|
assert_eq!(keys.len(), vals.len());
|
||||||
|
let scope = self.thunk_scopes.last_mut().expect("no active thunk scope");
|
||||||
|
scope.extend_bindings((base..base + keys.len()).map(ThunkId).zip(vals));
|
||||||
|
Ok(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_param_scope<F, R>(&mut self, param: StringId, arg: ArgId, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> R,
|
||||||
|
{
|
||||||
|
self.scopes.push(Scope::Param(param, arg));
|
||||||
|
let mut guard = ScopeGuard { ctx: self };
|
||||||
|
f(guard.as_ctx())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_with_scope<F, R>(&mut self, f: F) -> R
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> R,
|
||||||
|
{
|
||||||
|
self.with_scope_count += 1;
|
||||||
|
let ret = f(self);
|
||||||
|
self.with_scope_count -= 1;
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
fn with_thunk_scope<F, R>(
|
||||||
|
&mut self,
|
||||||
|
f: F,
|
||||||
|
) -> (
|
||||||
|
R,
|
||||||
|
bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
|
||||||
|
)
|
||||||
|
where
|
||||||
|
F: FnOnce(&mut Self) -> R,
|
||||||
|
{
|
||||||
|
self.thunk_scopes.push(ThunkScope::new_in(self.bump));
|
||||||
|
let ret = f(self);
|
||||||
|
(
|
||||||
|
ret,
|
||||||
|
self.thunk_scopes
|
||||||
|
.pop()
|
||||||
|
.expect("no thunk scope left???")
|
||||||
|
.bindings,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn bump(&self) -> &'ir bumpalo::Bump {
|
||||||
|
self.bump
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'id, 'ir, 'ctx: 'ir> DowngradeCtx<'ctx, 'id, 'ir> {
|
||||||
|
fn downgrade_toplevel(mut self, root: rnix::ast::Expr) -> Result<RawIrRef<'ir>> {
|
||||||
|
let body = root.downgrade(&mut self)?;
|
||||||
|
let thunks = self
|
||||||
|
.thunk_scopes
|
||||||
|
.pop()
|
||||||
|
.expect("no thunk scope left???")
|
||||||
|
.bindings;
|
||||||
|
let ir = IrRef::alloc(self.bump, Ir::TopLevel { body, thunks });
|
||||||
|
Ok(ir.freeze(self.token))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ThunkScope<'id, 'ir> {
|
||||||
|
bindings: bumpalo::collections::Vec<'ir, (ThunkId, IrRef<'id, 'ir>)>,
|
||||||
|
cache: HashTable<(IrRef<'id, 'ir>, ThunkId)>,
|
||||||
|
hasher: DefaultHashBuilder,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'id, 'ir> ThunkScope<'id, 'ir> {
|
||||||
|
fn new_in(bump: &'ir Bump) -> Self {
|
||||||
|
Self {
|
||||||
|
bindings: bumpalo::collections::Vec::new_in(bump),
|
||||||
|
cache: HashTable::new(),
|
||||||
|
hasher: DefaultHashBuilder::default(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lookup_cache(&self, key: IrRef<'id, 'ir>, token: &GhostToken<'id>) -> Option<ThunkId> {
|
||||||
|
let hash = self.hasher.hash_one(IrKey(key, token));
|
||||||
|
self.cache
|
||||||
|
.find(hash, |&(ir, _)| ir_content_eq(key, ir, token))
|
||||||
|
.map(|&(_, id)| id)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn add_binding(&mut self, id: ThunkId, ir: IrRef<'id, 'ir>, token: &GhostToken<'id>) {
|
||||||
|
self.bindings.push((id, ir));
|
||||||
|
let hash = self.hasher.hash_one(IrKey(ir, token));
|
||||||
|
self.cache.insert_unique(hash, (ir, id), |&(ir, _)| {
|
||||||
|
self.hasher.hash_one(IrKey(ir, token))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn extend_bindings(&mut self, iter: impl IntoIterator<Item = (ThunkId, IrRef<'id, 'ir>)>) {
|
||||||
|
self.bindings.extend(iter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
enum Scope<'ctx> {
|
||||||
|
Global(&'ctx HashMap<StringId, Ir<'static, RawIrRef<'static>>>),
|
||||||
|
Repl(&'ctx HashSet<StringId>),
|
||||||
|
ScopedImport(HashSet<StringId>),
|
||||||
|
Let(HashMap<StringId, ThunkId>),
|
||||||
|
Param(StringId, ArgId),
|
||||||
|
}
|
||||||
|
|
||||||
|
struct ScopeGuard<'a, 'ctx, 'id, 'ir> {
|
||||||
|
ctx: &'a mut DowngradeCtx<'ctx, 'id, 'ir>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Drop for ScopeGuard<'_, '_, '_, '_> {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
self.ctx.scopes.pop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'id, 'ir, 'ctx> ScopeGuard<'_, 'ctx, 'id, 'ir> {
|
||||||
|
fn as_ctx(&mut self) -> &mut DowngradeCtx<'ctx, 'id, 'ir> {
|
||||||
|
self.ctx
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct OwnedIr {
|
||||||
|
_bump: Bump,
|
||||||
|
ir: RawIrRef<'static>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl OwnedIr {
|
||||||
|
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
|
||||||
|
Self {
|
||||||
|
_bump: bump,
|
||||||
|
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_ref(&self) -> RawIrRef<'_> {
|
||||||
|
self.ir
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BytecodeContext for Runtime {
|
||||||
|
fn intern_string(&mut self, s: &str) -> StringId {
|
||||||
|
StringId(self.strings.get_or_intern(s))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn register_span(&mut self, range: TextRange) -> u32 {
|
||||||
|
let id = self.spans.len();
|
||||||
|
let source_id = self
|
||||||
|
.sources
|
||||||
|
.len()
|
||||||
|
.checked_sub(1)
|
||||||
|
.expect("current_source not set");
|
||||||
|
self.spans.push((source_id, range));
|
||||||
|
id as u32
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_code(&self) -> &[u8] {
|
||||||
|
&self.bytecode
|
||||||
|
}
|
||||||
|
|
||||||
|
fn get_code_mut(&mut self) -> &mut Vec<u8> {
|
||||||
|
&mut self.bytecode
|
||||||
|
}
|
||||||
|
}
|
||||||
51
fix/src/runtime/builtins.rs
Normal file
51
fix/src/runtime/builtins.rs
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
use hashbrown::HashMap;
|
||||||
|
use string_interner::DefaultStringInterner;
|
||||||
|
|
||||||
|
use crate::ir::{Ir, RawIrRef, StringId};
|
||||||
|
|
||||||
|
pub(super) fn new_builtins_env(
|
||||||
|
interner: &mut DefaultStringInterner,
|
||||||
|
) -> HashMap<StringId, Ir<'static, RawIrRef<'static>>> {
|
||||||
|
let mut builtins = HashMap::new();
|
||||||
|
let builtins_sym = StringId(interner.get_or_intern("builtins"));
|
||||||
|
builtins.insert(builtins_sym, Ir::Builtins);
|
||||||
|
|
||||||
|
let free_globals = [
|
||||||
|
"abort",
|
||||||
|
"baseNameOf",
|
||||||
|
"break",
|
||||||
|
"dirOf",
|
||||||
|
"derivation",
|
||||||
|
"derivationStrict",
|
||||||
|
"fetchGit",
|
||||||
|
"fetchMercurial",
|
||||||
|
"fetchTarball",
|
||||||
|
"fetchTree",
|
||||||
|
"fromTOML",
|
||||||
|
"import",
|
||||||
|
"isNull",
|
||||||
|
"map",
|
||||||
|
"placeholder",
|
||||||
|
"removeAttrs",
|
||||||
|
"scopedImport",
|
||||||
|
"throw",
|
||||||
|
"toString",
|
||||||
|
];
|
||||||
|
let consts = [
|
||||||
|
("true", Ir::Bool(true)),
|
||||||
|
("false", Ir::Bool(false)),
|
||||||
|
("null", Ir::Null),
|
||||||
|
];
|
||||||
|
|
||||||
|
for name in free_globals {
|
||||||
|
let name = StringId(interner.get_or_intern(name));
|
||||||
|
let value = Ir::Builtin(name);
|
||||||
|
builtins.insert(name, value);
|
||||||
|
}
|
||||||
|
for (name, value) in consts {
|
||||||
|
let name = StringId(interner.get_or_intern(name));
|
||||||
|
builtins.insert(name, value);
|
||||||
|
}
|
||||||
|
|
||||||
|
builtins
|
||||||
|
}
|
||||||
31
fix/src/runtime/corepkgs/derivation.nix
Normal file
31
fix/src/runtime/corepkgs/derivation.nix
Normal file
@@ -0,0 +1,31 @@
|
|||||||
|
drvAttrs@{
|
||||||
|
outputs ? [ "out" ],
|
||||||
|
...
|
||||||
|
}:
|
||||||
|
|
||||||
|
let
|
||||||
|
|
||||||
|
strict = derivationStrict drvAttrs;
|
||||||
|
|
||||||
|
commonAttrs =
|
||||||
|
drvAttrs
|
||||||
|
// (builtins.listToAttrs outputsList)
|
||||||
|
// {
|
||||||
|
all = map (x: x.value) outputsList;
|
||||||
|
inherit drvAttrs;
|
||||||
|
};
|
||||||
|
|
||||||
|
outputToAttrListElement = outputName: {
|
||||||
|
name = outputName;
|
||||||
|
value = commonAttrs // {
|
||||||
|
outPath = builtins.getAttr outputName strict;
|
||||||
|
drvPath = strict.drvPath;
|
||||||
|
type = "derivation";
|
||||||
|
inherit outputName;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
outputsList = map outputToAttrListElement outputs;
|
||||||
|
|
||||||
|
in
|
||||||
|
(builtins.head outputsList).value
|
||||||
89
fix/src/runtime/stack.rs
Normal file
89
fix/src/runtime/stack.rs
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
use std::mem::MaybeUninit;
|
||||||
|
|
||||||
|
use gc_arena::Collect;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
|
// FIXME: Drop???
|
||||||
|
pub(super) struct Stack<const N: usize, T> {
|
||||||
|
inner: Box<[MaybeUninit<T>; N]>,
|
||||||
|
len: usize,
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack<N, T> {
|
||||||
|
const NEEDS_TRACE: bool = true;
|
||||||
|
fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) {
|
||||||
|
for item in self.inner[..self.len].iter() {
|
||||||
|
unsafe {
|
||||||
|
item.assume_init_ref().trace(cc);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, T> Default for Stack<N, T> {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<const N: usize, T> Stack<N, T> {
|
||||||
|
pub(super) fn new() -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Box::new([const { MaybeUninit::uninit() }; N]),
|
||||||
|
len: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn push(&mut self, val: T) -> Result<(), T> {
|
||||||
|
if self.len == N {
|
||||||
|
return Err(val);
|
||||||
|
}
|
||||||
|
unsafe {
|
||||||
|
self.inner.get_unchecked_mut(self.len).write(val);
|
||||||
|
}
|
||||||
|
self.len += 1;
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn pop(&mut self) -> Option<T> {
|
||||||
|
if self.len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let ret = unsafe {
|
||||||
|
self.inner
|
||||||
|
.get_unchecked_mut(self.len - 1)
|
||||||
|
.assume_init_read()
|
||||||
|
};
|
||||||
|
self.len -= 1;
|
||||||
|
Some(ret)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn tos(&self) -> Option<&T> {
|
||||||
|
if self.len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(unsafe { self.inner.get_unchecked(self.len - 1).assume_init_ref() })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn tos_mut(&mut self) -> Option<&mut T> {
|
||||||
|
if self.len == 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(unsafe { self.inner.get_unchecked_mut(self.len - 1).assume_init_mut() })
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn len(&self) -> usize {
|
||||||
|
self.len
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(super) fn pop_n<const M: usize>(&mut self, n: usize) -> SmallVec<[T; M]> {
|
||||||
|
assert!(n <= self.len, "pop_n: not enough items on stack");
|
||||||
|
let mut result = SmallVec::new();
|
||||||
|
let start = self.len - n;
|
||||||
|
for i in start..self.len {
|
||||||
|
result.push(unsafe { self.inner.get_unchecked(i).assume_init_read() });
|
||||||
|
}
|
||||||
|
self.len = start;
|
||||||
|
result
|
||||||
|
}
|
||||||
|
}
|
||||||
489
fix/src/runtime/value.rs
Normal file
489
fix/src/runtime/value.rs
Normal file
@@ -0,0 +1,489 @@
|
|||||||
|
use std::fmt;
|
||||||
|
use std::marker::PhantomData;
|
||||||
|
use std::mem::size_of;
|
||||||
|
use std::ops::Deref;
|
||||||
|
|
||||||
|
use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue};
|
||||||
|
use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace};
|
||||||
|
use sealed::sealed;
|
||||||
|
use smallvec::SmallVec;
|
||||||
|
use string_interner::{Symbol, symbol::SymbolU32};
|
||||||
|
|
||||||
|
use crate::ir::StringId;
|
||||||
|
|
||||||
|
#[sealed]
|
||||||
|
pub(crate) trait Storable {
|
||||||
|
const TAG: (bool, u8);
|
||||||
|
}
|
||||||
|
pub(crate) trait InlineStorable: Storable + RawStore {}
|
||||||
|
pub(crate) trait GcStorable: Storable {}
|
||||||
|
|
||||||
|
macro_rules! define_value_types {
|
||||||
|
(
|
||||||
|
inline { $($itype:ty => $itag:expr, $iname:literal;)* }
|
||||||
|
gc { $($gtype:ty => $gtag:expr, $gname:literal;)* }
|
||||||
|
) => {
|
||||||
|
$(
|
||||||
|
#[sealed]
|
||||||
|
impl Storable for $itype {
|
||||||
|
const TAG: (bool, u8) = $itag;
|
||||||
|
}
|
||||||
|
impl InlineStorable for $itype {}
|
||||||
|
)*
|
||||||
|
$(
|
||||||
|
#[sealed]
|
||||||
|
impl Storable for $gtype {
|
||||||
|
const TAG: (bool, u8) = $gtag;
|
||||||
|
}
|
||||||
|
impl GcStorable for $gtype {}
|
||||||
|
)*
|
||||||
|
|
||||||
|
const _: () = assert!(size_of::<Value<'static>>() == 8);
|
||||||
|
$(const _: () = assert!(size_of::<$itype>() <= 6);)*
|
||||||
|
$(const _: () = { let (_, val) = $itag; assert!(val >= 1 && val <= 7); };)*
|
||||||
|
$(const _: () = { let (_, val) = $gtag; assert!(val >= 1 && val <= 7); };)*
|
||||||
|
|
||||||
|
const _: () = {
|
||||||
|
let tags: &[(bool, u8)] = &[$($itag),*, $($gtag),*];
|
||||||
|
let mut mask_false: u8 = 0;
|
||||||
|
let mut mask_true: u8 = 0;
|
||||||
|
let mut i = 0;
|
||||||
|
while i < tags.len() {
|
||||||
|
let (neg, val) = tags[i];
|
||||||
|
let bit = 1 << val;
|
||||||
|
if neg {
|
||||||
|
assert!(mask_true & bit == 0, "duplicate true tag id");
|
||||||
|
mask_true |= bit;
|
||||||
|
} else {
|
||||||
|
assert!(mask_false & bit == 0, "duplicate false tag id");
|
||||||
|
mask_false |= bit;
|
||||||
|
}
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
unsafe impl<'gc> Collect<'gc> for Value<'gc> {
|
||||||
|
const NEEDS_TRACE: bool = true;
|
||||||
|
fn trace<T: Trace<'gc>>(&self, cc: &mut T) {
|
||||||
|
let Some(tag) = self.raw.tag() else { return };
|
||||||
|
match tag.neg_val() {
|
||||||
|
$(<$gtype as Storable>::TAG => unsafe {
|
||||||
|
self.load_gc::<$gtype>().trace(cc)
|
||||||
|
},)*
|
||||||
|
$(<$itype as Storable>::TAG => (),)*
|
||||||
|
(neg, val) => unreachable!("invalid tag: neg={neg}, val={val}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for Value<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
match self.tag() {
|
||||||
|
None => write!(f, "Float({:?})", unsafe {
|
||||||
|
self.raw.float().unwrap_unchecked()
|
||||||
|
}),
|
||||||
|
$(Some(<$itype as Storable>::TAG) => write!(f, "{}({:?})", $iname, unsafe {
|
||||||
|
self.as_inline::<$itype>().unwrap_unchecked()
|
||||||
|
}),)*
|
||||||
|
$(Some(<$gtype as Storable>::TAG) =>
|
||||||
|
write!(f, "{}({:?})", $gname, unsafe { self.as_gc::<$gtype>().unwrap_unchecked() }),)*
|
||||||
|
Some((neg, val)) => write!(f, "Unknown(neg={neg}, val={val})"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
define_value_types! {
|
||||||
|
inline {
|
||||||
|
i32 => (false, 1), "SmallInt";
|
||||||
|
bool => (false, 2), "Bool";
|
||||||
|
Null => (false, 3), "Null";
|
||||||
|
StringId => (false, 4), "SmallString";
|
||||||
|
PrimOp => (false, 5), "PrimOp";
|
||||||
|
}
|
||||||
|
gc {
|
||||||
|
i64 => (false, 6), "BigInt";
|
||||||
|
NixString => (false, 7), "String";
|
||||||
|
AttrSet<'_> => (true, 1), "AttrSet";
|
||||||
|
List<'_> => (true, 2), "List";
|
||||||
|
Thunk<'_> => (true, 3), "Thunk";
|
||||||
|
Closure<'_> => (true, 4), "Closure";
|
||||||
|
PrimOpApp<'_> => (true, 5), "PrimOpApp";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Nix runtime value representation
|
||||||
|
///
|
||||||
|
/// NaN-boxed value fitting in 8 bytes.
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub(crate) struct Value<'gc> {
|
||||||
|
raw: RawBox,
|
||||||
|
_marker: PhantomData<Gc<'gc, ()>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for Value<'_> {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self {
|
||||||
|
raw: self.raw.clone(),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for Value<'_> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::new_inline(Null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> Value<'gc> {
|
||||||
|
#[inline(always)]
|
||||||
|
fn mk_tag(neg: bool, val: u8) -> RawTag {
|
||||||
|
// Safety: val is asserted to be in 1..=7.
|
||||||
|
unsafe { RawTag::new_unchecked(neg, val) }
|
||||||
|
}
|
||||||
|
|
||||||
|
#[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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `(negative, val)` tag, or `None` for a float.
|
||||||
|
#[inline(always)]
|
||||||
|
fn tag(&self) -> Option<(bool, u8)> {
|
||||||
|
self.raw.tag().map(|t| t.neg_val())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> Value<'gc> {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new_float(val: f64) -> Self {
|
||||||
|
Self {
|
||||||
|
raw: RawBox::from_float(val),
|
||||||
|
_marker: PhantomData,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new_inline<T: InlineStorable>(val: T) -> Self {
|
||||||
|
Self::from_raw_value(RawValue::store(Self::mk_tag(T::TAG.0, T::TAG.1), val))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn new_gc<T: GcStorable>(gc: Gc<'gc, T>) -> Self {
|
||||||
|
let ptr = Gc::as_ptr(gc);
|
||||||
|
Self::from_raw_value(RawValue::store(Self::mk_tag(T::TAG.0, T::TAG.1), ptr))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> Value<'gc> {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is_float(&self) -> bool {
|
||||||
|
self.raw.is_float()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn is<T: Storable>(&self) -> bool {
|
||||||
|
self.tag() == Some(T::TAG)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> Value<'gc> {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn as_float(&self) -> Option<f64> {
|
||||||
|
self.raw.float().copied()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) 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(crate) 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub(crate) 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"),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Heap-allocated Nix string.
|
||||||
|
///
|
||||||
|
/// Stored on the GC heap via `Gc<'gc, NixString>`. The string data itself
|
||||||
|
/// lives in a standard `Box<str>` owned by this struct; the GC only manages
|
||||||
|
/// the outer allocation.
|
||||||
|
#[derive(Collect)]
|
||||||
|
#[collect(require_static)]
|
||||||
|
pub(crate) struct NixString {
|
||||||
|
data: Box<str>,
|
||||||
|
// TODO: string context for derivation dependency tracking
|
||||||
|
}
|
||||||
|
|
||||||
|
impl NixString {
|
||||||
|
pub(crate) fn new(s: impl Into<Box<str>>) -> Self {
|
||||||
|
Self { data: s.into() }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn as_str(&self) -> &str {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for NixString {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Debug::fmt(&self.data, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Collect, Debug)]
|
||||||
|
#[collect(no_drop)]
|
||||||
|
pub(crate) struct AttrSet<'gc> {
|
||||||
|
pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> AttrSet<'gc> {
|
||||||
|
pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
|
||||||
|
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
|
||||||
|
Self { entries }
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn lookup(&self, key: StringId) -> Option<Value<'gc>> {
|
||||||
|
self.entries
|
||||||
|
.binary_search_by_key(&key, |(k, _)| *k)
|
||||||
|
.ok()
|
||||||
|
.map(|i| self.entries[i].1.clone())
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn has(&self, key: StringId) -> bool {
|
||||||
|
self.entries
|
||||||
|
.binary_search_by_key(&key, |(k, _)| *k)
|
||||||
|
.is_ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
|
||||||
|
use std::cmp::Ordering::*;
|
||||||
|
|
||||||
|
debug_assert!(self.entries.is_sorted_by_key(|(key, _)| *key));
|
||||||
|
debug_assert!(other.entries.is_sorted_by_key(|(key, _)| *key));
|
||||||
|
|
||||||
|
let mut entries = SmallVec::new();
|
||||||
|
let mut i = 0;
|
||||||
|
let mut j = 0;
|
||||||
|
while i < self.entries.len() && j < other.entries.len() {
|
||||||
|
match self.entries[i].0.cmp(&other.entries[j].0) {
|
||||||
|
Less => {
|
||||||
|
entries.push(self.entries[i].clone());
|
||||||
|
i += 1;
|
||||||
|
}
|
||||||
|
Greater => {
|
||||||
|
entries.push(other.entries[j].clone());
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
Equal => {
|
||||||
|
entries.push(other.entries[j].clone());
|
||||||
|
i += 1;
|
||||||
|
j += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
entries.extend(other.entries[j..].iter().cloned());
|
||||||
|
entries.extend(self.entries[i..].iter().cloned());
|
||||||
|
|
||||||
|
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
|
||||||
|
|
||||||
|
Gc::new(mc, AttrSet { entries })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Collect, Debug)]
|
||||||
|
#[collect(no_drop)]
|
||||||
|
pub(crate) struct List<'gc> {
|
||||||
|
pub(crate) inner: SmallVec<[Value<'gc>; 4]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) type Thunk<'gc> = RefLock<ThunkState<'gc>>;
|
||||||
|
|
||||||
|
#[derive(Collect, Debug)]
|
||||||
|
#[collect(no_drop)]
|
||||||
|
pub(crate) enum ThunkState<'gc> {
|
||||||
|
Pending {
|
||||||
|
ip: u32,
|
||||||
|
env: Gc<'gc, RefLock<Env<'gc>>>,
|
||||||
|
},
|
||||||
|
Blackhole,
|
||||||
|
Evaluated(Value<'gc>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Collect, Debug)]
|
||||||
|
#[collect(no_drop)]
|
||||||
|
pub(crate) struct Env<'gc> {
|
||||||
|
pub(crate) locals: SmallVec<[Value<'gc>; 4]>,
|
||||||
|
pub(crate) prev: Option<Gc<'gc, RefLock<Env<'gc>>>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> Env<'gc> {
|
||||||
|
pub(crate) fn empty() -> Self {
|
||||||
|
Env {
|
||||||
|
locals: SmallVec::new(),
|
||||||
|
prev: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_arg(
|
||||||
|
arg: Value<'gc>,
|
||||||
|
n_locals: u32,
|
||||||
|
prev: Gc<'gc, RefLock<Env<'gc>>>,
|
||||||
|
) -> Self {
|
||||||
|
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(crate) struct Closure<'gc> {
|
||||||
|
pub(crate) ip: u32,
|
||||||
|
pub(crate) n_locals: u32,
|
||||||
|
pub(crate) env: Gc<'gc, RefLock<Env<'gc>>>,
|
||||||
|
pub(crate) pattern: Option<Gc<'gc, PatternInfo>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Collect, Debug)]
|
||||||
|
#[collect(require_static)]
|
||||||
|
pub(crate) struct PatternInfo {
|
||||||
|
pub(crate) required: SmallVec<[StringId; 4]>,
|
||||||
|
pub(crate) optional: SmallVec<[StringId; 4]>,
|
||||||
|
pub(crate) ellipsis: bool,
|
||||||
|
pub(crate) param_spans: Box<[(StringId, u32)]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, Collect)]
|
||||||
|
#[collect(require_static)]
|
||||||
|
pub(crate) struct PrimOp {
|
||||||
|
pub(crate) id: u8,
|
||||||
|
pub(crate) arity: u8,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RawStore for PrimOp {
|
||||||
|
fn to_val(self, value: &mut RawValue) {
|
||||||
|
value.set_data([0, 0, 0, 0, self.id, self.arity]);
|
||||||
|
}
|
||||||
|
fn from_val(value: &RawValue) -> Self {
|
||||||
|
let [.., id, arity] = *value.data();
|
||||||
|
Self { id, arity }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Collect, Debug)]
|
||||||
|
#[collect(no_drop)]
|
||||||
|
pub(crate) struct PrimOpApp<'gc> {
|
||||||
|
pub(crate) primop: PrimOp,
|
||||||
|
pub(crate) args: SmallVec<[Value<'gc>; 2]>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[repr(transparent)]
|
||||||
|
pub(crate) struct StrictValue<'gc>(Value<'gc>);
|
||||||
|
|
||||||
|
impl<'gc> StrictValue<'gc> {
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn try_from_forced(val: Value<'gc>) -> Option<Self> {
|
||||||
|
if !val.is::<Thunk<'gc>>() {
|
||||||
|
Some(Self(val))
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[inline]
|
||||||
|
pub(crate) fn into_relaxed(self) -> Value<'gc> {
|
||||||
|
self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'gc> Deref for StrictValue<'gc> {
|
||||||
|
type Target = Value<'gc>;
|
||||||
|
#[inline]
|
||||||
|
fn deref(&self) -> &Value<'gc> {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Clone for StrictValue<'_> {
|
||||||
|
#[inline]
|
||||||
|
fn clone(&self) -> Self {
|
||||||
|
Self(self.0.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl fmt::Debug for StrictValue<'_> {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
fmt::Debug::fmt(&self.0, f)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
unsafe impl<'gc> Collect<'gc> for StrictValue<'gc> {
|
||||||
|
const NEEDS_TRACE: bool = true;
|
||||||
|
fn trace<T: gc_arena::collect::Trace<'gc>>(&self, cc: &mut T) {
|
||||||
|
self.0.trace(cc);
|
||||||
|
}
|
||||||
|
}
|
||||||
1849
fix/src/runtime/vm.rs
Normal file
1849
fix/src/runtime/vm.rs
Normal file
File diff suppressed because it is too large
Load Diff
40
fix/src/store.rs
Normal file
40
fix/src/store.rs
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
use crate::error::Result;
|
||||||
|
|
||||||
|
mod config;
|
||||||
|
mod daemon;
|
||||||
|
mod error;
|
||||||
|
mod validation;
|
||||||
|
|
||||||
|
pub use config::StoreConfig;
|
||||||
|
pub use daemon::DaemonStore;
|
||||||
|
pub use validation::validate_store_path;
|
||||||
|
|
||||||
|
pub trait Store: Send + Sync {
|
||||||
|
fn get_store_dir(&self) -> &str;
|
||||||
|
|
||||||
|
fn is_valid_path(&self, path: &str) -> Result<bool>;
|
||||||
|
|
||||||
|
fn ensure_path(&self, path: &str) -> Result<()>;
|
||||||
|
|
||||||
|
fn add_to_store(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
content: &[u8],
|
||||||
|
recursive: bool,
|
||||||
|
references: Vec<String>,
|
||||||
|
) -> Result<String>;
|
||||||
|
|
||||||
|
fn add_to_store_from_path(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
source_path: &std::path::Path,
|
||||||
|
references: Vec<String>,
|
||||||
|
) -> Result<String>;
|
||||||
|
|
||||||
|
fn add_text_to_store(
|
||||||
|
&self,
|
||||||
|
name: &str,
|
||||||
|
content: &str,
|
||||||
|
references: Vec<String>,
|
||||||
|
) -> Result<String>;
|
||||||
|
}
|
||||||
22
fix/src/store/config.rs
Normal file
22
fix/src/store/config.rs
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct StoreConfig {
|
||||||
|
pub daemon_socket: PathBuf,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StoreConfig {
|
||||||
|
pub fn from_env() -> Self {
|
||||||
|
let daemon_socket = std::env::var("NIX_DAEMON_SOCKET")
|
||||||
|
.map(PathBuf::from)
|
||||||
|
.unwrap_or_else(|_| PathBuf::from("/nix/var/nix/daemon-socket/socket"));
|
||||||
|
|
||||||
|
Self { daemon_socket }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for StoreConfig {
|
||||||
|
fn default() -> Self {
|
||||||
|
Self::from_env()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,5 +1,3 @@
|
|||||||
#![allow(dead_code)]
|
|
||||||
|
|
||||||
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
use std::io::{Error as IoError, ErrorKind as IoErrorKind, Result as IoResult};
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
|
||||||
@@ -9,13 +7,14 @@ use nix_compat::store_path::StorePath;
|
|||||||
use nix_compat::wire::ProtocolVersion;
|
use nix_compat::wire::ProtocolVersion;
|
||||||
use nix_compat::wire::de::{NixRead, NixReader};
|
use nix_compat::wire::de::{NixRead, NixReader};
|
||||||
use nix_compat::wire::ser::{NixSerialize, NixWrite, NixWriter, NixWriterBuilder};
|
use nix_compat::wire::ser::{NixSerialize, NixWrite, NixWriter, NixWriterBuilder};
|
||||||
|
use num_enum::{IntoPrimitive, TryFromPrimitive};
|
||||||
|
use thiserror::Error;
|
||||||
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
use tokio::io::{AsyncReadExt, AsyncWriteExt, ReadHalf, WriteHalf, split};
|
||||||
use tokio::net::UnixStream;
|
use tokio::net::UnixStream;
|
||||||
use tokio::sync::Mutex;
|
use tokio::sync::Mutex;
|
||||||
|
|
||||||
use crate::error::{Error, Result};
|
|
||||||
|
|
||||||
use super::Store;
|
use super::Store;
|
||||||
|
use crate::error::{Error, Result};
|
||||||
|
|
||||||
pub struct DaemonStore {
|
pub struct DaemonStore {
|
||||||
runtime: tokio::runtime::Runtime,
|
runtime: tokio::runtime::Runtime,
|
||||||
@@ -88,11 +87,12 @@ impl Store for DaemonStore {
|
|||||||
recursive: bool,
|
recursive: bool,
|
||||||
references: Vec<String>,
|
references: Vec<String>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
|
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
|
||||||
use nix_compat::nixhash::{CAHash, NixHash};
|
use nix_compat::nixhash::{CAHash, NixHash};
|
||||||
use nix_compat::store_path::{StorePath, build_ca_path};
|
use nix_compat::store_path::{StorePath, build_ca_path};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs;
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
let temp_file = NamedTempFile::new()
|
let temp_file = NamedTempFile::new()
|
||||||
@@ -238,11 +238,12 @@ impl Store for DaemonStore {
|
|||||||
content: &str,
|
content: &str,
|
||||||
references: Vec<String>,
|
references: Vec<String>,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
|
use std::fs;
|
||||||
|
|
||||||
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
|
use nix_compat::nix_daemon::types::AddToStoreNarRequest;
|
||||||
use nix_compat::nixhash::CAHash;
|
use nix_compat::nixhash::CAHash;
|
||||||
use nix_compat::store_path::{StorePath, build_text_path};
|
use nix_compat::store_path::{StorePath, build_text_path};
|
||||||
use sha2::{Digest, Sha256};
|
use sha2::{Digest, Sha256};
|
||||||
use std::fs;
|
|
||||||
use tempfile::NamedTempFile;
|
use tempfile::NamedTempFile;
|
||||||
|
|
||||||
let temp_file = NamedTempFile::new()
|
let temp_file = NamedTempFile::new()
|
||||||
@@ -305,51 +306,6 @@ impl Store for DaemonStore {
|
|||||||
|
|
||||||
Ok(store_path_str)
|
Ok(store_path_str)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn make_fixed_output_path(
|
|
||||||
&self,
|
|
||||||
hash_algo: &str,
|
|
||||||
hash: &str,
|
|
||||||
hash_mode: &str,
|
|
||||||
name: &str,
|
|
||||||
) -> Result<String> {
|
|
||||||
use nix_compat::nixhash::{CAHash, NixHash};
|
|
||||||
use nix_compat::store_path::build_ca_path;
|
|
||||||
|
|
||||||
let nix_hash = match hash_algo {
|
|
||||||
"sha256" => {
|
|
||||||
let hash_bytes = hex::decode(hash)
|
|
||||||
.map_err(|e| Error::internal(format!("Invalid hash hex: {}", e)))?;
|
|
||||||
if hash_bytes.len() != 32 {
|
|
||||||
return Err(Error::internal(format!(
|
|
||||||
"Invalid sha256 hash length: expected 32, got {}",
|
|
||||||
hash_bytes.len()
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
let mut arr = [0u8; 32];
|
|
||||||
arr.copy_from_slice(&hash_bytes);
|
|
||||||
NixHash::Sha256(arr)
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
return Err(Error::internal(format!(
|
|
||||||
"Unsupported hash algorithm: {}",
|
|
||||||
hash_algo
|
|
||||||
)));
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let ca_hash = if hash_mode == "r" {
|
|
||||||
CAHash::Nar(nix_hash)
|
|
||||||
} else {
|
|
||||||
CAHash::Flat(nix_hash)
|
|
||||||
};
|
|
||||||
|
|
||||||
let store_path: nix_compat::store_path::StorePath<String> =
|
|
||||||
build_ca_path(name, &ca_hash, Vec::<String>::new(), false)
|
|
||||||
.map_err(|e| Error::internal(format!("Failed to build store path: {}", e)))?;
|
|
||||||
|
|
||||||
Ok(store_path.to_absolute_path())
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(1, 37);
|
const PROTOCOL_VERSION: ProtocolVersion = ProtocolVersion::from_parts(1, 37);
|
||||||
@@ -472,6 +428,7 @@ pub struct NixDaemonClient {
|
|||||||
protocol_version: ProtocolVersion,
|
protocol_version: ProtocolVersion,
|
||||||
reader: NixReader<ReadHalf<UnixStream>>,
|
reader: NixReader<ReadHalf<UnixStream>>,
|
||||||
writer: NixWriter<WriteHalf<UnixStream>>,
|
writer: NixWriter<WriteHalf<UnixStream>>,
|
||||||
|
_marker: std::marker::PhantomData<std::cell::Cell<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl NixDaemonClient {
|
impl NixDaemonClient {
|
||||||
@@ -503,26 +460,10 @@ impl NixDaemonClient {
|
|||||||
protocol_version,
|
protocol_version,
|
||||||
reader,
|
reader,
|
||||||
writer,
|
writer,
|
||||||
|
_marker: Default::default(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Execute an operation that returns a typed result
|
|
||||||
///
|
|
||||||
/// This is the main method for implementing protocol operations:
|
|
||||||
/// 1. Send operation code
|
|
||||||
/// 2. Send operation parameters
|
|
||||||
/// 3. Receive response or error
|
|
||||||
async fn execute<T>(&mut self, operation: Operation) -> IoResult<T>
|
|
||||||
where
|
|
||||||
T: nix_compat::wire::de::NixDeserialize,
|
|
||||||
{
|
|
||||||
// Send operation
|
|
||||||
self.writer.write_value(&operation).await?;
|
|
||||||
self.writer.flush().await?;
|
|
||||||
|
|
||||||
self.read_response().await
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Execute an operation with a single parameter
|
/// Execute an operation with a single parameter
|
||||||
async fn execute_with<P, T>(&mut self, operation: Operation, param: &P) -> IoResult<T>
|
async fn execute_with<P, T>(&mut self, operation: Operation, param: &P) -> IoResult<T>
|
||||||
where
|
where
|
||||||
@@ -542,7 +483,7 @@ impl NixDaemonClient {
|
|||||||
///
|
///
|
||||||
/// The daemon sends either:
|
/// The daemon sends either:
|
||||||
/// - STDERR_LAST followed by the result
|
/// - STDERR_LAST followed by the result
|
||||||
/// - STDERR_ERROR followed by an error message
|
/// - STDERR_ERROR followed by a structured error
|
||||||
async fn read_response<T>(&mut self) -> IoResult<T>
|
async fn read_response<T>(&mut self) -> IoResult<T>
|
||||||
where
|
where
|
||||||
T: nix_compat::wire::de::NixDeserialize,
|
T: nix_compat::wire::de::NixDeserialize,
|
||||||
@@ -551,23 +492,47 @@ impl NixDaemonClient {
|
|||||||
let msg = self.reader.read_number().await?;
|
let msg = self.reader.read_number().await?;
|
||||||
|
|
||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
// Success, read the actual response
|
|
||||||
let result: T = self.reader.read_value().await?;
|
let result: T = self.reader.read_value().await?;
|
||||||
return Ok(result);
|
return Ok(result);
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
// IoError, read error message
|
let error_msg = self.read_daemon_error().await?;
|
||||||
// The error is sent as a NixIoError struct, but we just read the message
|
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
// Other STDERR_* codes (logging, etc.) - for now, we ignore them
|
|
||||||
// Read and discard the associated data
|
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn read_daemon_error(&mut self) -> IoResult<NixDaemonError> {
|
||||||
|
let type_marker: String = self.reader.read_value().await?;
|
||||||
|
assert_eq!(type_marker, "Error");
|
||||||
|
|
||||||
|
let level = NixDaemonErrorLevel::try_from_primitive(
|
||||||
|
self.reader
|
||||||
|
.read_number()
|
||||||
|
.await?
|
||||||
|
.try_into()
|
||||||
|
.map_err(|_| IoError::other("invalid nix-daemon error level"))?,
|
||||||
|
)
|
||||||
|
.map_err(|_| IoError::other("invalid nix-daemon error level"))?;
|
||||||
|
// removed
|
||||||
|
let _name: String = self.reader.read_value().await?;
|
||||||
|
let msg: String = self.reader.read_value().await?;
|
||||||
|
let have_pos: u64 = self.reader.read_number().await?;
|
||||||
|
assert_eq!(have_pos, 0);
|
||||||
|
|
||||||
|
let nr_traces: u64 = self.reader.read_number().await?;
|
||||||
|
let mut traces = Vec::new();
|
||||||
|
for _ in 0..nr_traces {
|
||||||
|
let _trace_pos: u64 = self.reader.read_number().await?;
|
||||||
|
let trace_hint: String = self.reader.read_value().await?;
|
||||||
|
traces.push(trace_hint);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(NixDaemonError { level, msg, traces })
|
||||||
|
}
|
||||||
|
|
||||||
/// Check if a path is valid in the store
|
/// Check if a path is valid in the store
|
||||||
pub async fn is_valid_path(&mut self, path: &str) -> IoResult<bool> {
|
pub async fn is_valid_path(&mut self, path: &str) -> IoResult<bool> {
|
||||||
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
||||||
@@ -577,23 +542,20 @@ impl NixDaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Query information about a store path
|
/// Query information about a store path
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn query_path_info(&mut self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
|
pub async fn query_path_info(&mut self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
|
||||||
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
||||||
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
||||||
|
|
||||||
// QueryPathInfo returns Option<UnkeyedValidPathInfo> which is serialized
|
|
||||||
// as a bool followed by the value if true
|
|
||||||
self.writer.write_value(&Operation::QueryPathInfo).await?;
|
self.writer.write_value(&Operation::QueryPathInfo).await?;
|
||||||
self.writer.write_value(&store_path).await?;
|
self.writer.write_value(&store_path).await?;
|
||||||
self.writer.flush().await?;
|
self.writer.flush().await?;
|
||||||
|
|
||||||
// Read response - it's serialized as bool + optional value
|
|
||||||
loop {
|
loop {
|
||||||
let msg = self.reader.read_number().await?;
|
let msg = self.reader.read_number().await?;
|
||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
let has_value: bool = self.reader.read_value().await?;
|
let has_value: bool = self.reader.read_value().await?;
|
||||||
if has_value {
|
if has_value {
|
||||||
// Manually deserialize UnkeyedValidPathInfo
|
|
||||||
use nix_compat::narinfo::Signature;
|
use nix_compat::narinfo::Signature;
|
||||||
use nix_compat::nixhash::CAHash;
|
use nix_compat::nixhash::CAHash;
|
||||||
|
|
||||||
@@ -621,7 +583,7 @@ impl NixDaemonClient {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
let error_msg = self.read_daemon_error().await?;
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
@@ -635,18 +597,16 @@ impl NixDaemonClient {
|
|||||||
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
let store_path = StorePath::<String>::from_absolute_path(path.as_bytes())
|
||||||
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
.map_err(|e| IoError::new(IoErrorKind::InvalidInput, e.to_string()))?;
|
||||||
|
|
||||||
// EnsurePath returns void (no value)
|
|
||||||
self.writer.write_value(&Operation::EnsurePath).await?;
|
self.writer.write_value(&Operation::EnsurePath).await?;
|
||||||
self.writer.write_value(&store_path).await?;
|
self.writer.write_value(&store_path).await?;
|
||||||
self.writer.flush().await?;
|
self.writer.flush().await?;
|
||||||
|
|
||||||
// Read response - expect STDERR_LAST with no value
|
|
||||||
loop {
|
loop {
|
||||||
let msg = self.reader.read_number().await?;
|
let msg = self.reader.read_number().await?;
|
||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
let error_msg = self.read_daemon_error().await?;
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
@@ -656,6 +616,7 @@ impl NixDaemonClient {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Query which paths are valid
|
/// Query which paths are valid
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn query_valid_paths(&mut self, paths: Vec<String>) -> IoResult<Vec<String>> {
|
pub async fn query_valid_paths(&mut self, paths: Vec<String>) -> IoResult<Vec<String>> {
|
||||||
let store_paths: IoResult<Vec<StorePath<String>>> = paths
|
let store_paths: IoResult<Vec<StorePath<String>>> = paths
|
||||||
.iter()
|
.iter()
|
||||||
@@ -729,7 +690,7 @@ impl NixDaemonClient {
|
|||||||
if msg == STDERR_LAST {
|
if msg == STDERR_LAST {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
} else if msg == STDERR_ERROR {
|
} else if msg == STDERR_ERROR {
|
||||||
let error_msg: String = self.reader.read_value().await?;
|
let error_msg = self.read_daemon_error().await?;
|
||||||
return Err(IoError::other(error_msg));
|
return Err(IoError::other(error_msg));
|
||||||
} else {
|
} else {
|
||||||
let _data: String = self.reader.read_value().await?;
|
let _data: String = self.reader.read_value().await?;
|
||||||
@@ -760,6 +721,7 @@ impl NixDaemonConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Query information about a store path
|
/// Query information about a store path
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn query_path_info(&self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
|
pub async fn query_path_info(&self, path: &str) -> IoResult<Option<UnkeyedValidPathInfo>> {
|
||||||
let mut client = self.client.lock().await;
|
let mut client = self.client.lock().await;
|
||||||
client.query_path_info(path).await
|
client.query_path_info(path).await
|
||||||
@@ -772,6 +734,7 @@ impl NixDaemonConnection {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Query which paths are valid
|
/// Query which paths are valid
|
||||||
|
#[allow(dead_code)]
|
||||||
pub async fn query_valid_paths(&self, paths: Vec<String>) -> IoResult<Vec<String>> {
|
pub async fn query_valid_paths(&self, paths: Vec<String>) -> IoResult<Vec<String>> {
|
||||||
let mut client = self.client.lock().await;
|
let mut client = self.client.lock().await;
|
||||||
client.query_valid_paths(paths).await
|
client.query_valid_paths(paths).await
|
||||||
@@ -787,3 +750,24 @@ impl NixDaemonConnection {
|
|||||||
client.add_to_store_nar(request, nar_data).await
|
client.add_to_store_nar(request, nar_data).await
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, IntoPrimitive, TryFromPrimitive)]
|
||||||
|
#[repr(u8)]
|
||||||
|
pub enum NixDaemonErrorLevel {
|
||||||
|
Error = 0,
|
||||||
|
Warn,
|
||||||
|
Notice,
|
||||||
|
Info,
|
||||||
|
Talkative,
|
||||||
|
Chatty,
|
||||||
|
Debug,
|
||||||
|
Vomit,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
#[error("{msg}")]
|
||||||
|
pub struct NixDaemonError {
|
||||||
|
level: NixDaemonErrorLevel,
|
||||||
|
msg: String,
|
||||||
|
traces: Vec<String>,
|
||||||
|
}
|
||||||
@@ -1,3 +1,5 @@
|
|||||||
|
#![allow(dead_code)]
|
||||||
|
|
||||||
use std::fmt;
|
use std::fmt;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -81,7 +81,7 @@ pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> {
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn test_valid_store_paths() {
|
fn test_valid_store_paths() {
|
||||||
let store_dir = "/nix/store";
|
let store_dir = "/nix/store";
|
||||||
let valid_paths = vec![
|
let valid_paths = vec![
|
||||||
@@ -100,7 +100,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn test_invalid_store_paths() {
|
fn test_invalid_store_paths() {
|
||||||
let store_dir = "/nix/store";
|
let store_dir = "/nix/store";
|
||||||
let invalid_paths = vec![
|
let invalid_paths = vec![
|
||||||
209
fix/src/string_context.rs
Normal file
209
fix/src/string_context.rs
Normal file
@@ -0,0 +1,209 @@
|
|||||||
|
use std::collections::{BTreeMap, BTreeSet, VecDeque};
|
||||||
|
|
||||||
|
pub enum StringContextElem {
|
||||||
|
Opaque { path: String },
|
||||||
|
DrvDeep { drv_path: String },
|
||||||
|
Built { drv_path: String, output: String },
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StringContextElem {
|
||||||
|
pub fn decode(encoded: &str) -> Self {
|
||||||
|
if let Some(drv_path) = encoded.strip_prefix('=') {
|
||||||
|
StringContextElem::DrvDeep {
|
||||||
|
drv_path: drv_path.to_string(),
|
||||||
|
}
|
||||||
|
} else if let Some(rest) = encoded.strip_prefix('!') {
|
||||||
|
if let Some(second_bang) = rest.find('!') {
|
||||||
|
let output = rest[..second_bang].to_string();
|
||||||
|
let drv_path = rest[second_bang + 1..].to_string();
|
||||||
|
StringContextElem::Built { drv_path, output }
|
||||||
|
} else {
|
||||||
|
StringContextElem::Opaque {
|
||||||
|
path: encoded.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
StringContextElem::Opaque {
|
||||||
|
path: encoded.to_string(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub type InputDrvs = BTreeMap<String, BTreeSet<String>>;
|
||||||
|
pub type Srcs = BTreeSet<String>;
|
||||||
|
pub fn extract_input_drvs_and_srcs(context: &[String]) -> Result<(InputDrvs, Srcs), String> {
|
||||||
|
let mut input_drvs: BTreeMap<String, BTreeSet<String>> = BTreeMap::new();
|
||||||
|
let mut input_srcs: BTreeSet<String> = BTreeSet::new();
|
||||||
|
|
||||||
|
for encoded in context {
|
||||||
|
match StringContextElem::decode(encoded) {
|
||||||
|
StringContextElem::Opaque { path } => {
|
||||||
|
input_srcs.insert(path);
|
||||||
|
}
|
||||||
|
StringContextElem::DrvDeep { drv_path } => {
|
||||||
|
compute_fs_closure(&drv_path, &mut input_drvs, &mut input_srcs)?;
|
||||||
|
}
|
||||||
|
StringContextElem::Built { drv_path, output } => {
|
||||||
|
input_drvs.entry(drv_path).or_default().insert(output);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok((input_drvs, input_srcs))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn compute_fs_closure(
|
||||||
|
drv_path: &str,
|
||||||
|
input_drvs: &mut BTreeMap<String, BTreeSet<String>>,
|
||||||
|
input_srcs: &mut BTreeSet<String>,
|
||||||
|
) -> Result<(), String> {
|
||||||
|
let mut queue: VecDeque<String> = VecDeque::new();
|
||||||
|
let mut visited: BTreeSet<String> = BTreeSet::new();
|
||||||
|
|
||||||
|
queue.push_back(drv_path.to_string());
|
||||||
|
|
||||||
|
while let Some(current_path) = queue.pop_front() {
|
||||||
|
if visited.contains(¤t_path) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
visited.insert(current_path.clone());
|
||||||
|
input_srcs.insert(current_path.clone());
|
||||||
|
|
||||||
|
if !current_path.ends_with(".drv") {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
let content = std::fs::read_to_string(¤t_path)
|
||||||
|
.map_err(|e| format!("failed to read derivation {}: {}", current_path, e))?;
|
||||||
|
|
||||||
|
let inputs = parse_derivation_inputs(&content)
|
||||||
|
.ok_or_else(|| format!("failed to parse derivation {}", current_path))?;
|
||||||
|
|
||||||
|
for src in inputs.input_srcs {
|
||||||
|
input_srcs.insert(src.clone());
|
||||||
|
if !visited.contains(&src) {
|
||||||
|
queue.push_back(src);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (dep_drv, outputs) in inputs.input_drvs {
|
||||||
|
input_srcs.insert(dep_drv.clone());
|
||||||
|
let entry = input_drvs.entry(dep_drv.clone()).or_default();
|
||||||
|
for output in outputs {
|
||||||
|
entry.insert(output);
|
||||||
|
}
|
||||||
|
if !visited.contains(&dep_drv) {
|
||||||
|
queue.push_back(dep_drv);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
struct DerivationInputs {
|
||||||
|
input_drvs: Vec<(String, Vec<String>)>,
|
||||||
|
input_srcs: Vec<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_derivation_inputs(aterm: &str) -> Option<DerivationInputs> {
|
||||||
|
let aterm = aterm.strip_prefix("Derive([")?;
|
||||||
|
|
||||||
|
let mut bracket_count: i32 = 1;
|
||||||
|
let mut pos = 0;
|
||||||
|
let bytes = aterm.as_bytes();
|
||||||
|
while pos < bytes.len() && bracket_count > 0 {
|
||||||
|
match bytes[pos] {
|
||||||
|
b'[' => bracket_count += 1,
|
||||||
|
b']' => bracket_count -= 1,
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
if bracket_count != 0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest = &aterm[pos..];
|
||||||
|
let rest = rest.strip_prefix(",[")?;
|
||||||
|
|
||||||
|
let mut input_drvs = Vec::new();
|
||||||
|
let mut bracket_count: i32 = 1;
|
||||||
|
let mut start = 0;
|
||||||
|
pos = 0;
|
||||||
|
let bytes = rest.as_bytes();
|
||||||
|
|
||||||
|
while pos < bytes.len() && bracket_count > 0 {
|
||||||
|
match bytes[pos] {
|
||||||
|
b'[' => bracket_count += 1,
|
||||||
|
b']' => bracket_count -= 1,
|
||||||
|
b'(' if bracket_count == 1 => {
|
||||||
|
start = pos;
|
||||||
|
}
|
||||||
|
b')' if bracket_count == 1 => {
|
||||||
|
let entry = &rest[start + 1..pos];
|
||||||
|
if let Some((drv_path, outputs)) = parse_input_drv_entry(entry) {
|
||||||
|
input_drvs.push((drv_path, outputs));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
let rest = &rest[pos..];
|
||||||
|
let rest = rest.strip_prefix(",[")?;
|
||||||
|
|
||||||
|
let mut input_srcs = Vec::new();
|
||||||
|
bracket_count = 1;
|
||||||
|
pos = 0;
|
||||||
|
let bytes = rest.as_bytes();
|
||||||
|
|
||||||
|
while pos < bytes.len() && bracket_count > 0 {
|
||||||
|
match bytes[pos] {
|
||||||
|
b'[' => bracket_count += 1,
|
||||||
|
b']' => bracket_count -= 1,
|
||||||
|
b'"' if bracket_count == 1 => {
|
||||||
|
pos += 1;
|
||||||
|
let src_start = pos;
|
||||||
|
while pos < bytes.len() && bytes[pos] != b'"' {
|
||||||
|
if bytes[pos] == b'\\' && pos + 1 < bytes.len() {
|
||||||
|
pos += 2;
|
||||||
|
} else {
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
let src = std::str::from_utf8(&bytes[src_start..pos]).ok()?;
|
||||||
|
input_srcs.push(src.to_string());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
pos += 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Some(DerivationInputs {
|
||||||
|
input_drvs,
|
||||||
|
input_srcs,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn parse_input_drv_entry(entry: &str) -> Option<(String, Vec<String>)> {
|
||||||
|
let entry = entry.strip_prefix('"')?;
|
||||||
|
let quote_end = entry.find('"')?;
|
||||||
|
let drv_path = entry[..quote_end].to_string();
|
||||||
|
|
||||||
|
let rest = &entry[quote_end + 1..];
|
||||||
|
let rest = rest.strip_prefix(",[")?;
|
||||||
|
let rest = rest.strip_suffix(']')?;
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
for part in rest.split(',') {
|
||||||
|
let part = part.trim();
|
||||||
|
if let Some(name) = part.strip_prefix('"').and_then(|s| s.strip_suffix('"')) {
|
||||||
|
outputs.push(name.to_string());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Some((drv_path, outputs))
|
||||||
|
}
|
||||||
372
fix/src/value.rs
Normal file
372
fix/src/value.rs
Normal file
@@ -0,0 +1,372 @@
|
|||||||
|
use core::fmt::{Debug, Display, Formatter, Result as FmtResult};
|
||||||
|
use core::hash::Hash;
|
||||||
|
use core::ops::Deref;
|
||||||
|
use std::borrow::Cow;
|
||||||
|
use std::collections::BTreeMap;
|
||||||
|
use std::ops::DerefMut;
|
||||||
|
|
||||||
|
use derive_more::{Constructor, IsVariant, Unwrap};
|
||||||
|
|
||||||
|
/// Represents a Nix symbol, which is used as a key in attribute sets.
|
||||||
|
#[derive(Debug, Clone, Hash, PartialEq, Eq, PartialOrd, Ord, Constructor)]
|
||||||
|
pub struct Symbol<'a>(Cow<'a, str>);
|
||||||
|
|
||||||
|
pub type StaticSymbol = Symbol<'static>;
|
||||||
|
|
||||||
|
impl From<String> for Symbol<'_> {
|
||||||
|
fn from(value: String) -> Self {
|
||||||
|
Symbol(Cow::Owned(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<'a> From<&'a str> for Symbol<'a> {
|
||||||
|
fn from(value: &'a str) -> Self {
|
||||||
|
Symbol(Cow::Borrowed(value))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Formats a string slice as a Nix symbol, quoting it if necessary.
|
||||||
|
pub fn format_symbol<'a>(sym: impl Into<Cow<'a, str>>) -> Cow<'a, str> {
|
||||||
|
let sym = sym.into();
|
||||||
|
if Symbol::NORMAL_REGEX.test(&sym) {
|
||||||
|
sym
|
||||||
|
} else {
|
||||||
|
Cow::Owned(escape_quote_string(&sym))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Symbol<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
if self.normal() {
|
||||||
|
write!(f, "{}", self.0)
|
||||||
|
} else {
|
||||||
|
write!(f, "{}", escape_quote_string(&self.0))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Symbol<'_> {
|
||||||
|
const NORMAL_REGEX: ere::Regex<1> = ere::compile_regex!("^[a-zA-Z_][a-zA-Z0-9_'-]*$");
|
||||||
|
/// Checks if the symbol is a "normal" identifier that doesn't require quotes.
|
||||||
|
fn normal(&self) -> bool {
|
||||||
|
Self::NORMAL_REGEX.test(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for Symbol<'_> {
|
||||||
|
type Target = str;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a Nix attribute set, which is a map from symbols to values.
|
||||||
|
#[derive(Constructor, Default, Clone, PartialEq)]
|
||||||
|
pub struct AttrSet {
|
||||||
|
data: BTreeMap<StaticSymbol, Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrSet {
|
||||||
|
/// Gets a value by key (string or Symbol).
|
||||||
|
pub fn get<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> Option<&'a Value> {
|
||||||
|
self.data.get(&key.into())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Checks if a key exists in the attribute set.
|
||||||
|
pub fn contains_key<'a, 'sym: 'a>(&'a self, key: impl Into<Symbol<'sym>>) -> bool {
|
||||||
|
self.data.contains_key(&key.into())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for AttrSet {
|
||||||
|
type Target = BTreeMap<StaticSymbol, Value>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for AttrSet {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Debug for AttrSet {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
use Value::*;
|
||||||
|
write!(f, "{{")?;
|
||||||
|
for (k, v) in self.data.iter() {
|
||||||
|
write!(f, " {k:?} = ")?;
|
||||||
|
match v {
|
||||||
|
List(_) => write!(f, "[ ... ];")?,
|
||||||
|
AttrSet(_) => write!(f, "{{ ... }};")?,
|
||||||
|
v => write!(f, "{v:?};")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, " }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for AttrSet {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
use Value::*;
|
||||||
|
if self.data.len() > 1 {
|
||||||
|
writeln!(f, "{{")?;
|
||||||
|
for (k, v) in self.data.iter() {
|
||||||
|
write!(f, " {k} = ")?;
|
||||||
|
match v {
|
||||||
|
List(_) => writeln!(f, "[ ... ];")?,
|
||||||
|
AttrSet(_) => writeln!(f, "{{ ... }};")?,
|
||||||
|
v => writeln!(f, "{v};")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, "}}")
|
||||||
|
} else {
|
||||||
|
write!(f, "{{")?;
|
||||||
|
for (k, v) in self.data.iter() {
|
||||||
|
write!(f, " {k} = ")?;
|
||||||
|
match v {
|
||||||
|
List(_) => write!(f, "[ ... ];")?,
|
||||||
|
AttrSet(_) => write!(f, "{{ ... }};")?,
|
||||||
|
v => write!(f, "{v};")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, " }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AttrSet {
|
||||||
|
pub fn display_compat(&self) -> AttrSetCompatDisplay<'_> {
|
||||||
|
AttrSetCompatDisplay(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct AttrSetCompatDisplay<'a>(&'a AttrSet);
|
||||||
|
|
||||||
|
impl Display for AttrSetCompatDisplay<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "{{")?;
|
||||||
|
for (k, v) in self.0.data.iter() {
|
||||||
|
write!(f, " {k} = {};", v.display_compat())?;
|
||||||
|
}
|
||||||
|
write!(f, " }}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents a Nix list, which is a vector of values.
|
||||||
|
#[derive(Constructor, Default, Clone, Debug, PartialEq)]
|
||||||
|
pub struct List {
|
||||||
|
data: Vec<Value>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Deref for List {
|
||||||
|
type Target = Vec<Value>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl DerefMut for List {
|
||||||
|
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||||
|
&mut self.data
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for List {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
use Value::*;
|
||||||
|
if self.data.len() > 1 {
|
||||||
|
writeln!(f, "[")?;
|
||||||
|
for v in self.data.iter() {
|
||||||
|
match v {
|
||||||
|
List(_) => writeln!(f, " [ ... ]")?,
|
||||||
|
AttrSet(_) => writeln!(f, " {{ ... }}")?,
|
||||||
|
v => writeln!(f, " {v}")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, "]")
|
||||||
|
} else {
|
||||||
|
write!(f, "[ ")?;
|
||||||
|
for v in self.data.iter() {
|
||||||
|
match v {
|
||||||
|
List(_) => write!(f, "[ ... ] ")?,
|
||||||
|
AttrSet(_) => write!(f, "{{ ... }} ")?,
|
||||||
|
v => write!(f, "{v} ")?,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
write!(f, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl List {
|
||||||
|
pub fn display_compat(&self) -> ListCompatDisplay<'_> {
|
||||||
|
ListCompatDisplay(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ListCompatDisplay<'a>(&'a List);
|
||||||
|
|
||||||
|
impl Display for ListCompatDisplay<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
write!(f, "[ ")?;
|
||||||
|
for v in self.0.data.iter() {
|
||||||
|
write!(f, "{} ", v.display_compat())?;
|
||||||
|
}
|
||||||
|
write!(f, "]")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Represents any possible Nix value that can be returned from an evaluation.
|
||||||
|
#[derive(IsVariant, Unwrap, Clone, Debug, PartialEq)]
|
||||||
|
pub enum Value {
|
||||||
|
/// An integer value.
|
||||||
|
Int(i64),
|
||||||
|
/// An floating-point value.
|
||||||
|
Float(f64),
|
||||||
|
/// An boolean value.
|
||||||
|
Bool(bool),
|
||||||
|
/// An null value.
|
||||||
|
Null,
|
||||||
|
/// A string value.
|
||||||
|
String(String),
|
||||||
|
/// A path value (absolute path string).
|
||||||
|
Path(String),
|
||||||
|
/// An attribute set.
|
||||||
|
AttrSet(AttrSet),
|
||||||
|
/// A list.
|
||||||
|
List(List),
|
||||||
|
/// A thunk, representing a delayed computation.
|
||||||
|
Thunk,
|
||||||
|
/// A function (lambda).
|
||||||
|
Func,
|
||||||
|
/// A primitive (built-in) operation.
|
||||||
|
PrimOp(String),
|
||||||
|
/// A partially applied primitive operation.
|
||||||
|
PrimOpApp(String),
|
||||||
|
/// A marker for a value that has been seen before during serialization, to break cycles.
|
||||||
|
/// This is used to prevent infinite recursion when printing or serializing cyclic data structures.
|
||||||
|
Repeated,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn escape_quote_string(s: &str) -> String {
|
||||||
|
let mut ret = String::with_capacity(s.len() + 2);
|
||||||
|
ret.push('"');
|
||||||
|
let mut iter = s.chars().peekable();
|
||||||
|
while let Some(c) = iter.next() {
|
||||||
|
match c {
|
||||||
|
'\\' => ret.push_str("\\\\"),
|
||||||
|
'"' => ret.push_str("\\\""),
|
||||||
|
'\n' => ret.push_str("\\n"),
|
||||||
|
'\r' => ret.push_str("\\r"),
|
||||||
|
'\t' => ret.push_str("\\t"),
|
||||||
|
'$' if iter.peek() == Some(&'{') => ret.push_str("\\$"),
|
||||||
|
c => ret.push(c),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ret.push('"');
|
||||||
|
ret
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Format a float matching C's `printf("%g", x)` with default precision 6.
|
||||||
|
fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
|
||||||
|
if !x.is_finite() {
|
||||||
|
return write!(f, "{x}");
|
||||||
|
}
|
||||||
|
if x == 0.0 {
|
||||||
|
return if x.is_sign_negative() {
|
||||||
|
write!(f, "-0")
|
||||||
|
} else {
|
||||||
|
write!(f, "0")
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
let precision: i32 = 6;
|
||||||
|
let exp = x.abs().log10().floor() as i32;
|
||||||
|
|
||||||
|
let formatted = if exp >= -4 && exp < precision {
|
||||||
|
let decimal_places = (precision - 1 - exp) as usize;
|
||||||
|
format!("{x:.decimal_places$}")
|
||||||
|
} else {
|
||||||
|
let sig_digits = (precision - 1) as usize;
|
||||||
|
let s = format!("{x:.sig_digits$e}");
|
||||||
|
let (mantissa, exp_part) = s
|
||||||
|
.split_once('e')
|
||||||
|
.expect("scientific notation must contain 'e'");
|
||||||
|
let (sign, digits) = if let Some(d) = exp_part.strip_prefix('-') {
|
||||||
|
("-", d)
|
||||||
|
} else if let Some(d) = exp_part.strip_prefix('+') {
|
||||||
|
("+", d)
|
||||||
|
} else {
|
||||||
|
("+", exp_part)
|
||||||
|
};
|
||||||
|
if digits.len() < 2 {
|
||||||
|
format!("{mantissa}e{sign}0{digits}")
|
||||||
|
} else {
|
||||||
|
format!("{mantissa}e{sign}{digits}")
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if formatted.contains('.') {
|
||||||
|
if let Some(e_pos) = formatted.find('e') {
|
||||||
|
let trimmed = formatted[..e_pos]
|
||||||
|
.trim_end_matches('0')
|
||||||
|
.trim_end_matches('.');
|
||||||
|
write!(f, "{}{}", trimmed, &formatted[e_pos..])
|
||||||
|
} else {
|
||||||
|
let trimmed = formatted.trim_end_matches('0').trim_end_matches('.');
|
||||||
|
write!(f, "{trimmed}")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
write!(f, "{formatted}")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Display for Value {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
use Value::*;
|
||||||
|
match self {
|
||||||
|
&Int(x) => write!(f, "{x}"),
|
||||||
|
&Float(x) => fmt_nix_float(f, x),
|
||||||
|
&Bool(x) => write!(f, "{x}"),
|
||||||
|
Null => write!(f, "null"),
|
||||||
|
String(x) => write!(f, "{}", escape_quote_string(x)),
|
||||||
|
Path(x) => write!(f, "{x}"),
|
||||||
|
AttrSet(x) => write!(f, "{x}"),
|
||||||
|
List(x) => write!(f, "{x}"),
|
||||||
|
Thunk => write!(f, "«code»"),
|
||||||
|
Func => write!(f, "«lambda»"),
|
||||||
|
PrimOp(name) => write!(f, "«primop {name}»"),
|
||||||
|
PrimOpApp(name) => write!(f, "«partially applied primop {name}»"),
|
||||||
|
Repeated => write!(f, "«repeated»"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Value {
|
||||||
|
pub fn display_compat(&self) -> ValueCompatDisplay<'_> {
|
||||||
|
ValueCompatDisplay(self)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct ValueCompatDisplay<'a>(&'a Value);
|
||||||
|
|
||||||
|
impl Display for ValueCompatDisplay<'_> {
|
||||||
|
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
|
||||||
|
use Value::*;
|
||||||
|
match self.0 {
|
||||||
|
&Int(x) => write!(f, "{x}"),
|
||||||
|
&Float(x) => fmt_nix_float(f, x),
|
||||||
|
&Bool(x) => write!(f, "{x}"),
|
||||||
|
Null => write!(f, "null"),
|
||||||
|
String(x) => write!(f, "{}", escape_quote_string(x)),
|
||||||
|
Path(x) => write!(f, "{x}"),
|
||||||
|
AttrSet(x) => write!(f, "{}", x.display_compat()),
|
||||||
|
List(x) => write!(f, "{}", x.display_compat()),
|
||||||
|
Thunk => write!(f, "«thunk»"),
|
||||||
|
Func => write!(f, "<LAMBDA>"),
|
||||||
|
PrimOp(_) => write!(f, "<PRIMOP>"),
|
||||||
|
PrimOpApp(_) => write!(f, "<PRIMOP-APP>"),
|
||||||
|
Repeated => write!(f, "«repeated»"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,34 +1,33 @@
|
|||||||
mod utils;
|
use fix::value::Value;
|
||||||
|
|
||||||
use nix_js::value::Value;
|
use crate::utils::{eval, eval_result};
|
||||||
use utils::eval;
|
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn arithmetic() {
|
fn arithmetic() {
|
||||||
assert_eq!(eval("1 + 1"), Value::Int(2));
|
assert_eq!(eval("1 + 1"), Value::Int(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn simple_function_application() {
|
fn simple_function_application() {
|
||||||
assert_eq!(eval("(x: x) 1"), Value::Int(1));
|
assert_eq!(eval("(x: x) 1"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn curried_function() {
|
fn curried_function() {
|
||||||
assert_eq!(eval("(x: y: x - y) 2 1"), Value::Int(1));
|
assert_eq!(eval("(x: y: x - y) 2 1"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn rec_attrset() {
|
fn rec_attrset() {
|
||||||
assert_eq!(eval("rec { b = a; a = 1; }.b"), Value::Int(1));
|
assert_eq!(eval("rec { b = a; a = 1; }.b"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn let_binding() {
|
fn let_binding() {
|
||||||
assert_eq!(eval("let b = a; a = 1; in b"), Value::Int(1));
|
assert_eq!(eval("let b = a; a = 1; in b"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fibonacci() {
|
fn fibonacci() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval(
|
eval(
|
||||||
@@ -38,7 +37,7 @@ fn fibonacci() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_point_combinator() {
|
fn fixed_point_combinator() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y"),
|
eval("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y"),
|
||||||
@@ -46,20 +45,25 @@ fn fixed_point_combinator() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn conditional_true() {
|
fn conditional_true() {
|
||||||
assert_eq!(eval("if true then 1 else 0"), Value::Int(1));
|
assert_eq!(eval("if true then 1 else 0"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn conditional_false() {
|
fn conditional_false() {
|
||||||
assert_eq!(eval("if false then 1 else 0"), Value::Int(0));
|
assert_eq!(eval("if false then 1 else 0"), Value::Int(0));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn nested_let() {
|
fn nested_let() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("let x = 1; in let y = x + 1; z = y + 1; in z"),
|
eval("let x = 1; in let y = x + 1; z = y + 1; in z"),
|
||||||
Value::Int(3)
|
Value::Int(3)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
|
fn rec_inherit_fails() {
|
||||||
|
assert!(eval_result("{ inherit x; }").is_err());
|
||||||
|
}
|
||||||
@@ -1,33 +1,32 @@
|
|||||||
mod utils;
|
|
||||||
|
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
use nix_js::value::{AttrSet, List, Value};
|
use fix::value::{AttrSet, List, Value};
|
||||||
use utils::eval;
|
|
||||||
|
|
||||||
#[test]
|
use crate::utils::eval;
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
fn builtins_accessible() {
|
fn builtins_accessible() {
|
||||||
let result = eval("builtins");
|
let result = eval("builtins");
|
||||||
assert!(matches!(result, Value::AttrSet(_)));
|
assert!(matches!(result, Value::AttrSet(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_self_reference() {
|
fn builtins_self_reference() {
|
||||||
let result = eval("builtins.builtins");
|
let result = eval("builtins.builtins");
|
||||||
assert!(matches!(result, Value::AttrSet(_)));
|
assert!(matches!(result, Value::AttrSet(_)));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_add() {
|
fn builtins_add() {
|
||||||
assert_eq!(eval("builtins.add 1 2"), Value::Int(3));
|
assert_eq!(eval("builtins.add 1 2"), Value::Int(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_length() {
|
fn builtins_length() {
|
||||||
assert_eq!(eval("builtins.length [1 2 3]"), Value::Int(3));
|
assert_eq!(eval("builtins.length [1 2 3]"), Value::Int(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_map() {
|
fn builtins_map() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.map (x: x * 2) [1 2 3]"),
|
eval("builtins.map (x: x * 2) [1 2 3]"),
|
||||||
@@ -35,7 +34,7 @@ fn builtins_map() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_filter() {
|
fn builtins_filter() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.filter (x: x > 1) [1 2 3]"),
|
eval("builtins.filter (x: x > 1) [1 2 3]"),
|
||||||
@@ -43,7 +42,7 @@ fn builtins_filter() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_attrnames() {
|
fn builtins_attrnames() {
|
||||||
let result = eval("builtins.attrNames { a = 1; b = 2; }");
|
let result = eval("builtins.attrNames { a = 1; b = 2; }");
|
||||||
assert!(matches!(result, Value::List(_)));
|
assert!(matches!(result, Value::List(_)));
|
||||||
@@ -52,12 +51,12 @@ fn builtins_attrnames() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_head() {
|
fn builtins_head() {
|
||||||
assert_eq!(eval("builtins.head [1 2 3]"), Value::Int(1));
|
assert_eq!(eval("builtins.head [1 2 3]"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_tail() {
|
fn builtins_tail() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.tail [1 2 3]"),
|
eval("builtins.tail [1 2 3]"),
|
||||||
@@ -65,17 +64,17 @@ fn builtins_tail() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_in_let() {
|
fn builtins_in_let() {
|
||||||
assert_eq!(eval("let b = builtins; in b.add 5 3"), Value::Int(8));
|
assert_eq!(eval("let b = builtins; in b.add 5 3"), Value::Int(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_in_with() {
|
fn builtins_in_with() {
|
||||||
assert_eq!(eval("with builtins; add 10 20"), Value::Int(30));
|
assert_eq!(eval("with builtins; add 10 20"), Value::Int(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_nested_calls() {
|
fn builtins_nested_calls() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)"),
|
eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)"),
|
||||||
@@ -83,32 +82,32 @@ fn builtins_nested_calls() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_is_list() {
|
fn builtins_is_list() {
|
||||||
assert_eq!(eval("builtins.isList [1 2 3]"), Value::Bool(true));
|
assert_eq!(eval("builtins.isList [1 2 3]"), Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_is_attrs() {
|
fn builtins_is_attrs() {
|
||||||
assert_eq!(eval("builtins.isAttrs { a = 1; }"), Value::Bool(true));
|
assert_eq!(eval("builtins.isAttrs { a = 1; }"), Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_is_function() {
|
fn builtins_is_function() {
|
||||||
assert_eq!(eval("builtins.isFunction (x: x)"), Value::Bool(true));
|
assert_eq!(eval("builtins.isFunction (x: x)"), Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_is_null() {
|
fn builtins_is_null() {
|
||||||
assert_eq!(eval("builtins.isNull null"), Value::Bool(true));
|
assert_eq!(eval("builtins.isNull null"), Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_is_bool() {
|
fn builtins_is_bool() {
|
||||||
assert_eq!(eval("builtins.isBool true"), Value::Bool(true));
|
assert_eq!(eval("builtins.isBool true"), Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_shadowing() {
|
fn builtins_shadowing() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("let builtins = { add = x: y: x - y; }; in builtins.add 5 3"),
|
eval("let builtins = { add = x: y: x - y; }; in builtins.add 5 3"),
|
||||||
@@ -116,13 +115,13 @@ fn builtins_shadowing() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_lazy_evaluation() {
|
fn builtins_lazy_evaluation() {
|
||||||
let result = eval("builtins.builtins.builtins.add 1 1");
|
let result = eval("builtins.builtins.builtins.add 1 1");
|
||||||
assert_eq!(result, Value::Int(2));
|
assert_eq!(result, Value::Int(2));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_foldl() {
|
fn builtins_foldl() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4 5]"),
|
eval("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4 5]"),
|
||||||
@@ -130,13 +129,13 @@ fn builtins_foldl() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_elem() {
|
fn builtins_elem() {
|
||||||
assert_eq!(eval("builtins.elem 2 [1 2 3]"), Value::Bool(true));
|
assert_eq!(eval("builtins.elem 2 [1 2 3]"), Value::Bool(true));
|
||||||
assert_eq!(eval("builtins.elem 5 [1 2 3]"), Value::Bool(false));
|
assert_eq!(eval("builtins.elem 5 [1 2 3]"), Value::Bool(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_concat_lists() {
|
fn builtins_concat_lists() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.concatLists [[1 2] [3 4] [5]]"),
|
eval("builtins.concatLists [[1 2] [3 4] [5]]"),
|
||||||
@@ -150,7 +149,7 @@ fn builtins_concat_lists() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_basic() {
|
fn builtins_compare_versions_basic() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.compareVersions \"1.0\" \"2.3\""),
|
eval("builtins.compareVersions \"1.0\" \"2.3\""),
|
||||||
@@ -174,7 +173,7 @@ fn builtins_compare_versions_basic() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_components() {
|
fn builtins_compare_versions_components() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.compareVersions \"2.3.1\" \"2.3\""),
|
eval("builtins.compareVersions \"2.3.1\" \"2.3\""),
|
||||||
@@ -186,7 +185,7 @@ fn builtins_compare_versions_components() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_numeric_vs_alpha() {
|
fn builtins_compare_versions_numeric_vs_alpha() {
|
||||||
// Numeric component comes before alpha component
|
// Numeric component comes before alpha component
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -199,7 +198,7 @@ fn builtins_compare_versions_numeric_vs_alpha() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_pre() {
|
fn builtins_compare_versions_pre() {
|
||||||
// "pre" is special: comes before everything except another "pre"
|
// "pre" is special: comes before everything except another "pre"
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -220,7 +219,7 @@ fn builtins_compare_versions_pre() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_alpha() {
|
fn builtins_compare_versions_alpha() {
|
||||||
// Alphabetic comparison
|
// Alphabetic comparison
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -233,7 +232,7 @@ fn builtins_compare_versions_alpha() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_symmetry() {
|
fn builtins_compare_versions_symmetry() {
|
||||||
// Test symmetry: compareVersions(a, b) == -compareVersions(b, a)
|
// Test symmetry: compareVersions(a, b) == -compareVersions(b, a)
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -246,7 +245,7 @@ fn builtins_compare_versions_symmetry() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_compare_versions_complex() {
|
fn builtins_compare_versions_complex() {
|
||||||
// Complex version strings with multiple components
|
// Complex version strings with multiple components
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -263,7 +262,7 @@ fn builtins_compare_versions_complex() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_generic_closure() {
|
fn builtins_generic_closure() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval(
|
eval(
|
||||||
@@ -279,7 +278,7 @@ fn builtins_generic_closure() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_function_args() {
|
fn builtins_function_args() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("builtins.functionArgs (x: 1)"),
|
eval("builtins.functionArgs (x: 1)"),
|
||||||
@@ -316,7 +315,7 @@ fn builtins_function_args() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn builtins_parse_drv_name() {
|
fn builtins_parse_drv_name() {
|
||||||
let result = eval(r#"builtins.parseDrvName "nix-js-0.1.0pre""#).unwrap_attr_set();
|
let result = eval(r#"builtins.parseDrvName "nix-js-0.1.0pre""#).unwrap_attr_set();
|
||||||
assert_eq!(result.get("name"), Some(&Value::String("nix-js".into())));
|
assert_eq!(result.get("name"), Some(&Value::String("nix-js".into())));
|
||||||
@@ -1,28 +1,9 @@
|
|||||||
mod utils;
|
use fix::value::Value;
|
||||||
|
|
||||||
use std::sync::Once;
|
use crate::utils::eval_result;
|
||||||
|
|
||||||
use nix_js::value::Value;
|
#[test_log::test]
|
||||||
use utils::eval_result;
|
|
||||||
|
|
||||||
fn init() {
|
|
||||||
static INIT: Once = Once::new();
|
|
||||||
INIT.call_once(|| {
|
|
||||||
#[cfg(not(feature = "daemon"))]
|
|
||||||
unsafe {
|
|
||||||
std::env::set_var("NIX_JS_STORE_MODE", "simulated")
|
|
||||||
};
|
|
||||||
#[cfg(feature = "daemon")]
|
|
||||||
unsafe {
|
|
||||||
std::env::set_var("NIX_JS_STORE_MODE", "daemon")
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn to_file_simple() {
|
fn to_file_simple() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result =
|
let result =
|
||||||
eval_result(r#"builtins.toFile "hello.txt" "Hello, World!""#).expect("Failed to evaluate");
|
eval_result(r#"builtins.toFile "hello.txt" "Hello, World!""#).expect("Failed to evaluate");
|
||||||
|
|
||||||
@@ -38,10 +19,8 @@ fn to_file_simple() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_with_references() {
|
fn to_file_with_references() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(
|
let result = eval_result(
|
||||||
r#"
|
r#"
|
||||||
let
|
let
|
||||||
@@ -63,10 +42,8 @@ fn to_file_with_references() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_invalid_name_with_slash() {
|
fn to_file_invalid_name_with_slash() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(r#"builtins.toFile "foo/bar.txt" "content""#);
|
let result = eval_result(r#"builtins.toFile "foo/bar.txt" "content""#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
@@ -78,30 +55,24 @@ fn to_file_invalid_name_with_slash() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_invalid_name_dot() {
|
fn to_file_invalid_name_dot() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(r#"builtins.toFile "." "content""#);
|
let result = eval_result(r#"builtins.toFile "." "content""#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.unwrap_err().to_string().contains("invalid name"));
|
assert!(result.unwrap_err().to_string().contains("invalid name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_invalid_name_dotdot() {
|
fn to_file_invalid_name_dotdot() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(r#"builtins.toFile ".." "content""#);
|
let result = eval_result(r#"builtins.toFile ".." "content""#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
assert!(result.unwrap_err().to_string().contains("invalid name"));
|
assert!(result.unwrap_err().to_string().contains("invalid name"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn store_path_validation_not_in_store() {
|
fn store_path_validation_not_in_store() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(r#"builtins.storePath "/tmp/foo""#);
|
let result = eval_result(r#"builtins.storePath "/tmp/foo""#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
@@ -113,10 +84,8 @@ fn store_path_validation_not_in_store() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn store_path_validation_malformed_hash() {
|
fn store_path_validation_malformed_hash() {
|
||||||
init();
|
|
||||||
|
|
||||||
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
|
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
|
||||||
.expect("Failed to create dummy file");
|
.expect("Failed to create dummy file");
|
||||||
|
|
||||||
@@ -143,10 +112,8 @@ fn store_path_validation_malformed_hash() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn store_path_validation_missing_name() {
|
fn store_path_validation_missing_name() {
|
||||||
init();
|
|
||||||
|
|
||||||
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
|
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
|
||||||
.expect("Failed to create dummy file");
|
.expect("Failed to create dummy file");
|
||||||
|
|
||||||
@@ -173,10 +140,8 @@ fn store_path_validation_missing_name() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_curried_application() {
|
fn to_file_curried_application() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(
|
let result = eval_result(
|
||||||
r#"
|
r#"
|
||||||
let
|
let
|
||||||
@@ -197,10 +162,8 @@ fn to_file_curried_application() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_number_conversion() {
|
fn to_file_number_conversion() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(r#"builtins.toFile "number.txt" (builtins.toString 42)"#)
|
let result = eval_result(r#"builtins.toFile "number.txt" (builtins.toString 42)"#)
|
||||||
.expect("Failed to evaluate");
|
.expect("Failed to evaluate");
|
||||||
|
|
||||||
@@ -213,10 +176,8 @@ fn to_file_number_conversion() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_file_list_conversion() {
|
fn to_file_list_conversion() {
|
||||||
init();
|
|
||||||
|
|
||||||
let result = eval_result(
|
let result = eval_result(
|
||||||
r#"builtins.toFile "list.txt" (builtins.concatStringsSep "\n" ["line1" "line2" "line3"])"#,
|
r#"builtins.toFile "list.txt" (builtins.concatStringsSep "\n" ["line1" "line2" "line3"])"#,
|
||||||
)
|
)
|
||||||
@@ -1,14 +1,48 @@
|
|||||||
#![cfg(feature = "daemon")]
|
use fix::value::Value;
|
||||||
|
|
||||||
mod utils;
|
use crate::utils::{eval_deep, eval_deep_result};
|
||||||
|
|
||||||
use nix_js::value::Value;
|
#[test_log::test]
|
||||||
use utils::{eval, eval_result};
|
fn add_operator_preserves_derivation_context() {
|
||||||
|
let result = eval_deep(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
dep = derivation { name = "dep"; builder = "/bin/sh"; system = "x86_64-linux"; outputs = ["out" "dev"]; };
|
||||||
|
getOutput = output: pkg: pkg.${output} or pkg.out or pkg;
|
||||||
|
user = derivation {
|
||||||
|
name = "user";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
libPath = (getOutput "lib" dep) + "/lib";
|
||||||
|
devPath = dep.dev + "/include";
|
||||||
|
};
|
||||||
|
in user.drvPath
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
#[test]
|
let nix_result = eval_deep(
|
||||||
|
r#"
|
||||||
|
let
|
||||||
|
dep = derivation { name = "dep"; builder = "/bin/sh"; system = "x86_64-linux"; outputs = ["out" "dev"]; };
|
||||||
|
user = derivation {
|
||||||
|
name = "user";
|
||||||
|
builder = "/bin/sh";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
libPath = "${dep.out}/lib";
|
||||||
|
devPath = "${dep.dev}/include";
|
||||||
|
};
|
||||||
|
in user.drvPath
|
||||||
|
"#,
|
||||||
|
);
|
||||||
|
|
||||||
|
assert_eq!(result, nix_result);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test_log::test]
|
||||||
fn derivation_minimal() {
|
fn derivation_minimal() {
|
||||||
let result =
|
let result = eval_deep(
|
||||||
eval(r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||||
|
);
|
||||||
|
|
||||||
match result {
|
match result {
|
||||||
Value::AttrSet(attrs) => {
|
Value::AttrSet(attrs) => {
|
||||||
@@ -42,9 +76,9 @@ fn derivation_minimal() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_with_args() {
|
fn derivation_with_args() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "test";
|
name = "test";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -64,9 +98,9 @@ fn derivation_with_args() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_to_string() {
|
fn derivation_to_string() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"toString (derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; })"#,
|
r#"toString (derivation { name = "foo"; builder = "/bin/sh"; system = "x86_64-linux"; })"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -76,18 +110,19 @@ fn derivation_to_string() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_missing_name() {
|
fn derivation_missing_name() {
|
||||||
let result = eval_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
let result =
|
||||||
|
eval_deep_result(r#"derivation { builder = "/bin/sh"; system = "x86_64-linux"; }"#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err_msg = result.unwrap_err().to_string();
|
let err_msg = result.unwrap_err().to_string();
|
||||||
assert!(err_msg.contains("missing required attribute 'name'"));
|
assert!(err_msg.contains("missing required attribute 'name'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_invalid_name_with_drv_suffix() {
|
fn derivation_invalid_name_with_drv_suffix() {
|
||||||
let result = eval_result(
|
let result = eval_deep_result(
|
||||||
r#"derivation { name = "foo.drv"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
r#"derivation { name = "foo.drv"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -96,27 +131,27 @@ fn derivation_invalid_name_with_drv_suffix() {
|
|||||||
assert!(err_msg.contains("cannot end with .drv"));
|
assert!(err_msg.contains("cannot end with .drv"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_missing_builder() {
|
fn derivation_missing_builder() {
|
||||||
let result = eval_result(r#"derivation { name = "test"; system = "x86_64-linux"; }"#);
|
let result = eval_deep_result(r#"derivation { name = "test"; system = "x86_64-linux"; }"#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err_msg = result.unwrap_err().to_string();
|
let err_msg = result.unwrap_err().to_string();
|
||||||
assert!(err_msg.contains("missing required attribute 'builder'"));
|
assert!(err_msg.contains("missing required attribute 'builder'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_missing_system() {
|
fn derivation_missing_system() {
|
||||||
let result = eval_result(r#"derivation { name = "test"; builder = "/bin/sh"; }"#);
|
let result = eval_deep_result(r#"derivation { name = "test"; builder = "/bin/sh"; }"#);
|
||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err_msg = result.unwrap_err().to_string();
|
let err_msg = result.unwrap_err().to_string();
|
||||||
assert!(err_msg.contains("missing required attribute 'system'"));
|
assert!(err_msg.contains("missing required attribute 'system'"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_with_env_vars() {
|
fn derivation_with_env_vars() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "test";
|
name = "test";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -135,9 +170,9 @@ fn derivation_with_env_vars() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_strict() {
|
fn derivation_strict() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"builtins.derivationStrict { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
r#"builtins.derivationStrict { name = "test"; builder = "/bin/sh"; system = "x86_64-linux"; }"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -152,12 +187,12 @@ fn derivation_strict() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_deterministic_paths() {
|
fn derivation_deterministic_paths() {
|
||||||
let expr = r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#;
|
let expr = r#"derivation { name = "hello"; builder = "/bin/sh"; system = "x86_64-linux"; }"#;
|
||||||
|
|
||||||
let result1 = eval(expr);
|
let result1 = eval_deep(expr);
|
||||||
let result2 = eval(expr);
|
let result2 = eval_deep(expr);
|
||||||
|
|
||||||
match (result1, result2) {
|
match (result1, result2) {
|
||||||
(Value::AttrSet(attrs1), Value::AttrSet(attrs2)) => {
|
(Value::AttrSet(attrs1), Value::AttrSet(attrs2)) => {
|
||||||
@@ -168,9 +203,9 @@ fn derivation_deterministic_paths() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn derivation_escaping_in_aterm() {
|
fn derivation_escaping_in_aterm() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "test";
|
name = "test";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -188,9 +223,9 @@ fn derivation_escaping_in_aterm() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn multi_output_two_outputs() {
|
fn multi_output_two_outputs() {
|
||||||
let drv = eval(
|
let drv = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "multi";
|
name = "multi";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -231,9 +266,9 @@ fn multi_output_two_outputs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn multi_output_three_outputs() {
|
fn multi_output_three_outputs() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "three";
|
name = "three";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -279,9 +314,9 @@ fn multi_output_three_outputs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn multi_output_backward_compat() {
|
fn multi_output_backward_compat() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "compat";
|
name = "compat";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -305,9 +340,9 @@ fn multi_output_backward_compat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn multi_output_deterministic() {
|
fn multi_output_deterministic() {
|
||||||
let result1 = eval(
|
let result1 = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "determ";
|
name = "determ";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -316,7 +351,7 @@ fn multi_output_deterministic() {
|
|||||||
}"#,
|
}"#,
|
||||||
);
|
);
|
||||||
|
|
||||||
let result2 = eval(
|
let result2 = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "determ";
|
name = "determ";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -328,9 +363,9 @@ fn multi_output_deterministic() {
|
|||||||
assert_eq!(result1, result2);
|
assert_eq!(result1, result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_output_sha256_flat() {
|
fn fixed_output_sha256_flat() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "fixed";
|
name = "fixed";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -365,32 +400,24 @@ fn fixed_output_sha256_flat() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_output_default_algo() {
|
fn fixed_output_missing_hashalgo() {
|
||||||
let result = eval(
|
assert!(
|
||||||
|
eval_deep_result(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "default";
|
name = "default";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
}"#,
|
}"#,
|
||||||
|
)
|
||||||
|
.is_err()
|
||||||
);
|
);
|
||||||
|
|
||||||
match result {
|
|
||||||
Value::AttrSet(attrs) => {
|
|
||||||
assert!(attrs.contains_key("outPath"));
|
|
||||||
// Verify it defaults to sha256 (same as explicitly specifying it)
|
|
||||||
if let Some(Value::String(out_path)) = attrs.get("outPath") {
|
|
||||||
assert!(out_path.contains("/nix/store/"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => panic!("Expected AttrSet"),
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_output_recursive_mode() {
|
fn fixed_output_recursive_mode() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "recursive";
|
name = "recursive";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -418,14 +445,15 @@ fn fixed_output_recursive_mode() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_output_rejects_multi_output() {
|
fn fixed_output_rejects_multi_output() {
|
||||||
let result = eval_result(
|
let result = eval_deep_result(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "invalid";
|
name = "invalid";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
outputs = ["out" "dev"];
|
outputs = ["out" "dev"];
|
||||||
}"#,
|
}"#,
|
||||||
);
|
);
|
||||||
@@ -435,9 +463,9 @@ fn fixed_output_rejects_multi_output() {
|
|||||||
assert!(err_msg.contains("fixed-output") && err_msg.contains("one"));
|
assert!(err_msg.contains("fixed-output") && err_msg.contains("one"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_output_invalid_hash_mode() {
|
fn fixed_output_invalid_hash_mode() {
|
||||||
let result = eval_result(
|
let result = eval_deep_result(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "invalid";
|
name = "invalid";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -452,9 +480,9 @@ fn fixed_output_invalid_hash_mode() {
|
|||||||
assert!(err_msg.contains("outputHashMode") && err_msg.contains("invalid"));
|
assert!(err_msg.contains("outputHashMode") && err_msg.contains("invalid"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn structured_attrs_basic() {
|
fn structured_attrs_basic() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "struct";
|
name = "struct";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -470,16 +498,16 @@ fn structured_attrs_basic() {
|
|||||||
Value::AttrSet(attrs) => {
|
Value::AttrSet(attrs) => {
|
||||||
assert!(attrs.contains_key("drvPath"));
|
assert!(attrs.contains_key("drvPath"));
|
||||||
assert!(attrs.contains_key("outPath"));
|
assert!(attrs.contains_key("outPath"));
|
||||||
assert!(!attrs.contains_key("foo"));
|
assert!(attrs.contains_key("foo"));
|
||||||
assert!(!attrs.contains_key("count"));
|
assert!(attrs.contains_key("count"));
|
||||||
}
|
}
|
||||||
_ => panic!("Expected AttrSet"),
|
_ => panic!("Expected AttrSet"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn structured_attrs_nested() {
|
fn structured_attrs_nested() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "nested";
|
name = "nested";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -492,15 +520,15 @@ fn structured_attrs_nested() {
|
|||||||
match result {
|
match result {
|
||||||
Value::AttrSet(attrs) => {
|
Value::AttrSet(attrs) => {
|
||||||
assert!(attrs.contains_key("drvPath"));
|
assert!(attrs.contains_key("drvPath"));
|
||||||
assert!(!attrs.contains_key("data"));
|
assert!(attrs.contains_key("data"));
|
||||||
}
|
}
|
||||||
_ => panic!("Expected AttrSet"),
|
_ => panic!("Expected AttrSet"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn structured_attrs_rejects_functions() {
|
fn structured_attrs_rejects_functions() {
|
||||||
let result = eval_result(
|
let result = eval_deep_result(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "invalid";
|
name = "invalid";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -512,12 +540,12 @@ fn structured_attrs_rejects_functions() {
|
|||||||
|
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
let err_msg = result.unwrap_err().to_string();
|
let err_msg = result.unwrap_err().to_string();
|
||||||
assert!(err_msg.contains("function") && err_msg.contains("serialize"));
|
assert!(err_msg.contains("cannot convert lambda to JSON"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn structured_attrs_false() {
|
fn structured_attrs_false() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "normal";
|
name = "normal";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -538,9 +566,9 @@ fn structured_attrs_false() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn ignore_nulls_true() {
|
fn ignore_nulls_true() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "ignore";
|
name = "ignore";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -554,15 +582,15 @@ fn ignore_nulls_true() {
|
|||||||
match result {
|
match result {
|
||||||
Value::AttrSet(attrs) => {
|
Value::AttrSet(attrs) => {
|
||||||
assert!(attrs.contains_key("foo"));
|
assert!(attrs.contains_key("foo"));
|
||||||
assert!(!attrs.contains_key("nullValue"));
|
assert!(attrs.contains_key("nullValue"));
|
||||||
}
|
}
|
||||||
_ => panic!("Expected AttrSet"),
|
_ => panic!("Expected AttrSet"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn ignore_nulls_false() {
|
fn ignore_nulls_false() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "keep";
|
name = "keep";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -583,9 +611,9 @@ fn ignore_nulls_false() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn ignore_nulls_with_structured_attrs() {
|
fn ignore_nulls_with_structured_attrs() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "combined";
|
name = "combined";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -600,16 +628,16 @@ fn ignore_nulls_with_structured_attrs() {
|
|||||||
match result {
|
match result {
|
||||||
Value::AttrSet(attrs) => {
|
Value::AttrSet(attrs) => {
|
||||||
assert!(attrs.contains_key("drvPath"));
|
assert!(attrs.contains_key("drvPath"));
|
||||||
assert!(!attrs.contains_key("foo"));
|
assert!(attrs.contains_key("foo"));
|
||||||
assert!(!attrs.contains_key("nullValue"));
|
assert!(attrs.contains_key("nullValue"));
|
||||||
}
|
}
|
||||||
_ => panic!("Expected AttrSet"),
|
_ => panic!("Expected AttrSet"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn all_features_combined() {
|
fn all_features_combined() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "all";
|
name = "all";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
@@ -627,21 +655,22 @@ fn all_features_combined() {
|
|||||||
assert!(attrs.contains_key("out"));
|
assert!(attrs.contains_key("out"));
|
||||||
assert!(attrs.contains_key("dev"));
|
assert!(attrs.contains_key("dev"));
|
||||||
assert!(attrs.contains_key("outPath"));
|
assert!(attrs.contains_key("outPath"));
|
||||||
assert!(!attrs.contains_key("data"));
|
assert!(attrs.contains_key("data"));
|
||||||
assert!(!attrs.contains_key("nullValue"));
|
assert!(attrs.contains_key("nullValue"));
|
||||||
}
|
}
|
||||||
_ => panic!("Expected AttrSet"),
|
_ => panic!("Expected AttrSet"),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn fixed_output_with_structured_attrs() {
|
fn fixed_output_with_structured_attrs() {
|
||||||
let result = eval(
|
let result = eval_deep(
|
||||||
r#"derivation {
|
r#"derivation {
|
||||||
name = "fixstruct";
|
name = "fixstruct";
|
||||||
builder = "/bin/sh";
|
builder = "/bin/sh";
|
||||||
system = "x86_64-linux";
|
system = "x86_64-linux";
|
||||||
outputHash = "abc123";
|
outputHash = "0000000000000000000000000000000000000000000000000000000000000000";
|
||||||
|
outputHashAlgo = "sha256";
|
||||||
__structuredAttrs = true;
|
__structuredAttrs = true;
|
||||||
data = { key = "value"; };
|
data = { key = "value"; };
|
||||||
}"#,
|
}"#,
|
||||||
@@ -651,7 +680,7 @@ fn fixed_output_with_structured_attrs() {
|
|||||||
Value::AttrSet(attrs) => {
|
Value::AttrSet(attrs) => {
|
||||||
assert!(attrs.contains_key("outPath"));
|
assert!(attrs.contains_key("outPath"));
|
||||||
assert!(attrs.contains_key("drvPath"));
|
assert!(attrs.contains_key("drvPath"));
|
||||||
assert!(!attrs.contains_key("data"));
|
assert!(attrs.contains_key("data"));
|
||||||
}
|
}
|
||||||
_ => panic!("Expected AttrSet"),
|
_ => panic!("Expected AttrSet"),
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
mod utils;
|
use crate::utils::eval;
|
||||||
|
|
||||||
use utils::eval;
|
#[test_log::test]
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_find_file_corepkg_fetchurl() {
|
fn test_find_file_corepkg_fetchurl() {
|
||||||
let result = eval(
|
let result = eval(
|
||||||
r#"
|
r#"
|
||||||
@@ -17,13 +15,13 @@ fn test_find_file_corepkg_fetchurl() {
|
|||||||
assert!(result.to_string().contains("fetchurl.nix"));
|
assert!(result.to_string().contains("fetchurl.nix"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn test_lookup_path_syntax() {
|
fn test_lookup_path_syntax() {
|
||||||
let result = eval(r#"<nix/fetchurl.nix>"#);
|
let result = eval(r#"<nix/fetchurl.nix>"#);
|
||||||
assert!(result.to_string().contains("fetchurl.nix"));
|
assert!(result.to_string().contains("fetchurl.nix"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn test_import_corepkg() {
|
fn test_import_corepkg() {
|
||||||
let result = eval(
|
let result = eval(
|
||||||
r#"
|
r#"
|
||||||
@@ -1,24 +1,23 @@
|
|||||||
mod utils;
|
use fix::value::{List, Value};
|
||||||
|
|
||||||
use nix_js::value::{List, Value};
|
use crate::utils::{eval, eval_result};
|
||||||
use utils::eval;
|
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn true_literal() {
|
fn true_literal() {
|
||||||
assert_eq!(eval("true"), Value::Bool(true));
|
assert_eq!(eval("true"), Value::Bool(true));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn false_literal() {
|
fn false_literal() {
|
||||||
assert_eq!(eval("false"), Value::Bool(false));
|
assert_eq!(eval("false"), Value::Bool(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn null_literal() {
|
fn null_literal() {
|
||||||
assert_eq!(eval("null"), Value::Null);
|
assert_eq!(eval("null"), Value::Null);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn map_function() {
|
fn map_function() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("map (x: x * 2) [1 2 3]"),
|
eval("map (x: x * 2) [1 2 3]"),
|
||||||
@@ -26,23 +25,23 @@ fn map_function() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn is_null_function() {
|
fn is_null_function() {
|
||||||
assert_eq!(eval("isNull null"), Value::Bool(true));
|
assert_eq!(eval("isNull null"), Value::Bool(true));
|
||||||
assert_eq!(eval("isNull 5"), Value::Bool(false));
|
assert_eq!(eval("isNull 5"), Value::Bool(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn shadow_true() {
|
fn shadow_true() {
|
||||||
assert_eq!(eval("let true = false; in true"), Value::Bool(false));
|
assert_eq!(eval("let true = false; in true"), Value::Bool(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn shadow_map() {
|
fn shadow_map() {
|
||||||
assert_eq!(eval("let map = x: y: x; in map 1 2"), Value::Int(1));
|
assert_eq!(eval("let map = x: y: x; in map 1 2"), Value::Int(1));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn mixed_usage() {
|
fn mixed_usage() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("if true then map (x: x + 1) [1 2] else []"),
|
eval("if true then map (x: x + 1) [1 2] else []"),
|
||||||
@@ -50,7 +49,7 @@ fn mixed_usage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn in_let_bindings() {
|
fn in_let_bindings() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("let x = true; y = false; in x && y"),
|
eval("let x = true; y = false; in x && y"),
|
||||||
@@ -58,18 +57,18 @@ fn in_let_bindings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn shadow_in_function() {
|
fn shadow_in_function() {
|
||||||
assert_eq!(eval("(true: true) false"), Value::Bool(false));
|
assert_eq!(eval("(true: true) false"), Value::Bool(false));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn throw_function() {
|
fn throw_function() {
|
||||||
let result = utils::eval_result("throw \"error message\"");
|
let result = eval_result("throw \"error message\"");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn to_string_function() {
|
fn to_string_function() {
|
||||||
assert_eq!(eval("toString 42"), Value::String("42".to_string()));
|
assert_eq!(eval("toString 42"), Value::String("42".to_string()));
|
||||||
}
|
}
|
||||||
@@ -1,20 +1,19 @@
|
|||||||
mod utils;
|
use fix::value::Value;
|
||||||
|
|
||||||
use nix_js::value::Value;
|
use crate::utils::{eval, eval_result};
|
||||||
use utils::{eval, eval_result};
|
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn required_parameters() {
|
fn required_parameters() {
|
||||||
assert_eq!(eval("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
|
assert_eq!(eval("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn missing_required_parameter() {
|
fn missing_required_parameter() {
|
||||||
let result = eval_result("({ a, b }: a + b) { a = 1; }");
|
let result = eval_result("({ a, b }: a + b) { a = 1; }");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn all_required_parameters_present() {
|
fn all_required_parameters_present() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }"),
|
eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }"),
|
||||||
@@ -22,13 +21,13 @@ fn all_required_parameters_present() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn reject_unexpected_arguments() {
|
fn reject_unexpected_arguments() {
|
||||||
let result = eval_result("({ a, b }: a + b) { a = 1; b = 2; c = 3; }");
|
let result = eval_result("({ a, b }: a + b) { a = 1; b = 2; c = 3; }");
|
||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn ellipsis_accepts_extra_arguments() {
|
fn ellipsis_accepts_extra_arguments() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }"),
|
eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }"),
|
||||||
@@ -36,12 +35,12 @@ fn ellipsis_accepts_extra_arguments() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn default_parameters() {
|
fn default_parameters() {
|
||||||
assert_eq!(eval("({ a, b ? 5 }: a + b) { a = 1; }"), Value::Int(6));
|
assert_eq!(eval("({ a, b ? 5 }: a + b) { a = 1; }"), Value::Int(6));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn override_default_parameter() {
|
fn override_default_parameter() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }"),
|
eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }"),
|
||||||
@@ -49,7 +48,7 @@ fn override_default_parameter() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn at_pattern_alias() {
|
fn at_pattern_alias() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }"),
|
eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }"),
|
||||||
@@ -57,17 +56,17 @@ fn at_pattern_alias() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn simple_parameter_no_validation() {
|
fn simple_parameter_no_validation() {
|
||||||
assert_eq!(eval("(x: x.a + x.b) { a = 1; b = 2; }"), Value::Int(3));
|
assert_eq!(eval("(x: x.a + x.b) { a = 1; b = 2; }"), Value::Int(3));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn simple_parameter_accepts_any_argument() {
|
fn simple_parameter_accepts_any_argument() {
|
||||||
assert_eq!(eval("(x: x) 42"), Value::Int(42));
|
assert_eq!(eval("(x: x) 42"), Value::Int(42));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn nested_function_parameters() {
|
fn nested_function_parameters() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a }: { b }: a + b) { a = 5; } { b = 3; }"),
|
eval("({ a }: { b }: a + b) { a = 5; } { b = 3; }"),
|
||||||
@@ -75,12 +74,12 @@ fn nested_function_parameters() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn pattern_param_simple_reference_in_default() {
|
fn pattern_param_simple_reference_in_default() {
|
||||||
assert_eq!(eval("({ a, b ? a }: b) { a = 10; }"), Value::Int(10));
|
assert_eq!(eval("({ a, b ? a }: b) { a = 10; }"), Value::Int(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn pattern_param_multiple_references_in_default() {
|
fn pattern_param_multiple_references_in_default() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a, b ? a + 5, c ? 1 }: b + c) { a = 10; }"),
|
eval("({ a, b ? a + 5, c ? 1 }: b + c) { a = 10; }"),
|
||||||
@@ -88,7 +87,7 @@ fn pattern_param_multiple_references_in_default() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn pattern_param_mutual_reference() {
|
fn pattern_param_mutual_reference() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; }"),
|
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; }"),
|
||||||
@@ -96,7 +95,7 @@ fn pattern_param_mutual_reference() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn pattern_param_override_mutual_reference() {
|
fn pattern_param_override_mutual_reference() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; c = 10; }"),
|
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; c = 10; }"),
|
||||||
@@ -104,7 +103,7 @@ fn pattern_param_override_mutual_reference() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn pattern_param_reference_list() {
|
fn pattern_param_reference_list() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("({ a, b ? [ a 2 ] }: builtins.elemAt b 0) { a = 42; }"),
|
eval("({ a, b ? [ a 2 ] }: builtins.elemAt b 0) { a = 42; }"),
|
||||||
@@ -112,7 +111,7 @@ fn pattern_param_reference_list() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn pattern_param_alias_in_default() {
|
fn pattern_param_alias_in_default() {
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
eval("(args@{ a, b ? args.a + 10 }: b) { a = 5; }"),
|
eval("(args@{ a, b ? args.a + 10 }: b) { a = 5; }"),
|
||||||
@@ -1,12 +1,10 @@
|
|||||||
mod utils;
|
use fix::error::Source;
|
||||||
|
use fix::runtime::Runtime;
|
||||||
use nix_js::context::Context;
|
use fix::value::Value;
|
||||||
use nix_js::error::Source;
|
|
||||||
use nix_js::value::Value;
|
|
||||||
|
|
||||||
use crate::utils::{eval, eval_result};
|
use crate::utils::{eval, eval_result};
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn import_absolute_path() {
|
fn import_absolute_path() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let lib_path = temp_dir.path().join("nix_test_lib.nix");
|
let lib_path = temp_dir.path().join("nix_test_lib.nix");
|
||||||
@@ -17,7 +15,7 @@ fn import_absolute_path() {
|
|||||||
assert_eq!(eval(&expr), Value::Int(8));
|
assert_eq!(eval(&expr), Value::Int(8));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn import_nested() {
|
fn import_nested() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
@@ -35,7 +33,7 @@ fn import_nested() {
|
|||||||
assert_eq!(eval(&expr), Value::Int(30));
|
assert_eq!(eval(&expr), Value::Int(30));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn import_relative_path() {
|
fn import_relative_path() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let subdir = temp_dir.path().join("subdir");
|
let subdir = temp_dir.path().join("subdir");
|
||||||
@@ -66,7 +64,7 @@ fn import_relative_path() {
|
|||||||
assert_eq!(eval(&expr), Value::Int(7));
|
assert_eq!(eval(&expr), Value::Int(7));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn import_returns_function() {
|
fn import_returns_function() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let func_path = temp_dir.path().join("nix_test_func.nix");
|
let func_path = temp_dir.path().join("nix_test_func.nix");
|
||||||
@@ -76,7 +74,7 @@ fn import_returns_function() {
|
|||||||
assert_eq!(eval(&expr), Value::Int(10));
|
assert_eq!(eval(&expr), Value::Int(10));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn import_with_complex_dependency_graph() {
|
fn import_with_complex_dependency_graph() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
||||||
@@ -97,26 +95,26 @@ fn import_with_complex_dependency_graph() {
|
|||||||
|
|
||||||
// Tests for builtins.path
|
// Tests for builtins.path
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_with_file() {
|
fn path_with_file() {
|
||||||
let mut ctx = Context::new().unwrap();
|
let mut ctx = Runtime::new().unwrap();
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("test.txt");
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
std::fs::write(&test_file, "Hello, World!").unwrap();
|
std::fs::write(&test_file, "Hello, World!").unwrap();
|
||||||
|
|
||||||
let expr = format!(r#"builtins.path {{ path = {}; }}"#, test_file.display());
|
let expr = format!(r#"builtins.path {{ path = {}; }}"#, test_file.display());
|
||||||
let result = ctx.eval_code(Source::new_eval(expr).unwrap()).unwrap();
|
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
|
||||||
|
|
||||||
// Should return a store path string
|
// Should return a store path string
|
||||||
if let Value::String(store_path) = result {
|
if let Value::String(store_path) = result {
|
||||||
assert!(store_path.starts_with(ctx.get_store_dir()));
|
assert!(store_path.starts_with("/nix/store"));
|
||||||
assert!(store_path.contains("test.txt"));
|
assert!(store_path.contains("test.txt"));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected string, got {:?}", result);
|
panic!("Expected string, got {:?}", result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_with_custom_name() {
|
fn path_with_custom_name() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("original.txt");
|
let test_file = temp_dir.path().join("original.txt");
|
||||||
@@ -136,9 +134,9 @@ fn path_with_custom_name() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_with_directory_recursive() {
|
fn path_with_directory_recursive() {
|
||||||
let mut ctx = Context::new().unwrap();
|
let mut ctx = Runtime::new().unwrap();
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_dir = temp_dir.path().join("mydir");
|
let test_dir = temp_dir.path().join("mydir");
|
||||||
std::fs::create_dir_all(&test_dir).unwrap();
|
std::fs::create_dir_all(&test_dir).unwrap();
|
||||||
@@ -149,19 +147,19 @@ fn path_with_directory_recursive() {
|
|||||||
r#"builtins.path {{ path = {}; recursive = true; }}"#,
|
r#"builtins.path {{ path = {}; recursive = true; }}"#,
|
||||||
test_dir.display()
|
test_dir.display()
|
||||||
);
|
);
|
||||||
let result = ctx.eval_code(Source::new_eval(expr).unwrap()).unwrap();
|
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
|
||||||
|
|
||||||
if let Value::String(store_path) = result {
|
if let Value::String(store_path) = result {
|
||||||
assert!(store_path.starts_with(ctx.get_store_dir()));
|
assert!(store_path.starts_with("/nix/store"));
|
||||||
assert!(store_path.contains("mydir"));
|
assert!(store_path.contains("mydir"));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected string, got {:?}", result);
|
panic!("Expected string, got {:?}", result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_flat_with_file() {
|
fn path_flat_with_file() {
|
||||||
let mut ctx = Context::new().unwrap();
|
let mut ctx = Runtime::new().unwrap();
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("flat.txt");
|
let test_file = temp_dir.path().join("flat.txt");
|
||||||
std::fs::write(&test_file, "Flat content").unwrap();
|
std::fs::write(&test_file, "Flat content").unwrap();
|
||||||
@@ -170,16 +168,16 @@ fn path_flat_with_file() {
|
|||||||
r#"builtins.path {{ path = {}; recursive = false; }}"#,
|
r#"builtins.path {{ path = {}; recursive = false; }}"#,
|
||||||
test_file.display()
|
test_file.display()
|
||||||
);
|
);
|
||||||
let result = ctx.eval_code(Source::new_eval(expr).unwrap()).unwrap();
|
let result = ctx.eval(Source::new_eval(expr).unwrap()).unwrap();
|
||||||
|
|
||||||
if let Value::String(store_path) = result {
|
if let Value::String(store_path) = result {
|
||||||
assert!(store_path.starts_with(ctx.get_store_dir()));
|
assert!(store_path.starts_with("/nix/store"));
|
||||||
} else {
|
} else {
|
||||||
panic!("Expected string, got {:?}", result);
|
panic!("Expected string, got {:?}", result);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_flat_with_directory_fails() {
|
fn path_flat_with_directory_fails() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_dir = temp_dir.path().join("mydir");
|
let test_dir = temp_dir.path().join("mydir");
|
||||||
@@ -196,7 +194,7 @@ fn path_flat_with_directory_fails() {
|
|||||||
assert!(err_msg.contains("recursive") || err_msg.contains("regular file"));
|
assert!(err_msg.contains("recursive") || err_msg.contains("regular file"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_nonexistent_fails() {
|
fn path_nonexistent_fails() {
|
||||||
let expr = r#"builtins.path { path = "/nonexistent/path/that/should/not/exist"; }"#;
|
let expr = r#"builtins.path { path = "/nonexistent/path/that/should/not/exist"; }"#;
|
||||||
let result = eval_result(expr);
|
let result = eval_result(expr);
|
||||||
@@ -206,7 +204,7 @@ fn path_nonexistent_fails() {
|
|||||||
assert!(err_msg.contains("does not exist"));
|
assert!(err_msg.contains("does not exist"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_missing_path_param() {
|
fn path_missing_path_param() {
|
||||||
let expr = r#"builtins.path { name = "test"; }"#;
|
let expr = r#"builtins.path { name = "test"; }"#;
|
||||||
let result = eval_result(expr);
|
let result = eval_result(expr);
|
||||||
@@ -216,7 +214,7 @@ fn path_missing_path_param() {
|
|||||||
assert!(err_msg.contains("path") && err_msg.contains("required"));
|
assert!(err_msg.contains("path") && err_msg.contains("required"));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_with_sha256() {
|
fn path_with_sha256() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("hash_test.txt");
|
let test_file = temp_dir.path().join("hash_test.txt");
|
||||||
@@ -243,7 +241,7 @@ fn path_with_sha256() {
|
|||||||
assert_eq!(store_path1, store_path2);
|
assert_eq!(store_path1, store_path2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn path_deterministic() {
|
fn path_deterministic() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("deterministic.txt");
|
let test_file = temp_dir.path().join("deterministic.txt");
|
||||||
@@ -261,7 +259,7 @@ fn path_deterministic() {
|
|||||||
assert_eq!(result1, result2);
|
assert_eq!(result1, result2);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_file_type_regular_file() {
|
fn read_file_type_regular_file() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("test.txt");
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
@@ -271,7 +269,7 @@ fn read_file_type_regular_file() {
|
|||||||
assert_eq!(eval(&expr), Value::String("regular".to_string()));
|
assert_eq!(eval(&expr), Value::String("regular".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_file_type_directory() {
|
fn read_file_type_directory() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_dir = temp_dir.path().join("testdir");
|
let test_dir = temp_dir.path().join("testdir");
|
||||||
@@ -281,7 +279,7 @@ fn read_file_type_directory() {
|
|||||||
assert_eq!(eval(&expr), Value::String("directory".to_string()));
|
assert_eq!(eval(&expr), Value::String("directory".to_string()));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_file_type_symlink() {
|
fn read_file_type_symlink() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let target = temp_dir.path().join("target.txt");
|
let target = temp_dir.path().join("target.txt");
|
||||||
@@ -299,7 +297,7 @@ fn read_file_type_symlink() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_dir_basic() {
|
fn read_dir_basic() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_dir = temp_dir.path().join("readdir_test");
|
let test_dir = temp_dir.path().join("readdir_test");
|
||||||
@@ -331,7 +329,7 @@ fn read_dir_basic() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_dir_empty() {
|
fn read_dir_empty() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_dir = temp_dir.path().join("empty_dir");
|
let test_dir = temp_dir.path().join("empty_dir");
|
||||||
@@ -347,7 +345,7 @@ fn read_dir_empty() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_dir_nonexistent_fails() {
|
fn read_dir_nonexistent_fails() {
|
||||||
let expr = r#"builtins.readDir "/nonexistent/directory""#;
|
let expr = r#"builtins.readDir "/nonexistent/directory""#;
|
||||||
let result = eval_result(expr);
|
let result = eval_result(expr);
|
||||||
@@ -355,7 +353,7 @@ fn read_dir_nonexistent_fails() {
|
|||||||
assert!(result.is_err());
|
assert!(result.is_err());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test_log::test]
|
||||||
fn read_dir_on_file_fails() {
|
fn read_dir_on_file_fails() {
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
let test_file = temp_dir.path().join("test.txt");
|
let test_file = temp_dir.path().join("test.txt");
|
||||||
319
fix/tests/tests/lang.rs
Normal file
319
fix/tests/tests/lang.rs
Normal file
@@ -0,0 +1,319 @@
|
|||||||
|
#![allow(non_snake_case)]
|
||||||
|
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
use fix::error::Source;
|
||||||
|
use fix::runtime::Runtime;
|
||||||
|
use fix::value::Value;
|
||||||
|
|
||||||
|
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 expr = format!(r#"import "{}""#, nix_path.display());
|
||||||
|
|
||||||
|
let mut ctx = Runtime::new().map_err(|e| e.to_string())?;
|
||||||
|
let source = Source {
|
||||||
|
ty: fix::error::SourceType::File(nix_path.into()),
|
||||||
|
src: expr.into(),
|
||||||
|
};
|
||||||
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
|
||||||
|
eval_fail_test!(fail_abort);
|
||||||
|
eval_fail_test!(fail_addDrvOutputDependencies_empty_context);
|
||||||
|
eval_fail_test!(fail_addDrvOutputDependencies_multi_elem_context);
|
||||||
|
eval_fail_test!(fail_addDrvOutputDependencies_wrong_element_kind);
|
||||||
|
eval_fail_test!(fail_addErrorRuntime_example);
|
||||||
|
eval_fail_test!(fail_assert);
|
||||||
|
eval_fail_test!(fail_assert_equal_attrs_names);
|
||||||
|
eval_fail_test!(fail_assert_equal_attrs_names_2);
|
||||||
|
eval_fail_test!(fail_assert_equal_derivations);
|
||||||
|
eval_fail_test!(fail_assert_equal_derivations_extra);
|
||||||
|
eval_fail_test!(fail_assert_equal_floats);
|
||||||
|
eval_fail_test!(fail_assert_equal_function_direct);
|
||||||
|
eval_fail_test!(fail_assert_equal_int_float);
|
||||||
|
eval_fail_test!(fail_assert_equal_ints);
|
||||||
|
eval_fail_test!(fail_assert_equal_list_length);
|
||||||
|
eval_fail_test!(fail_assert_equal_paths);
|
||||||
|
eval_fail_test!(fail_assert_equal_type);
|
||||||
|
eval_fail_test!(fail_assert_equal_type_nested);
|
||||||
|
eval_fail_test!(fail_assert_nested_bool);
|
||||||
|
eval_fail_test!(fail_attr_name_type);
|
||||||
|
eval_fail_test!(fail_attrset_merge_drops_later_rec);
|
||||||
|
eval_fail_test!(fail_bad_string_interpolation_1);
|
||||||
|
eval_fail_test!(fail_bad_string_interpolation_2);
|
||||||
|
eval_fail_test!(fail_bad_string_interpolation_3);
|
||||||
|
eval_fail_test!(fail_bad_string_interpolation_4);
|
||||||
|
eval_fail_test!(fail_blackhole);
|
||||||
|
eval_fail_test!(fail_call_primop);
|
||||||
|
eval_fail_test!(fail_deepseq);
|
||||||
|
eval_fail_test!(fail_derivation_name);
|
||||||
|
eval_fail_test!(fail_dup_dynamic_attrs);
|
||||||
|
eval_fail_test!(fail_duplicate_traces);
|
||||||
|
eval_fail_test!(fail_eol_1);
|
||||||
|
eval_fail_test!(fail_eol_2);
|
||||||
|
eval_fail_test!(fail_eol_3);
|
||||||
|
eval_fail_test!(fail_fetchTree_negative);
|
||||||
|
eval_fail_test!(fail_fetchurl_baseName);
|
||||||
|
eval_fail_test!(fail_fetchurl_baseName_attrs);
|
||||||
|
eval_fail_test!(fail_fetchurl_baseName_attrs_name);
|
||||||
|
eval_fail_test!(fail_flake_ref_to_string_negative_integer);
|
||||||
|
eval_fail_test!(fail_foldlStrict_strict_op_application);
|
||||||
|
eval_fail_test!(fail_fromJSON_keyWithNullByte);
|
||||||
|
eval_fail_test!(fail_fromJSON_overflowing);
|
||||||
|
eval_fail_test!(fail_fromJSON_valueWithNullByte);
|
||||||
|
eval_fail_test!(fail_fromTOML_keyWithNullByte);
|
||||||
|
eval_fail_test!(fail_fromTOML_timestamps);
|
||||||
|
eval_fail_test!(fail_fromTOML_valueWithNullByte);
|
||||||
|
eval_fail_test!(fail_hashfile_missing);
|
||||||
|
eval_fail_test!(fail_infinite_recursion_lambda);
|
||||||
|
eval_fail_test!(fail_list);
|
||||||
|
eval_fail_test!(fail_missing_arg);
|
||||||
|
eval_fail_test!(fail_mutual_recursion);
|
||||||
|
eval_fail_test!(fail_nested_list_items);
|
||||||
|
eval_fail_test!(fail_nonexist_path);
|
||||||
|
eval_fail_test!(fail_not_throws);
|
||||||
|
eval_fail_test!(fail_overflowing_add);
|
||||||
|
eval_fail_test!(fail_overflowing_div);
|
||||||
|
eval_fail_test!(fail_overflowing_mul);
|
||||||
|
eval_fail_test!(fail_overflowing_sub);
|
||||||
|
eval_fail_test!(fail_path_slash);
|
||||||
|
eval_fail_test!(fail_pipe_operators);
|
||||||
|
eval_fail_test!(fail_recursion);
|
||||||
|
eval_fail_test!(fail_remove);
|
||||||
|
eval_fail_test!(fail_scope_5);
|
||||||
|
eval_fail_test!(fail_seq);
|
||||||
|
eval_fail_test!(fail_set);
|
||||||
|
eval_fail_test!(fail_set_override);
|
||||||
|
eval_fail_test!(fail_string_nul_1);
|
||||||
|
eval_fail_test!(fail_string_nul_2);
|
||||||
|
eval_fail_test!(fail_substring);
|
||||||
|
eval_fail_test!(fail_toJSON);
|
||||||
|
eval_fail_test!(fail_toJSON_non_utf_8);
|
||||||
|
eval_fail_test!(fail_to_path);
|
||||||
|
eval_fail_test!(fail_undeclared_arg);
|
||||||
|
eval_fail_test!(fail_using_set_as_attr_name);
|
||||||
BIN
fix/tests/tests/lang/binary-data
Normal file
BIN
fix/tests/tests/lang/binary-data
Normal file
Binary file not shown.
1
fix/tests/tests/lang/data
Normal file
1
fix/tests/tests/lang/data
Normal file
@@ -0,0 +1 @@
|
|||||||
|
foo
|
||||||
1
fix/tests/tests/lang/dir1/a.nix
Normal file
1
fix/tests/tests/lang/dir1/a.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"a"
|
||||||
1
fix/tests/tests/lang/dir2/a.nix
Normal file
1
fix/tests/tests/lang/dir2/a.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"X"
|
||||||
1
fix/tests/tests/lang/dir2/b.nix
Normal file
1
fix/tests/tests/lang/dir2/b.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"b"
|
||||||
1
fix/tests/tests/lang/dir3/a.nix
Normal file
1
fix/tests/tests/lang/dir3/a.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"X"
|
||||||
1
fix/tests/tests/lang/dir3/b.nix
Normal file
1
fix/tests/tests/lang/dir3/b.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"X"
|
||||||
1
fix/tests/tests/lang/dir3/c.nix
Normal file
1
fix/tests/tests/lang/dir3/c.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"c"
|
||||||
1
fix/tests/tests/lang/dir4/a.nix
Normal file
1
fix/tests/tests/lang/dir4/a.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"X"
|
||||||
1
fix/tests/tests/lang/dir4/c.nix
Normal file
1
fix/tests/tests/lang/dir4/c.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
"X"
|
||||||
8
fix/tests/tests/lang/eval-fail-abort.err.exp
Normal file
8
fix/tests/tests/lang/eval-fail-abort.err.exp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while calling the 'abort' builtin
|
||||||
|
at /pwd/lang/eval-fail-abort.nix:1:14:
|
||||||
|
1| if true then abort "this should fail" else 1
|
||||||
|
| ^
|
||||||
|
2|
|
||||||
|
|
||||||
|
error: evaluation aborted with the following error message: 'this should fail'
|
||||||
1
fix/tests/tests/lang/eval-fail-abort.nix
Normal file
1
fix/tests/tests/lang/eval-fail-abort.nix
Normal file
@@ -0,0 +1 @@
|
|||||||
|
if true then abort "this should fail" else 1
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while calling the 'addDrvOutputDependencies' builtin
|
||||||
|
at /pwd/lang/eval-fail-addDrvOutputDependencies-empty-context.nix:1:1:
|
||||||
|
1| builtins.addDrvOutputDependencies ""
|
||||||
|
| ^
|
||||||
|
2|
|
||||||
|
|
||||||
|
error: context of string '' must have exactly one element, but has 0
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
builtins.addDrvOutputDependencies ""
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
error:
|
||||||
|
… while calling the 'addDrvOutputDependencies' builtin
|
||||||
|
at /pwd/lang/eval-fail-addDrvOutputDependencies-multi-elem-context.nix:25:1:
|
||||||
|
24| in
|
||||||
|
25| builtins.addDrvOutputDependencies combo-path
|
||||||
|
| ^
|
||||||
|
26|
|
||||||
|
|
||||||
|
error: context of string '/nix/store/pg9yqs4yd85yhdm3f4i5dyaqp5jahrsz-fail.drv/nix/store/2dxd5frb715z451vbf7s8birlf3argbk-fail-2.drv' must have exactly one element, but has 2
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
let
|
||||||
|
drv0 = derivation {
|
||||||
|
name = "fail";
|
||||||
|
builder = "/bin/false";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = [
|
||||||
|
"out"
|
||||||
|
"foo"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
drv1 = derivation {
|
||||||
|
name = "fail-2";
|
||||||
|
builder = "/bin/false";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = [
|
||||||
|
"out"
|
||||||
|
"foo"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
combo-path = "${drv0.drvPath}${drv1.drvPath}";
|
||||||
|
|
||||||
|
in
|
||||||
|
builtins.addDrvOutputDependencies combo-path
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
error:
|
||||||
|
… while calling the 'addDrvOutputDependencies' builtin
|
||||||
|
at /pwd/lang/eval-fail-addDrvOutputDependencies-wrong-element-kind.nix:13:1:
|
||||||
|
12| in
|
||||||
|
13| builtins.addDrvOutputDependencies drv.outPath
|
||||||
|
| ^
|
||||||
|
14|
|
||||||
|
|
||||||
|
error: `addDrvOutputDependencies` can only act on derivations, not on a derivation output such as 'out'
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
let
|
||||||
|
drv = derivation {
|
||||||
|
name = "fail";
|
||||||
|
builder = "/bin/false";
|
||||||
|
system = "x86_64-linux";
|
||||||
|
outputs = [
|
||||||
|
"out"
|
||||||
|
"foo"
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
in
|
||||||
|
builtins.addDrvOutputDependencies drv.outPath
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
error:
|
||||||
|
… while counting down; n = 10
|
||||||
|
|
||||||
|
… while counting down; n = 9
|
||||||
|
|
||||||
|
… while counting down; n = 8
|
||||||
|
|
||||||
|
… while counting down; n = 7
|
||||||
|
|
||||||
|
… while counting down; n = 6
|
||||||
|
|
||||||
|
… while counting down; n = 5
|
||||||
|
|
||||||
|
… while counting down; n = 4
|
||||||
|
|
||||||
|
… while counting down; n = 3
|
||||||
|
|
||||||
|
… while counting down; n = 2
|
||||||
|
|
||||||
|
… while counting down; n = 1
|
||||||
|
|
||||||
|
(stack trace truncated; use '--show-trace' to show the full, detailed trace)
|
||||||
|
|
||||||
|
error: kaboom
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
let
|
||||||
|
countDown =
|
||||||
|
n:
|
||||||
|
if n == 0 then
|
||||||
|
throw "kaboom"
|
||||||
|
else
|
||||||
|
builtins.addErrorContext "while counting down; n = ${toString n}" ("x" + countDown (n - 1));
|
||||||
|
in
|
||||||
|
countDown 10
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ a = true; } == { a = true; b = true; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-attrs-names-2.nix:1:1:
|
||||||
|
1| assert
|
||||||
|
| ^
|
||||||
|
2| {
|
||||||
|
|
||||||
|
error: attribute names of attribute set '{ a = true; }' differs from attribute set '{ a = true; b = true; }'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
assert
|
||||||
|
{
|
||||||
|
a = true;
|
||||||
|
} == {
|
||||||
|
a = true;
|
||||||
|
b = true;
|
||||||
|
};
|
||||||
|
throw "unreachable"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ a = true; b = true; } == { a = true; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-attrs-names.nix:1:1:
|
||||||
|
1| assert
|
||||||
|
| ^
|
||||||
|
2| {
|
||||||
|
|
||||||
|
error: attribute names of attribute set '{ a = true; b = true; }' differs from attribute set '{ a = true; }'
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
assert
|
||||||
|
{
|
||||||
|
a = true;
|
||||||
|
b = true;
|
||||||
|
} == {
|
||||||
|
a = true;
|
||||||
|
};
|
||||||
|
throw "unreachable"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ foo = { outPath = "/nix/store/0"; type = "derivation"; }; } == { foo = { devious = true; outPath = "/nix/store/1"; type = "derivation"; }; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:1:1:
|
||||||
|
1| assert
|
||||||
|
| ^
|
||||||
|
2| {
|
||||||
|
|
||||||
|
… while comparing attribute 'foo'
|
||||||
|
|
||||||
|
… where left hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:3:5:
|
||||||
|
2| {
|
||||||
|
3| foo = {
|
||||||
|
| ^
|
||||||
|
4| type = "derivation";
|
||||||
|
|
||||||
|
… where right hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-derivations-extra.nix:8:5:
|
||||||
|
7| } == {
|
||||||
|
8| foo = {
|
||||||
|
| ^
|
||||||
|
9| type = "derivation";
|
||||||
|
|
||||||
|
… while comparing a derivation by its 'outPath' attribute
|
||||||
|
|
||||||
|
error: string '"/nix/store/0"' is not equal to string '"/nix/store/1"'
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
assert
|
||||||
|
{
|
||||||
|
foo = {
|
||||||
|
type = "derivation";
|
||||||
|
outPath = "/nix/store/0";
|
||||||
|
};
|
||||||
|
} == {
|
||||||
|
foo = {
|
||||||
|
type = "derivation";
|
||||||
|
outPath = "/nix/store/1";
|
||||||
|
devious = true;
|
||||||
|
};
|
||||||
|
};
|
||||||
|
throw "unreachable"
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ foo = { ignored = (abort "not ignored"); outPath = "/nix/store/0"; type = "derivation"; }; } == { foo = { ignored = (abort "not ignored"); outPath = "/nix/store/1"; type = "derivation"; }; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-derivations.nix:1:1:
|
||||||
|
1| assert
|
||||||
|
| ^
|
||||||
|
2| {
|
||||||
|
|
||||||
|
… while comparing attribute 'foo'
|
||||||
|
|
||||||
|
… where left hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-derivations.nix:3:5:
|
||||||
|
2| {
|
||||||
|
3| foo = {
|
||||||
|
| ^
|
||||||
|
4| type = "derivation";
|
||||||
|
|
||||||
|
… where right hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-derivations.nix:9:5:
|
||||||
|
8| } == {
|
||||||
|
9| foo = {
|
||||||
|
| ^
|
||||||
|
10| type = "derivation";
|
||||||
|
|
||||||
|
… while comparing a derivation by its 'outPath' attribute
|
||||||
|
|
||||||
|
error: string '"/nix/store/0"' is not equal to string '"/nix/store/1"'
|
||||||
15
fix/tests/tests/lang/eval-fail-assert-equal-derivations.nix
Normal file
15
fix/tests/tests/lang/eval-fail-assert-equal-derivations.nix
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
assert
|
||||||
|
{
|
||||||
|
foo = {
|
||||||
|
type = "derivation";
|
||||||
|
outPath = "/nix/store/0";
|
||||||
|
ignored = abort "not ignored";
|
||||||
|
};
|
||||||
|
} == {
|
||||||
|
foo = {
|
||||||
|
type = "derivation";
|
||||||
|
outPath = "/nix/store/1";
|
||||||
|
ignored = abort "not ignored";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
throw "unreachable"
|
||||||
22
fix/tests/tests/lang/eval-fail-assert-equal-floats.err.exp
Normal file
22
fix/tests/tests/lang/eval-fail-assert-equal-floats.err.exp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ b = 1; } == { b = 1.01; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:1:
|
||||||
|
1| assert { b = 1.0; } == { b = 1.01; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
… while comparing attribute 'b'
|
||||||
|
|
||||||
|
… where left hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:10:
|
||||||
|
1| assert { b = 1.0; } == { b = 1.01; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
… where right hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-floats.nix:1:26:
|
||||||
|
1| assert { b = 1.0; } == { b = 1.01; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
error: a float with value '1' is not equal to a float with value '1.01'
|
||||||
2
fix/tests/tests/lang/eval-fail-assert-equal-floats.nix
Normal file
2
fix/tests/tests/lang/eval-fail-assert-equal-floats.nix
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
assert { b = 1.0; } == { b = 1.01; };
|
||||||
|
abort "unreachable"
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '((x: x) == (x: x))'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-function-direct.nix:3:1:
|
||||||
|
2| # This only compares a direct comparison and makes no claims about functions in nested structures.
|
||||||
|
3| assert (x: x) == (x: x);
|
||||||
|
| ^
|
||||||
|
4| abort "unreachable"
|
||||||
|
|
||||||
|
error: distinct functions and immediate comparisons of identical functions compare as unequal
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
# Note: functions in nested structures, e.g. attributes, may be optimized away by pointer identity optimization.
|
||||||
|
# This only compares a direct comparison and makes no claims about functions in nested structures.
|
||||||
|
assert (x: x) == (x: x);
|
||||||
|
abort "unreachable"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '(1 == 1.1)'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-int-float.nix:1:1:
|
||||||
|
1| assert 1 == 1.1;
|
||||||
|
| ^
|
||||||
|
2| throw "unreachable"
|
||||||
|
|
||||||
|
error: an integer with value '1' is not equal to a float with value '1.1'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
assert 1 == 1.1;
|
||||||
|
throw "unreachable"
|
||||||
22
fix/tests/tests/lang/eval-fail-assert-equal-ints.err.exp
Normal file
22
fix/tests/tests/lang/eval-fail-assert-equal-ints.err.exp
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ b = 1; } == { b = 2; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:1:
|
||||||
|
1| assert { b = 1; } == { b = 2; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
… while comparing attribute 'b'
|
||||||
|
|
||||||
|
… where left hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:10:
|
||||||
|
1| assert { b = 1; } == { b = 2; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
… where right hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-ints.nix:1:24:
|
||||||
|
1| assert { b = 1; } == { b = 2; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
error: an integer with value '1' is not equal to an integer with value '2'
|
||||||
2
fix/tests/tests/lang/eval-fail-assert-equal-ints.nix
Normal file
2
fix/tests/tests/lang/eval-fail-assert-equal-ints.nix
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
assert { b = 1; } == { b = 2; };
|
||||||
|
abort "unreachable"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '([ (1) (0) ] == [ (10) ])'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-list-length.nix:1:1:
|
||||||
|
1| assert
|
||||||
|
| ^
|
||||||
|
2| [
|
||||||
|
|
||||||
|
error: list of size '2' is not equal to list of size '1', left hand side is '[ 1 0 ]', right hand side is '[ 10 ]'
|
||||||
@@ -0,0 +1,6 @@
|
|||||||
|
assert
|
||||||
|
[
|
||||||
|
1
|
||||||
|
0
|
||||||
|
] == [ 10 ];
|
||||||
|
throw "unreachable"
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '(/pwd/lang/foo == /pwd/lang/bar)'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-paths.nix:1:1:
|
||||||
|
1| assert ./foo == ./bar;
|
||||||
|
| ^
|
||||||
|
2| throw "unreachable"
|
||||||
|
|
||||||
|
error: path '/pwd/lang/foo' is not equal to path '/pwd/lang/bar'
|
||||||
2
fix/tests/tests/lang/eval-fail-assert-equal-paths.nix
Normal file
2
fix/tests/tests/lang/eval-fail-assert-equal-paths.nix
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
assert ./foo == ./bar;
|
||||||
|
throw "unreachable"
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '({ ding = false; } == { ding = null; })'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:1:
|
||||||
|
1| assert { ding = false; } == { ding = null; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
… while comparing attribute 'ding'
|
||||||
|
|
||||||
|
… where left hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:10:
|
||||||
|
1| assert { ding = false; } == { ding = null; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
… where right hand side is
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-type-nested.nix:1:31:
|
||||||
|
1| assert { ding = false; } == { ding = null; };
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
error: a Boolean of value 'false' is not equal to null of value 'null'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
assert { ding = false; } == { ding = null; };
|
||||||
|
abort "unreachable"
|
||||||
8
fix/tests/tests/lang/eval-fail-assert-equal-type.err.exp
Normal file
8
fix/tests/tests/lang/eval-fail-assert-equal-type.err.exp
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
error:
|
||||||
|
… while evaluating the condition of the assertion '(false == null)'
|
||||||
|
at /pwd/lang/eval-fail-assert-equal-type.nix:1:1:
|
||||||
|
1| assert false == null;
|
||||||
|
| ^
|
||||||
|
2| abort "unreachable"
|
||||||
|
|
||||||
|
error: a Boolean of value 'false' is not equal to null of value 'null'
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user