Compare commits

..

1 Commits

Author SHA1 Message Date
imxyy1soope1 112777e1b9 temp 2026-03-21 16:00:53 +08:00
33 changed files with 5739 additions and 2296 deletions
Generated
+8 -1
View File
@@ -188,6 +188,13 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "boxing"
version = "0.1.3"
dependencies = [
"sptr",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.12.1" version = "1.12.1"
@@ -791,6 +798,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"base64", "base64",
"boxing",
"bumpalo", "bumpalo",
"bzip2", "bzip2",
"clap", "clap",
@@ -825,7 +833,6 @@ dependencies = [
"sha2", "sha2",
"small-map", "small-map",
"smallvec", "smallvec",
"sptr",
"string-interner", "string-interner",
"tap", "tap",
"tar", "tar",
+1
View File
@@ -2,6 +2,7 @@
resolver = "3" resolver = "3"
members = [ members = [
"fix", "fix",
"boxing",
] ]
[profile.profiling] [profile.profiling]
+8
View 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
View File
@@ -0,0 +1,2 @@
pub mod nan;
mod utils;
+7
View 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;
+46 -53
View File
@@ -1,28 +1,11 @@
use super::{NEG_QUIET_NAN, QUIET_NAN, SIGN_MASK};
use crate::utils::ArrayExt;
use sptr::Strict;
use std::fmt; use std::fmt;
use std::mem::ManuallyDrop;
use std::num::NonZeroU8; use std::num::NonZeroU8;
use sptr::Strict; pub trait RawStore: Sized {
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
pub(crate) trait ArrayExt<const LEN: usize> {
type Elem;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
}
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
type Elem = T;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
let copy_len = usize::min(N, M);
let mut out = [T::default(); M];
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
out
}
}
pub(crate) trait RawStore: Sized {
fn to_val(self, value: &mut Value); fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self; fn from_val(value: &Value) -> Self;
} }
@@ -155,18 +138,18 @@ enum TagVal {
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Copy, Clone, PartialEq, Eq)]
pub(crate) struct RawTag(TagVal); pub struct RawTag(TagVal);
impl RawTag { impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag { pub fn new(neg: bool, val: NonZeroU8) -> RawTag {
unsafe { Self::new_unchecked(neg, val.get() & 0x07) } unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn new_checked(neg: bool, val: u8) -> Option<RawTag> { pub fn new_checked(neg: bool, val: u8) -> Option<RawTag> {
Some(RawTag(match (neg, val) { Some(RawTag(match (neg, val) {
(false, 1) => TagVal::_P1, (false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2, (false, 2) => TagVal::_P2,
@@ -193,7 +176,7 @@ impl RawTag {
/// `val` must be in the range `1..8` /// `val` must be in the range `1..8`
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag { pub unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
RawTag(match (neg, val) { RawTag(match (neg, val) {
(false, 1) => TagVal::_P1, (false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2, (false, 2) => TagVal::_P2,
@@ -217,7 +200,7 @@ impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn is_neg(self) -> bool { pub fn is_neg(self) -> bool {
matches!(self.0, |TagVal::_N1| TagVal::_N2 matches!(self.0, |TagVal::_N1| TagVal::_N2
| TagVal::_N3 | TagVal::_N3
| TagVal::_N4 | TagVal::_N4
@@ -228,7 +211,7 @@ impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn val(self) -> NonZeroU8 { pub fn val(self) -> NonZeroU8 {
match self.0 { match self.0 {
TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN, TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN,
TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1), TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1),
@@ -242,7 +225,7 @@ impl RawTag {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn neg_val(self) -> (bool, u8) { pub fn neg_val(self) -> (bool, u8) {
match self.0 { match self.0 {
TagVal::_P1 => (false, 1), TagVal::_P1 => (false, 1),
TagVal::_P2 => (false, 2), TagVal::_P2 => (false, 2),
@@ -303,9 +286,9 @@ impl Header {
} }
} }
#[derive(Copy, Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
#[repr(C, align(8))] #[repr(C, align(8))]
pub(crate) struct Value { pub struct Value {
#[cfg(target_endian = "big")] #[cfg(target_endian = "big")]
header: Header, header: Header,
data: [u8; 6], data: [u8; 6],
@@ -315,7 +298,7 @@ pub(crate) struct Value {
impl Value { impl Value {
#[inline] #[inline]
pub(crate) fn new(tag: RawTag, data: [u8; 6]) -> Value { pub fn new(tag: RawTag, data: [u8; 6]) -> Value {
Value { Value {
header: Header::new(tag), header: Header::new(tag),
data, data,
@@ -323,23 +306,23 @@ impl Value {
} }
#[inline] #[inline]
pub(crate) fn empty(tag: RawTag) -> Value { pub fn empty(tag: RawTag) -> Value {
Value::new(tag, [0; 6]) Value::new(tag, [0; 6])
} }
pub(crate) fn store<T: RawStore>(tag: RawTag, val: T) -> Value { pub fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
let mut v = Value::new(tag, [0; 6]); let mut v = Value::new(tag, [0; 6]);
T::to_val(val, &mut v); T::to_val(val, &mut v);
v v
} }
pub(crate) fn load<T: RawStore>(self) -> T { pub fn load<T: RawStore>(self) -> T {
T::from_val(&self) T::from_val(&self)
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn tag(&self) -> RawTag { pub fn tag(&self) -> RawTag {
self.header.tag() self.header.tag()
} }
@@ -349,42 +332,41 @@ impl Value {
} }
#[inline] #[inline]
pub(crate) fn set_data(&mut self, val: [u8; 6]) { pub fn set_data(&mut self, val: [u8; 6]) {
self.data = val; self.data = val;
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn data(&self) -> &[u8; 6] { pub fn data(&self) -> &[u8; 6] {
&self.data &self.data
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn data_mut(&mut self) -> &mut [u8; 6] { pub fn data_mut(&mut self) -> &mut [u8; 6] {
&mut self.data &mut self.data
} }
#[inline] #[inline]
#[must_use] #[must_use]
unsafe fn whole(&self) -> &[u8; 8] { pub unsafe fn whole(&self) -> &[u8; 8] {
let ptr = (self as *const Value).cast::<[u8; 8]>(); let ptr = (self as *const Value).cast::<[u8; 8]>();
unsafe { &*ptr } unsafe { &*ptr }
} }
#[inline] #[inline]
#[must_use] #[must_use]
unsafe fn whole_mut(&mut self) -> &mut [u8; 8] { pub unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
let ptr = (self as *mut Value).cast::<[u8; 8]>(); let ptr = (self as *mut Value).cast::<[u8; 8]>();
unsafe { &mut *ptr } unsafe { &mut *ptr }
} }
} }
#[repr(C)] #[repr(C)]
#[derive(Copy, Clone)] pub union RawBox {
pub(crate) union RawBox {
float: f64, float: f64,
value: Value, value: ManuallyDrop<Value>,
bits: u64, bits: u64,
#[cfg(target_pointer_width = "64")] #[cfg(target_pointer_width = "64")]
ptr: *const (), ptr: *const (),
@@ -395,7 +377,7 @@ pub(crate) union RawBox {
impl RawBox { impl RawBox {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn from_float(val: f64) -> RawBox { pub fn from_float(val: f64) -> RawBox {
match (val.is_nan(), val.is_sign_positive()) { match (val.is_nan(), val.is_sign_positive()) {
(true, true) => RawBox { (true, true) => RawBox {
float: f64::from_bits(QUIET_NAN), float: f64::from_bits(QUIET_NAN),
@@ -409,13 +391,15 @@ impl RawBox {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn from_value(value: Value) -> RawBox { pub fn from_value(value: Value) -> RawBox {
RawBox { value } RawBox {
value: ManuallyDrop::new(value),
}
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn tag(&self) -> Option<RawTag> { pub fn tag(&self) -> Option<RawTag> {
if self.is_value() { if self.is_value() {
Some(unsafe { self.value.tag() }) Some(unsafe { self.value.tag() })
} else { } else {
@@ -425,19 +409,19 @@ impl RawBox {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn is_float(&self) -> bool { pub fn is_float(&self) -> bool {
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN }) (unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn is_value(&self) -> bool { pub fn is_value(&self) -> bool {
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN }) (unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
} }
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn float(&self) -> Option<&f64> { pub fn float(&self) -> Option<&f64> {
if self.is_float() { if self.is_float() {
Some(unsafe { &self.float }) Some(unsafe { &self.float })
} else { } else {
@@ -447,7 +431,7 @@ impl RawBox {
#[inline] #[inline]
#[must_use] #[must_use]
pub(crate) fn value(&self) -> Option<&Value> { pub fn value(&self) -> Option<&Value> {
if self.is_value() { if self.is_value() {
Some(unsafe { &self.value }) Some(unsafe { &self.value })
} else { } else {
@@ -456,11 +440,20 @@ impl RawBox {
} }
#[inline] #[inline]
pub(crate) fn into_float_unchecked(self) -> f64 { pub fn into_float_unchecked(self) -> f64 {
unsafe { self.float } unsafe { self.float }
} }
} }
impl Clone for RawBox {
#[inline]
fn clone(&self) -> Self {
RawBox {
ptr: unsafe { self.ptr },
}
}
}
impl fmt::Debug for RawBox { impl fmt::Debug for RawBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.float() { match self.float() {
+16
View 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
}
}
+1 -1
View File
@@ -79,7 +79,7 @@ tap = "1.0.1"
ghost-cell = "0.2" ghost-cell = "0.2"
colored = "3.1" colored = "3.1"
sptr = "0.3" boxing = { path = "../boxing" }
sealed = "0.6" sealed = "0.6"
small-map = "0.1" small-map = "0.1"
smallvec = "1.15" smallvec = "1.15"
+5 -2
View File
@@ -125,6 +125,8 @@ pub enum Error {
#[label("error occurred here")] #[label("error occurred here")]
span: Option<SourceSpan>, span: Option<SourceSpan>,
message: String, message: String,
#[help]
js_backtrace: Option<String>,
#[related] #[related]
stack_trace: Vec<StackFrame>, stack_trace: Vec<StackFrame>,
}, },
@@ -161,11 +163,12 @@ impl Error {
.into() .into()
} }
pub fn eval_error(msg: impl Into<String>) -> Box<Self> { pub fn eval_error(msg: String, backtrace: Option<String>) -> Box<Self> {
Error::EvalError { Error::EvalError {
src: None, src: None,
span: None, span: None,
message: msg.into(), message: msg,
js_backtrace: backtrace,
stack_trace: Vec::new(), stack_trace: Vec::new(),
} }
.into() .into()
-1
View File
@@ -1,7 +1,6 @@
#![warn(clippy::unwrap_used)] #![warn(clippy::unwrap_used)]
#![allow(dead_code)] #![allow(dead_code)]
mod boxing;
pub mod error; pub mod error;
pub mod logging; pub mod logging;
pub mod runtime; pub mod runtime;
+20 -33
View File
@@ -12,12 +12,10 @@ use crate::downgrade::{Downgrade as _, DowngradeContext};
use crate::error::{Error, Result, Source}; use crate::error::{Error, Result, Source};
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq}; use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq};
use crate::runtime::builtins::new_builtins_env; use crate::runtime::builtins::new_builtins_env;
use crate::runtime::vm::ForceMode;
use crate::store::{DaemonStore, StoreConfig}; use crate::store::{DaemonStore, StoreConfig};
use crate::value::Symbol; use crate::value::Symbol;
mod builtins; mod builtins;
mod primops;
mod stack; mod stack;
mod value; mod value;
mod vm; mod vm;
@@ -45,7 +43,6 @@ impl Runtime {
let store = DaemonStore::connect(&config.daemon_socket)?; let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self { Ok(Self {
arena: Arena::new(|mc| VM::new(mc, &mut strings)),
global_env, global_env,
store, store,
strings, strings,
@@ -53,19 +50,25 @@ impl Runtime {
bytecode: Vec::new(), bytecode: Vec::new(),
sources: Vec::new(), sources: Vec::new(),
spans: Vec::new(), spans: Vec::new(),
arena: Arena::new(|mc| VM::new(mc)),
}) })
} }
pub fn eval(&mut self, source: Source) -> Result<crate::value::Value> { pub fn eval(&mut self, source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::Normal) 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> { pub fn eval_shallow(&mut self, _source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::Shallow) todo!()
} }
pub fn eval_deep(&mut self, source: Source) -> Result<crate::value::Value> { pub fn eval_deep(&mut self, source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::Deep) // 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( pub fn eval_repl(
@@ -73,18 +76,10 @@ impl Runtime {
source: Source, source: Source,
scope: &HashSet<StringId>, scope: &HashSet<StringId>,
) -> Result<crate::value::Value> { ) -> Result<crate::value::Value> {
self.do_eval(source, Some(Scope::Repl(scope)), ForceMode::Shallow) // FIXME: shallow
} let root = self.downgrade(source, Some(Scope::Repl(scope)))?;
fn do_eval<'ctx>(
&'ctx mut self,
source: Source,
extra_scope: Option<Scope<'ctx>>,
force_mode: ForceMode,
) -> Result<crate::value::Value> {
let root = self.downgrade(source, extra_scope)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self); let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip, force_mode) self.run(ip)
} }
pub fn add_binding( pub fn add_binding(
@@ -113,11 +108,8 @@ impl Runtime {
bump, bump,
token, token,
strings, strings,
source: sources.last().expect("no current source").clone(), source: sources.last().unwrap().clone(),
scopes: [Scope::Global(global_env)] scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope).collect(),
.into_iter()
.chain(extra_scope)
.collect(),
with_scope_count: 0, with_scope_count: 0,
arg_count: 0, arg_count: 0,
thunk_count, thunk_count,
@@ -125,11 +117,7 @@ impl Runtime {
} }
} }
fn downgrade<'a>( fn downgrade<'a>(&'a mut self, source: Source, extra_scope: Option<Scope<'a>>) -> Result<OwnedIr> {
&'a mut self,
source: Source,
extra_scope: Option<Scope<'a>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression"); tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone()); self.sources.push(source.clone());
@@ -152,10 +140,8 @@ impl Runtime {
}) })
} }
fn run(&mut self, ip: InstructionPtr, force_mode: ForceMode) -> Result<crate::value::Value> { fn run(&mut self, ip: InstructionPtr) -> Result<crate::value::Value> {
let mut pc = ip.0; let mut pc = ip.0;
self.arena
.mutate_root(|_, vm| vm.set_force_mode(force_mode));
loop { loop {
let Runtime { let Runtime {
bytecode, bytecode,
@@ -163,7 +149,8 @@ impl Runtime {
arena, arena,
.. ..
} = self; } = self;
let action = arena.mutate_root(|mc, vm| vm.run_batch(bytecode, &mut pc, mc, strings)); let action =
arena.mutate_root(|mc, root| root.run_batch(bytecode, &mut pc, mc, strings));
match action { match action {
Action::NeedGc => { Action::NeedGc => {
if self.arena.collection_phase() == CollectionPhase::Sweeping { if self.arena.collection_phase() == CollectionPhase::Sweeping {
@@ -509,7 +496,7 @@ impl OwnedIr {
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self { unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self { Self {
_bump: bump, _bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }, ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }
} }
} }
File diff suppressed because it is too large Load Diff
-929
View File
@@ -1,929 +0,0 @@
use gc_arena::{Collect, Gc, Mutation, RefLock};
use smallvec::SmallVec;
use string_interner::DefaultStringInterner;
use super::builtins::{BuiltinId, PrimOpArgs, PrimOpStrictArgs};
use super::value::*;
use super::vm::{ForceResult, VM, VmError};
use crate::ir::StringId;
pub(super) enum BuiltinResult<'gc> {
Done(Value<'gc>),
Force(BuiltinState<'gc>, Value<'gc>),
Call(BuiltinState<'gc>, StrictValue<'gc>, Value<'gc>),
CallAndForce(BuiltinState<'gc>, StrictValue<'gc>, Value<'gc>),
Error(VmError),
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(super) enum BuiltinState<'gc> {
FoldlStrict(FoldlStrict<'gc>),
// future: Filter, GenericClosure, Sort, All, Any, ConcatMap, ...
}
impl<'gc> BuiltinState<'gc> {
pub(super) fn resume(
self,
val: StrictValue<'gc>,
ctx: &PrimOpCtx<'_, 'gc>,
) -> BuiltinResult<'gc> {
match self {
BuiltinState::FoldlStrict(s) => s.resume(val, ctx),
}
}
}
pub(super) struct PrimOpCtx<'a, 'gc> {
pub(super) vm: &'a VM<'gc>,
pub(super) mc: &'a Mutation<'gc>,
pub(super) strings: &'a DefaultStringInterner,
}
macro_rules! force_inline_or_err {
($ctx:expr, $val:expr) => {{
let val = $val;
match $ctx.vm.force_inline(val) {
Ok(ForceResult::Ready(v)) => v,
Ok(_) => {
return BuiltinResult::Error(VM::err(
"value requires evaluation in non-stateful builtin context",
));
}
Err(e) => return BuiltinResult::Error(e),
}
}};
}
macro_rules! force {
($ctx:expr, $state:expr, $val:expr) => {{
let val = $val;
match $ctx.vm.force_inline(val) {
Ok(ForceResult::Ready(v)) => v,
Ok(ForceResult::NeedEval { .. } | ForceResult::NeedApply(_)) => {
return BuiltinResult::Force($state, val);
}
Err(e) => return BuiltinResult::Error(e),
}
}};
}
macro_rules! call {
($ctx:expr, $state:expr, $func:expr, $arg:expr) => {{
let func = $func;
let arg = $arg;
return BuiltinResult::Call($state, func, arg);
}};
}
macro_rules! call_and_force {
($ctx:expr, $state:expr, $func:expr, $arg:expr) => {{
let func = $func;
let arg = $arg;
return BuiltinResult::CallAndForce($state, func, arg);
}};
}
pub(super) fn dispatch_strict_builtin<'gc>(
id: BuiltinId,
args: PrimOpStrictArgs<'gc>,
_arity: u8,
ctx: &PrimOpCtx<'_, 'gc>,
) -> BuiltinResult<'gc> {
match id {
BuiltinId::TypeOf => {
let val = args[0];
let name = if val.as_inline::<i32>().is_some() || val.as_gc::<i64>().is_some() {
"int"
} else if val.as_float().is_some() {
"float"
} else if val.as_inline::<bool>().is_some() {
"bool"
} else if VM::get_string(val, ctx.strings).is_some() {
"string"
} else if val.is::<Null>() {
"null"
} else if val.as_gc::<AttrSet<'gc>>().is_some() {
"set"
} else if val.as_gc::<List<'gc>>().is_some() {
"list"
} else if val.as_gc::<Closure<'gc>>().is_some()
|| val.as_inline::<PrimOp>().is_some()
|| val.as_gc::<PrimOpApp<'gc>>().is_some()
{
"lambda"
} else {
return BuiltinResult::Error(VM::err("typeOf: unknown type"));
};
let sid = ctx.strings.get(name).expect("typeOf string not interned");
BuiltinResult::Done(Value::new_inline(StringId(sid)))
}
BuiltinId::IsNull => BuiltinResult::Done(Value::new_inline(args[0].is::<Null>())),
BuiltinId::IsAttrs => {
BuiltinResult::Done(Value::new_inline(args[0].as_gc::<AttrSet<'gc>>().is_some()))
}
BuiltinId::IsBool => {
BuiltinResult::Done(Value::new_inline(args[0].as_inline::<bool>().is_some()))
}
BuiltinId::IsFloat => BuiltinResult::Done(Value::new_inline(args[0].as_float().is_some())),
BuiltinId::IsFunction => {
let v = args[0];
let is_func = v.as_gc::<Closure<'gc>>().is_some()
|| v.as_inline::<PrimOp>().is_some()
|| v.as_gc::<PrimOpApp<'gc>>().is_some();
BuiltinResult::Done(Value::new_inline(is_func))
}
BuiltinId::IsInt => {
let v = args[0];
let is_int = v.as_inline::<i32>().is_some() || v.as_gc::<i64>().is_some();
BuiltinResult::Done(Value::new_inline(is_int))
}
BuiltinId::IsList => {
BuiltinResult::Done(Value::new_inline(args[0].as_gc::<List<'gc>>().is_some()))
}
BuiltinId::IsString => {
let v = args[0];
let is_str = v.as_inline::<StringId>().is_some() || v.as_gc::<NixString>().is_some();
BuiltinResult::Done(Value::new_inline(is_str))
}
BuiltinId::IsPath => BuiltinResult::Done(Value::new_inline(false)),
BuiltinId::Length => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.length: not a list"));
};
BuiltinResult::Done(VM::make_int(list.inner.len() as i64, ctx.mc))
}
BuiltinId::Head => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.head: not a list"));
};
if list.inner.is_empty() {
return BuiltinResult::Error(VM::err("builtins.head: empty list"));
}
BuiltinResult::Done(list.inner[0])
}
BuiltinId::Tail => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.tail: not a list"));
};
if list.inner.is_empty() {
return BuiltinResult::Error(VM::err("builtins.tail: empty list"));
}
let tail = List {
inner: SmallVec::from_slice(&list.inner[1..]),
};
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, tail)))
}
BuiltinId::AttrNames => {
let Some(attrs) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.attrNames: not a set"));
};
let items: SmallVec<[Value<'gc>; 4]> =
attrs.iter().map(|(k, _)| Value::new_inline(*k)).collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items })))
}
BuiltinId::AttrValues => {
let Some(attrs) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.attrValues: not a set"));
};
let items: SmallVec<[Value<'gc>; 4]> = attrs.iter().map(|(_, v)| *v).collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items })))
}
BuiltinId::Map => {
let f = args[0];
let Some(list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.map: second argument is not a list",
));
};
if list.inner.is_empty() {
return BuiltinResult::Done(Value::new_gc(Gc::new(
ctx.mc,
List {
inner: SmallVec::new(),
},
)));
}
let new_elems: SmallVec<[Value<'gc>; 4]> = list
.inner
.iter()
.map(|elem| {
let thunk: Gc<'gc, Thunk<'gc>> = Gc::new(
ctx.mc,
RefLock::new(ThunkState::Apply {
func: f.relax(),
arg: *elem,
}),
);
Value::new_gc(thunk)
})
.collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: new_elems })))
}
BuiltinId::GenList => {
let f = args[0];
let len_val = args[1];
let Some(len) = VM::as_num(len_val) else {
return BuiltinResult::Error(VM::err(
"builtins.genList: second argument is not a number",
));
};
let super::vm::NixNum::Int(len) = len else {
return BuiltinResult::Error(VM::err(
"builtins.genList: second argument is not an integer",
));
};
if len < 0 {
return BuiltinResult::Error(VM::err("builtins.genList: negative length"));
}
let items: SmallVec<[Value<'gc>; 4]> = (0..len)
.map(|i| {
let arg = VM::make_int(i, ctx.mc);
let thunk: Gc<'gc, Thunk<'gc>> = Gc::new(
ctx.mc,
RefLock::new(ThunkState::Apply {
func: f.relax(),
arg,
}),
);
Value::new_gc(thunk)
})
.collect();
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: items })))
}
BuiltinId::ElemAt => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.elemAt: not a list"));
};
let Some(idx) = VM::as_num(args[1]) else {
return BuiltinResult::Error(VM::err("builtins.elemAt: index is not a number"));
};
let super::vm::NixNum::Int(idx) = idx else {
return BuiltinResult::Error(VM::err("builtins.elemAt: index is not an integer"));
};
if idx < 0 || idx as usize >= list.inner.len() {
return BuiltinResult::Error(VM::err(format!(
"builtins.elemAt: index {} out of bounds for list of length {}",
idx,
list.inner.len()
)));
}
BuiltinResult::Done(list.inner[idx as usize])
}
BuiltinId::GetAttr => {
let Some(name) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.getAttr: first argument is not a string",
));
};
let Some(sid) = ctx.strings.get(name) else {
return BuiltinResult::Error(VM::err(format!(
"builtins.getAttr: attribute '{}' not found",
name
)));
};
let Some(attrs) = args[1].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.getAttr: second argument is not a set",
));
};
match attrs.lookup(StringId(sid)) {
Some(v) => BuiltinResult::Done(v),
None => BuiltinResult::Error(VM::err(format!("attribute '{}' missing", name))),
}
}
BuiltinId::HasAttr => {
let Some(name) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.hasAttr: first argument is not a string",
));
};
let Some(attrs) = args[1].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.hasAttr: second argument is not a set",
));
};
let has = ctx
.strings
.get(name)
.map(|sid| attrs.has(StringId(sid)))
.unwrap_or(false);
BuiltinResult::Done(Value::new_inline(has))
}
BuiltinId::RemoveAttrs => {
let Some(attrs) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.removeAttrs: first argument is not a set",
));
};
let Some(remove_list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.removeAttrs: second argument is not a list",
));
};
let mut to_remove = Vec::new();
for item in remove_list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
if let Some(s) = VM::get_string(sv, ctx.strings)
&& let Some(sid) = ctx.strings.get(s)
{
to_remove.push(StringId(sid));
}
}
let entries: SmallVec<[(StringId, Value<'gc>); 4]> = attrs
.iter()
.filter(|(k, _)| !to_remove.contains(k))
.cloned()
.collect();
let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(new_attrs))
}
BuiltinId::IntersectAttrs => {
let Some(a) = args[0].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.intersectAttrs: first argument is not a set",
));
};
let Some(b) = args[1].as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.intersectAttrs: second argument is not a set",
));
};
let entries: SmallVec<[(StringId, Value<'gc>); 4]> =
b.iter().filter(|(k, _)| a.has(*k)).cloned().collect();
let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(new_attrs))
}
BuiltinId::ListToAttrs => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.listToAttrs: not a list"));
};
let name_sid = ctx.strings.get("name").expect("'name' not interned");
let value_sid = ctx.strings.get("value").expect("'value' not interned");
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
for item in list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
let Some(attr_set) = sv.as_gc::<AttrSet<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: element is not a set",
));
};
let Some(name_val) = attr_set.lookup(StringId(name_sid)) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: element missing 'name'",
));
};
let name_sv = force_inline_or_err!(ctx, name_val);
let Some(name_str) = VM::get_string(name_sv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: 'name' is not a string",
));
};
let Some(value_val) = attr_set.lookup(StringId(value_sid)) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: element missing 'value'",
));
};
let Some(key_sym) = ctx.strings.get(name_str) else {
return BuiltinResult::Error(VM::err(
"builtins.listToAttrs: name not interned",
));
};
entries.push((StringId(key_sym), value_val));
}
entries.sort_by_key(|(k, _)| *k);
entries.dedup_by_key(|(k, _)| *k);
let new_attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(new_attrs))
}
BuiltinId::ConcatLists => {
let Some(list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.concatLists: not a list"));
};
let mut result = SmallVec::<[Value<'gc>; 4]>::new();
for item in list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
let Some(inner) = sv.as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.concatLists: element is not a list",
));
};
result.extend(inner.inner.iter().cloned());
}
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, List { inner: result })))
}
BuiltinId::LessThan => {
match ctx
.vm
.compare_values(args[0], args[1], ctx.strings, |o| o.is_lt())
{
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Add => {
match ctx.vm.compute_binop(
super::vm::BinOpTag::Add,
args[0],
args[1],
ctx.mc,
ctx.strings,
) {
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Sub => {
match ctx
.vm
.numeric_binop(args[0], args[1], ctx.mc, i64::wrapping_sub, |a, b| a - b)
{
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Mul => {
match ctx
.vm
.numeric_binop(args[0], args[1], ctx.mc, i64::wrapping_mul, |a, b| a * b)
{
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::Div => {
match ctx.vm.compute_binop(
super::vm::BinOpTag::Div,
args[0],
args[1],
ctx.mc,
ctx.strings,
) {
Ok(v) => BuiltinResult::Done(v),
Err(e) => BuiltinResult::Error(e),
}
}
BuiltinId::BitAnd => {
let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) =
(VM::as_num(args[0]), VM::as_num(args[1]))
else {
return BuiltinResult::Error(VM::err(
"builtins.bitAnd: arguments must be integers",
));
};
BuiltinResult::Done(VM::make_int(a & b, ctx.mc))
}
BuiltinId::BitOr => {
let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) =
(VM::as_num(args[0]), VM::as_num(args[1]))
else {
return BuiltinResult::Error(VM::err("builtins.bitOr: arguments must be integers"));
};
BuiltinResult::Done(VM::make_int(a | b, ctx.mc))
}
BuiltinId::BitXor => {
let (Some(super::vm::NixNum::Int(a)), Some(super::vm::NixNum::Int(b))) =
(VM::as_num(args[0]), VM::as_num(args[1]))
else {
return BuiltinResult::Error(VM::err(
"builtins.bitXor: arguments must be integers",
));
};
BuiltinResult::Done(VM::make_int(a ^ b, ctx.mc))
}
BuiltinId::Ceil => {
if let Some(f) = args[0].as_float() {
BuiltinResult::Done(VM::make_int(f.ceil() as i64, ctx.mc))
} else if VM::as_num(args[0]).is_some() {
BuiltinResult::Done(args[0].relax())
} else {
BuiltinResult::Error(VM::err("builtins.ceil: not a number"))
}
}
BuiltinId::Floor => {
if let Some(f) = args[0].as_float() {
BuiltinResult::Done(VM::make_int(f.floor() as i64, ctx.mc))
} else if VM::as_num(args[0]).is_some() {
BuiltinResult::Done(args[0].relax())
} else {
BuiltinResult::Error(VM::err("builtins.floor: not a number"))
}
}
BuiltinId::StringLength => {
let Some(s) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.stringLength: not a string"));
};
BuiltinResult::Done(VM::make_int(s.len() as i64, ctx.mc))
}
BuiltinId::Substring => {
let Some(super::vm::NixNum::Int(start)) = VM::as_num(args[0]) else {
return BuiltinResult::Error(VM::err(
"builtins.substring: start is not an integer",
));
};
let Some(super::vm::NixNum::Int(len)) = VM::as_num(args[1]) else {
return BuiltinResult::Error(VM::err(
"builtins.substring: length is not an integer",
));
};
let Some(s) = VM::get_string(args[2], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.substring: third argument is not a string",
));
};
let start = start.max(0) as usize;
if start >= s.len() {
let ns = Gc::new(ctx.mc, NixString::new(""));
return BuiltinResult::Done(Value::new_gc(ns));
}
let end = if len < 0 {
s.len()
} else {
(start + len as usize).min(s.len())
};
let result = &s[start..end];
let ns = Gc::new(ctx.mc, NixString::new(result));
BuiltinResult::Done(Value::new_gc(ns))
}
BuiltinId::ToString => {
let v = args[0];
if let Some(s) = VM::get_string(v, ctx.strings) {
let ns = Gc::new(ctx.mc, NixString::new(s));
BuiltinResult::Done(Value::new_gc(ns))
} else if let Some(b) = v.as_inline::<bool>() {
let s = if b { "1" } else { "" };
let ns = Gc::new(ctx.mc, NixString::new(s));
BuiltinResult::Done(Value::new_gc(ns))
} else if v.is::<Null>() {
let ns = Gc::new(ctx.mc, NixString::new(""));
BuiltinResult::Done(Value::new_gc(ns))
} else if let Some(n) = VM::as_num(v) {
let s = match n {
super::vm::NixNum::Int(i) => i.to_string(),
super::vm::NixNum::Float(f) => format!("{f}"),
};
let ns = Gc::new(ctx.mc, NixString::new(s));
BuiltinResult::Done(Value::new_gc(ns))
} else {
BuiltinResult::Error(VM::err("builtins.toString: cannot coerce to string"))
}
}
BuiltinId::Abort => {
let Some(msg) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.abort: argument is not a string"));
};
BuiltinResult::Error(VmError::Uncatchable(crate::error::Error::eval_error(
format!("evaluation aborted with the following error message: '{msg}'"),
)))
}
BuiltinId::Throw => {
let Some(msg) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.throw: argument is not a string"));
};
BuiltinResult::Error(VmError::Catchable(msg.to_owned()))
}
BuiltinId::FunctionArgs => {
let v = args[0];
if let Some(closure) = v.as_gc::<Closure<'gc>>() {
if let Some(ref pattern) = closure.pattern {
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
for &name in &pattern.required {
entries.push((name, Value::new_inline(false)));
}
for &name in &pattern.optional {
entries.push((name, Value::new_inline(true)));
}
entries.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(ctx.mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
BuiltinResult::Done(Value::new_gc(attrs))
} else {
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, AttrSet::default())))
}
} else {
BuiltinResult::Done(Value::new_gc(Gc::new(ctx.mc, AttrSet::default())))
}
}
BuiltinId::FoldlStrict => FoldlStrict::call(args, ctx),
BuiltinId::Elem => {
let Some(list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.elem: second argument is not a list",
));
};
let needle = args[0];
for item in list.inner.iter() {
let sv = force_inline_or_err!(ctx, *item);
if ctx.vm.values_equal(needle, sv, ctx.strings) {
return BuiltinResult::Done(Value::new_inline(true));
}
}
BuiltinResult::Done(Value::new_inline(false))
}
BuiltinId::ReplaceStrings => {
let Some(from_list) = args[0].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: first argument is not a list",
));
};
let Some(to_list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: second argument is not a list",
));
};
let Some(s) = VM::get_string(args[2], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: third argument is not a string",
));
};
if from_list.inner.len() != to_list.inner.len() {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: lists must have same length",
));
}
let mut from_strs = Vec::new();
let mut to_strs = Vec::new();
for (f, t) in from_list.inner.iter().zip(to_list.inner.iter()) {
let fv = force_inline_or_err!(ctx, *f);
let tv = force_inline_or_err!(ctx, *t);
let Some(fs) = VM::get_string(fv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: from element is not a string",
));
};
let Some(ts) = VM::get_string(tv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.replaceStrings: to element is not a string",
));
};
from_strs.push(fs.to_owned());
to_strs.push(ts.to_owned());
}
let s = s.to_owned();
let mut result = String::new();
let mut i = 0;
while i < s.len() {
let mut found = false;
for (j, from) in from_strs.iter().enumerate() {
if from.is_empty() {
result.push_str(&to_strs[j]);
result.push(s.as_bytes()[i] as char);
i += 1;
found = true;
break;
}
if s[i..].starts_with(from.as_str()) {
result.push_str(&to_strs[j]);
i += from.len();
found = true;
break;
}
}
if !found {
result.push(s.as_bytes()[i] as char);
i += 1;
}
}
if from_strs.iter().any(|f| f.is_empty()) {
let j = from_strs
.iter()
.position(|f| f.is_empty())
.expect("just checked");
result.push_str(&to_strs[j]);
}
let ns = Gc::new(ctx.mc, NixString::new(result));
BuiltinResult::Done(Value::new_gc(ns))
}
BuiltinId::ConcatStringsSep => {
let Some(sep) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.concatStringsSep: first argument is not a string",
));
};
let sep = sep.to_owned();
let Some(list) = args[1].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err(
"builtins.concatStringsSep: second argument is not a list",
));
};
let mut result = String::new();
for (i, item) in list.inner.iter().enumerate() {
if i > 0 {
result.push_str(&sep);
}
let sv = force_inline_or_err!(ctx, *item);
let Some(s) = VM::get_string(sv, ctx.strings) else {
return BuiltinResult::Error(VM::err(
"builtins.concatStringsSep: element is not a string",
));
};
result.push_str(s);
}
let ns = Gc::new(ctx.mc, NixString::new(result));
BuiltinResult::Done(Value::new_gc(ns))
}
BuiltinId::FromJSON => {
let Some(s) = VM::get_string(args[0], ctx.strings) else {
return BuiltinResult::Error(VM::err("builtins.fromJSON: not a string"));
};
match serde_json::from_str::<serde_json::Value>(s) {
Ok(json) => {
let v = json_to_nix(&json, ctx.mc, ctx.strings);
BuiltinResult::Done(v)
}
Err(e) => BuiltinResult::Error(VM::err(format!("builtins.fromJSON: {e}"))),
}
}
BuiltinId::ToJSON => BuiltinResult::Error(VM::err("builtins.toJSON: not yet implemented")),
BuiltinId::Null => BuiltinResult::Done(Value::new_inline(Null)),
_ => BuiltinResult::Error(VM::err(format!("builtin {:?} not yet implemented", id))),
}
}
pub(super) fn dispatch_lazy_builtin<'gc>(
id: BuiltinId,
args: &PrimOpArgs<'gc>,
_arity: u8,
ctx: &PrimOpCtx<'_, 'gc>,
) -> BuiltinResult<'gc> {
match id {
BuiltinId::Seq => {
let _ = force_inline_or_err!(ctx, args[0]);
BuiltinResult::Done(args[1])
}
BuiltinId::DeepSeq => {
// TODO: deep force
let _ = force_inline_or_err!(ctx, args[0]);
BuiltinResult::Done(args[1])
}
BuiltinId::Trace => {
let sv = force_inline_or_err!(ctx, args[0]);
if let Some(s) = VM::get_string(sv, ctx.strings) {
eprintln!("trace: {s}");
} else {
eprintln!("trace: <non-string>");
}
BuiltinResult::Done(args[1])
}
BuiltinId::Warn => {
let sv = force_inline_or_err!(ctx, args[0]);
if let Some(s) = VM::get_string(sv, ctx.strings) {
eprintln!("warning: {s}");
} else {
eprintln!("warning: <non-string>");
}
BuiltinResult::Done(args[1])
}
BuiltinId::TryEval => BuiltinResult::Error(VM::err(
"builtins.tryEval: requires catch frame support (TODO)",
)),
BuiltinId::AddErrorContext => BuiltinResult::Done(args[1]),
BuiltinId::Break => BuiltinResult::Done(args[0]),
_ => BuiltinResult::Error(VM::err(format!(
"lazy builtin {:?} not yet implemented",
id
))),
}
}
fn json_to_nix<'gc>(
json: &serde_json::Value,
mc: &Mutation<'gc>,
strings: &DefaultStringInterner,
) -> Value<'gc> {
match json {
serde_json::Value::Null => Value::new_inline(Null),
serde_json::Value::Bool(b) => Value::new_inline(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
VM::make_int(i, mc)
} else if let Some(f) = n.as_f64() {
Value::new_float(f)
} else {
Value::new_inline(Null)
}
}
serde_json::Value::String(s) => {
let ns = Gc::new(mc, NixString::new(s.as_str()));
Value::new_gc(ns)
}
serde_json::Value::Array(arr) => {
let items: SmallVec<[Value<'gc>; 4]> =
arr.iter().map(|v| json_to_nix(v, mc, strings)).collect();
Value::new_gc(Gc::new(mc, List { inner: items }))
}
serde_json::Value::Object(obj) => {
let mut entries = SmallVec::<[(StringId, Value<'gc>); 4]>::new();
for (k, v) in obj {
if let Some(sym) = strings.get(k.as_str()) {
entries.push((StringId(sym), json_to_nix(v, mc, strings)));
}
}
entries.sort_by_key(|(k, _)| *k);
let attrs = Gc::new(mc, unsafe { AttrSet::from_sorted_unchecked(entries) });
Value::new_gc(attrs)
}
}
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
pub(super) struct FoldlStrict<'gc> {
op: StrictValue<'gc>,
list: Gc<'gc, List<'gc>>,
acc: StrictValue<'gc>,
index: usize,
phase: FoldlPhase<'gc>,
}
#[derive(Collect, Debug)]
#[collect(no_drop)]
enum FoldlPhase<'gc> {
CallOp,
CallPartial(StrictValue<'gc>),
}
impl<'gc> FoldlStrict<'gc> {
fn call(args: PrimOpStrictArgs<'gc>, ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> {
let op = args[0];
let nul = args[1];
let Some(list) = args[2].as_gc::<List<'gc>>() else {
return BuiltinResult::Error(VM::err("builtins.foldl': third argument is not a list"));
};
if list.inner.is_empty() {
return BuiltinResult::Done(nul.relax());
}
let state = FoldlStrict {
op,
list,
acc: nul,
index: 0,
phase: FoldlPhase::CallOp,
};
state.step(ctx)
}
fn step(self, _ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> {
let state = BuiltinState::FoldlStrict(FoldlStrict {
op: self.op,
list: self.list,
acc: self.acc,
index: self.index,
phase: FoldlPhase::CallOp,
});
call!(ctx, state, self.op, self.acc.relax())
}
fn resume(mut self, val: StrictValue<'gc>, ctx: &PrimOpCtx<'_, 'gc>) -> BuiltinResult<'gc> {
match self.phase {
FoldlPhase::CallOp => {
let partial = val;
let elem = self.list.inner[self.index];
self.phase = FoldlPhase::CallPartial(partial);
let state = BuiltinState::FoldlStrict(self);
call_and_force!(ctx, state, partial, elem)
}
FoldlPhase::CallPartial(_) => {
self.acc = val;
self.index += 1;
self.phase = FoldlPhase::CallOp;
if self.index >= self.list.inner.len() {
return BuiltinResult::Done(self.acc.relax());
}
self.step(ctx)
}
}
}
}
+2 -1
View File
@@ -3,13 +3,14 @@ use std::mem::MaybeUninit;
use gc_arena::Collect; use gc_arena::Collect;
use smallvec::SmallVec; use smallvec::SmallVec;
// FIXME: Drop???
pub(super) struct Stack<const N: usize, T> { pub(super) struct Stack<const N: usize, T> {
inner: Box<[MaybeUninit<T>; N]>, inner: Box<[MaybeUninit<T>; N]>,
len: usize, len: usize,
} }
unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack<N, T> { unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack<N, T> {
const NEEDS_TRACE: bool = T::NEEDS_TRACE; const NEEDS_TRACE: bool = true;
fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) { fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) {
for item in self.inner[..self.len].iter() { for item in self.inner[..self.len].iter() {
unsafe { unsafe {
+37 -39
View File
@@ -3,19 +3,16 @@ use std::marker::PhantomData;
use std::mem::size_of; use std::mem::size_of;
use std::ops::Deref; use std::ops::Deref;
use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue};
use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace}; use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace};
use num_enum::TryFromPrimitive;
use sealed::sealed; use sealed::sealed;
use smallvec::SmallVec; use smallvec::SmallVec;
use string_interner::{Symbol, symbol::SymbolU32}; use string_interner::{Symbol, symbol::SymbolU32};
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue}; use crate::ir::StringId;
use crate::{ir::StringId, runtime::builtins::BuiltinId};
#[sealed] #[sealed]
/// # Safety pub(crate) trait Storable {
/// TAG must be unique among all implementors.
pub(crate) unsafe trait Storable {
const TAG: (bool, u8); const TAG: (bool, u8);
} }
pub(crate) trait InlineStorable: Storable + RawStore {} pub(crate) trait InlineStorable: Storable + RawStore {}
@@ -28,14 +25,14 @@ macro_rules! define_value_types {
) => { ) => {
$( $(
#[sealed] #[sealed]
unsafe impl Storable for $itype { impl Storable for $itype {
const TAG: (bool, u8) = $itag; const TAG: (bool, u8) = $itag;
} }
impl InlineStorable for $itype {} impl InlineStorable for $itype {}
)* )*
$( $(
#[sealed] #[sealed]
unsafe impl Storable for $gtype { impl Storable for $gtype {
const TAG: (bool, u8) = $gtag; const TAG: (bool, u8) = $gtag;
} }
impl GcStorable for $gtype {} impl GcStorable for $gtype {}
@@ -119,13 +116,22 @@ define_value_types! {
/// # Nix runtime value representation /// # Nix runtime value representation
/// ///
/// NaN-boxed value fitting in 8 bytes. /// NaN-boxed value fitting in 8 bytes.
#[derive(Copy, Clone)]
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct Value<'gc> { pub(crate) struct Value<'gc> {
raw: RawBox, raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>, _marker: PhantomData<Gc<'gc, ()>>,
} }
impl Clone for Value<'_> {
#[inline]
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
_marker: PhantomData,
}
}
}
impl Default for Value<'_> { impl Default for Value<'_> {
#[inline(always)] #[inline(always)]
fn default() -> Self { fn default() -> Self {
@@ -285,23 +291,14 @@ impl fmt::Debug for NixString {
} }
} }
#[derive(Collect, Debug, Default)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct AttrSet<'gc> { pub(crate) struct AttrSet<'gc> {
entries: SmallVec<[(StringId, Value<'gc>); 4]>, pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>,
}
impl<'gc> Deref for AttrSet<'gc> {
type Target = [(StringId, Value<'gc>)];
fn deref(&self) -> &Self::Target {
&self.entries
}
} }
impl<'gc> AttrSet<'gc> { impl<'gc> AttrSet<'gc> {
pub(crate) unsafe fn from_sorted_unchecked( pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key)); debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries } Self { entries }
} }
@@ -310,11 +307,13 @@ impl<'gc> AttrSet<'gc> {
self.entries self.entries
.binary_search_by_key(&key, |(k, _)| *k) .binary_search_by_key(&key, |(k, _)| *k)
.ok() .ok()
.map(|i| self.entries[i].1) .map(|i| self.entries[i].1.clone())
} }
pub(crate) fn has(&self, key: StringId) -> bool { pub(crate) fn has(&self, key: StringId) -> bool {
self.entries.binary_search_by_key(&key, |(k, _)| *k).is_ok() self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.is_ok()
} }
pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> { pub(crate) fn merge(&self, other: &Self, mc: &Mutation<'gc>) -> Gc<'gc, Self> {
@@ -329,15 +328,15 @@ impl<'gc> AttrSet<'gc> {
while i < self.entries.len() && j < other.entries.len() { while i < self.entries.len() && j < other.entries.len() {
match self.entries[i].0.cmp(&other.entries[j].0) { match self.entries[i].0.cmp(&other.entries[j].0) {
Less => { Less => {
entries.push(self.entries[i]); entries.push(self.entries[i].clone());
i += 1; i += 1;
} }
Greater => { Greater => {
entries.push(other.entries[j]); entries.push(other.entries[j].clone());
j += 1; j += 1;
} }
Equal => { Equal => {
entries.push(other.entries[j]); entries.push(other.entries[j].clone());
i += 1; i += 1;
j += 1; j += 1;
} }
@@ -352,7 +351,7 @@ impl<'gc> AttrSet<'gc> {
} }
} }
#[derive(Collect, Debug, Default)] #[derive(Collect, Debug)]
#[collect(no_drop)] #[collect(no_drop)]
pub(crate) struct List<'gc> { pub(crate) struct List<'gc> {
pub(crate) inner: SmallVec<[Value<'gc>; 4]>, pub(crate) inner: SmallVec<[Value<'gc>; 4]>,
@@ -367,10 +366,6 @@ pub(crate) enum ThunkState<'gc> {
ip: u32, ip: u32,
env: Gc<'gc, RefLock<Env<'gc>>>, env: Gc<'gc, RefLock<Env<'gc>>>,
}, },
Apply {
func: Value<'gc>,
arg: Value<'gc>,
},
Blackhole, Blackhole,
Evaluated(Value<'gc>), Evaluated(Value<'gc>),
} }
@@ -425,20 +420,17 @@ pub(crate) struct PatternInfo {
#[derive(Clone, Copy, Debug, Collect)] #[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)] #[collect(require_static)]
pub(crate) struct PrimOp { pub(crate) struct PrimOp {
pub(crate) id: BuiltinId, pub(crate) id: u8,
pub(crate) arity: u8, pub(crate) arity: u8,
} }
impl RawStore for PrimOp { impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) { fn to_val(self, value: &mut RawValue) {
value.set_data([0, 0, 0, 0, self.id as u8, self.arity]); value.set_data([0, 0, 0, 0, self.id, self.arity]);
} }
fn from_val(value: &RawValue) -> Self { fn from_val(value: &RawValue) -> Self {
let [.., id, arity] = *value.data(); let [.., id, arity] = *value.data();
Self { Self { id, arity }
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"),
arity,
}
} }
} }
@@ -449,7 +441,6 @@ pub(crate) struct PrimOpApp<'gc> {
pub(crate) args: SmallVec<[Value<'gc>; 2]>, pub(crate) args: SmallVec<[Value<'gc>; 2]>,
} }
#[derive(Copy, Clone, Default)]
#[repr(transparent)] #[repr(transparent)]
pub(crate) struct StrictValue<'gc>(Value<'gc>); pub(crate) struct StrictValue<'gc>(Value<'gc>);
@@ -464,7 +455,7 @@ impl<'gc> StrictValue<'gc> {
} }
#[inline] #[inline]
pub(crate) fn relax(self) -> Value<'gc> { pub(crate) fn into_relaxed(self) -> Value<'gc> {
self.0 self.0
} }
} }
@@ -477,6 +468,13 @@ impl<'gc> Deref for StrictValue<'gc> {
} }
} }
impl Clone for StrictValue<'_> {
#[inline]
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl fmt::Debug for StrictValue<'_> { impl fmt::Debug for StrictValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f) fmt::Debug::fmt(&self.0, f)
+1790 -982
View File
File diff suppressed because it is too large Load Diff
+7 -4
View File
@@ -69,10 +69,13 @@ impl Store for DaemonStore {
fn ensure_path(&self, path: &str) -> Result<()> { fn ensure_path(&self, path: &str) -> Result<()> {
self.block_on(async { self.block_on(async {
self.connection.ensure_path(path).await.map_err(|e| { self.connection.ensure_path(path).await.map_err(|e| {
Error::eval_error(format!( Error::eval_error(
"builtins.storePath: path '{}' is not valid in nix store: {}", format!(
path, e "builtins.storePath: path '{}' is not valid in nix store: {}",
)) path, e
),
None,
)
}) })
}) })
} }
+37 -34
View File
@@ -2,72 +2,75 @@ use crate::error::{Error, Result};
pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> { pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> {
if !path.starts_with(store_dir) { if !path.starts_with(store_dir) {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"path '{}' is not in the Nix store", format!("path '{}' is not in the Nix store", path),
path None,
))); ));
} }
let relative = path let relative = path
.strip_prefix(store_dir) .strip_prefix(store_dir)
.and_then(|s| s.strip_prefix('/')) .and_then(|s| s.strip_prefix('/'))
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path)))?; .ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path), None))?;
if relative.is_empty() { if relative.is_empty() {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"store path cannot be store directory itself: {}", format!("store path cannot be store directory itself: {}", path),
path None,
))); ));
} }
let parts: Vec<&str> = relative.splitn(2, '-').collect(); let parts: Vec<&str> = relative.splitn(2, '-').collect();
if parts.len() != 2 { if parts.len() != 2 {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"invalid store path format (missing name): {}", format!("invalid store path format (missing name): {}", path),
path None,
))); ));
} }
let hash = parts[0]; let hash = parts[0];
let name = parts[1]; let name = parts[1];
if hash.len() != 32 { if hash.len() != 32 {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"invalid store path hash length (expected 32, got {}): {}", format!(
hash.len(), "invalid store path hash length (expected 32, got {}): {}",
hash hash.len(),
))); hash
),
None,
));
} }
for ch in hash.chars() { for ch in hash.chars() {
if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') { if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"invalid character '{}' in store path hash: {}", format!("invalid character '{}' in store path hash: {}", ch, hash),
ch, hash None,
))); ));
} }
} }
if name.is_empty() { if name.is_empty() {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"store path has empty name: {}", format!("store path has empty name: {}", path),
path None,
))); ));
} }
if name.starts_with('.') { if name.starts_with('.') {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"store path name cannot start with '.': {}", format!("store path name cannot start with '.': {}", name),
name None,
))); ));
} }
for ch in name.chars() { for ch in name.chars() {
if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') { if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') {
return Err(Error::eval_error(format!( return Err(Error::eval_error(
"invalid character '{}' in store path name: {}", format!("invalid character '{}' in store path name: {}", ch, name),
ch, name None,
))); ));
} }
} }
+69
View File
@@ -0,0 +1,69 @@
use fix::value::Value;
use crate::utils::{eval, eval_result};
#[test_log::test]
fn arithmetic() {
assert_eq!(eval("1 + 1"), Value::Int(2));
}
#[test_log::test]
fn simple_function_application() {
assert_eq!(eval("(x: x) 1"), Value::Int(1));
}
#[test_log::test]
fn curried_function() {
assert_eq!(eval("(x: y: x - y) 2 1"), Value::Int(1));
}
#[test_log::test]
fn rec_attrset() {
assert_eq!(eval("rec { b = a; a = 1; }.b"), Value::Int(1));
}
#[test_log::test]
fn let_binding() {
assert_eq!(eval("let b = a; a = 1; in b"), Value::Int(1));
}
#[test_log::test]
fn fibonacci() {
assert_eq!(
eval(
"let fib = n: if n == 1 || n == 2 then 1 else (fib (n - 1)) + (fib (n - 2)); in fib 30"
),
Value::Int(832040)
);
}
#[test_log::test]
fn fixed_point_combinator() {
assert_eq!(
eval("((f: let x = f x; in x)(self: { x = 1; y = self.x + 1; })).y"),
Value::Int(2)
);
}
#[test_log::test]
fn conditional_true() {
assert_eq!(eval("if true then 1 else 0"), Value::Int(1));
}
#[test_log::test]
fn conditional_false() {
assert_eq!(eval("if false then 1 else 0"), Value::Int(0));
}
#[test_log::test]
fn nested_let() {
assert_eq!(
eval("let x = 1; in let y = x + 1; z = y + 1; in z"),
Value::Int(3)
);
}
#[test_log::test]
fn rec_inherit_fails() {
assert!(eval_result("{ inherit x; }").is_err());
}
+326
View File
@@ -0,0 +1,326 @@
use std::collections::BTreeMap;
use fix::value::{AttrSet, List, Value};
use crate::utils::eval;
#[test_log::test]
fn builtins_accessible() {
let result = eval("builtins");
assert!(matches!(result, Value::AttrSet(_)));
}
#[test_log::test]
fn builtins_self_reference() {
let result = eval("builtins.builtins");
assert!(matches!(result, Value::AttrSet(_)));
}
#[test_log::test]
fn builtins_add() {
assert_eq!(eval("builtins.add 1 2"), Value::Int(3));
}
#[test_log::test]
fn builtins_length() {
assert_eq!(eval("builtins.length [1 2 3]"), Value::Int(3));
}
#[test_log::test]
fn builtins_map() {
assert_eq!(
eval("builtins.map (x: x * 2) [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(4), Value::Int(6)]))
);
}
#[test_log::test]
fn builtins_filter() {
assert_eq!(
eval("builtins.filter (x: x > 1) [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(3)]))
);
}
#[test_log::test]
fn builtins_attrnames() {
let result = eval("builtins.attrNames { a = 1; b = 2; }");
assert!(matches!(result, Value::List(_)));
if let Value::List(list) = result {
assert_eq!(format!("{:?}", list).matches(',').count() + 1, 2);
}
}
#[test_log::test]
fn builtins_head() {
assert_eq!(eval("builtins.head [1 2 3]"), Value::Int(1));
}
#[test_log::test]
fn builtins_tail() {
assert_eq!(
eval("builtins.tail [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(3)]))
);
}
#[test_log::test]
fn builtins_in_let() {
assert_eq!(eval("let b = builtins; in b.add 5 3"), Value::Int(8));
}
#[test_log::test]
fn builtins_in_with() {
assert_eq!(eval("with builtins; add 10 20"), Value::Int(30));
}
#[test_log::test]
fn builtins_nested_calls() {
assert_eq!(
eval("builtins.add (builtins.mul 2 3) (builtins.sub 10 5)"),
Value::Int(11)
);
}
#[test_log::test]
fn builtins_is_list() {
assert_eq!(eval("builtins.isList [1 2 3]"), Value::Bool(true));
}
#[test_log::test]
fn builtins_is_attrs() {
assert_eq!(eval("builtins.isAttrs { a = 1; }"), Value::Bool(true));
}
#[test_log::test]
fn builtins_is_function() {
assert_eq!(eval("builtins.isFunction (x: x)"), Value::Bool(true));
}
#[test_log::test]
fn builtins_is_null() {
assert_eq!(eval("builtins.isNull null"), Value::Bool(true));
}
#[test_log::test]
fn builtins_is_bool() {
assert_eq!(eval("builtins.isBool true"), Value::Bool(true));
}
#[test_log::test]
fn builtins_shadowing() {
assert_eq!(
eval("let builtins = { add = x: y: x - y; }; in builtins.add 5 3"),
Value::Int(2)
);
}
#[test_log::test]
fn builtins_lazy_evaluation() {
let result = eval("builtins.builtins.builtins.add 1 1");
assert_eq!(result, Value::Int(2));
}
#[test_log::test]
fn builtins_foldl() {
assert_eq!(
eval("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4 5]"),
Value::Int(15)
);
}
#[test_log::test]
fn builtins_elem() {
assert_eq!(eval("builtins.elem 2 [1 2 3]"), Value::Bool(true));
assert_eq!(eval("builtins.elem 5 [1 2 3]"), Value::Bool(false));
}
#[test_log::test]
fn builtins_concat_lists() {
assert_eq!(
eval("builtins.concatLists [[1 2] [3 4] [5]]"),
Value::List(List::new(vec![
Value::Int(1),
Value::Int(2),
Value::Int(3),
Value::Int(4),
Value::Int(5)
]))
);
}
#[test_log::test]
fn builtins_compare_versions_basic() {
assert_eq!(
eval("builtins.compareVersions \"1.0\" \"2.3\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.1\" \"2.3\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3\" \"2.3\""),
Value::Int(0)
);
assert_eq!(
eval("builtins.compareVersions \"2.5\" \"2.3\""),
Value::Int(1)
);
assert_eq!(
eval("builtins.compareVersions \"3.1\" \"2.3\""),
Value::Int(1)
);
}
#[test_log::test]
fn builtins_compare_versions_components() {
assert_eq!(
eval("builtins.compareVersions \"2.3.1\" \"2.3\""),
Value::Int(1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3\" \"2.3.1\""),
Value::Int(-1)
);
}
#[test_log::test]
fn builtins_compare_versions_numeric_vs_alpha() {
// Numeric component comes before alpha component
assert_eq!(
eval("builtins.compareVersions \"2.3.1\" \"2.3a\""),
Value::Int(1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3a\" \"2.3.1\""),
Value::Int(-1)
);
}
#[test_log::test]
fn builtins_compare_versions_pre() {
// "pre" is special: comes before everything except another "pre"
assert_eq!(
eval("builtins.compareVersions \"2.3pre1\" \"2.3\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3pre3\" \"2.3pre12\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3pre1\" \"2.3c\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3pre1\" \"2.3q\""),
Value::Int(-1)
);
}
#[test_log::test]
fn builtins_compare_versions_alpha() {
// Alphabetic comparison
assert_eq!(
eval("builtins.compareVersions \"2.3a\" \"2.3c\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3c\" \"2.3a\""),
Value::Int(1)
);
}
#[test_log::test]
fn builtins_compare_versions_symmetry() {
// Test symmetry: compareVersions(a, b) == -compareVersions(b, a)
assert_eq!(
eval("builtins.compareVersions \"1.0\" \"2.3\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"2.3\" \"1.0\""),
Value::Int(1)
);
}
#[test_log::test]
fn builtins_compare_versions_complex() {
// Complex version strings with multiple components
assert_eq!(
eval("builtins.compareVersions \"1.2.3.4\" \"1.2.3.5\""),
Value::Int(-1)
);
assert_eq!(
eval("builtins.compareVersions \"1.2.10\" \"1.2.9\""),
Value::Int(1)
);
assert_eq!(
eval("builtins.compareVersions \"1.2a3\" \"1.2a10\""),
Value::Int(-1)
);
}
#[test_log::test]
fn builtins_generic_closure() {
assert_eq!(
eval(
"with builtins; length (genericClosure { startSet = [ { key = 1; } ]; operator = { key }: [ { key = key / 1.; } ]; a = 1; })"
),
Value::Int(1),
);
assert_eq!(
eval(
"with builtins; (elemAt (genericClosure { startSet = [ { key = 1; } ]; operator = { key }: [ { key = key / 1.; } ]; a = 1; }) 0).key"
),
Value::Int(1),
);
}
#[test_log::test]
fn builtins_function_args() {
assert_eq!(
eval("builtins.functionArgs (x: 1)"),
Value::AttrSet(AttrSet::default())
);
assert_eq!(
eval("builtins.functionArgs ({}: 1)"),
Value::AttrSet(AttrSet::default())
);
assert_eq!(
eval("builtins.functionArgs ({...}: 1)"),
Value::AttrSet(AttrSet::default())
);
assert_eq!(
eval("builtins.functionArgs ({a}: 1)"),
Value::AttrSet(AttrSet::new(BTreeMap::from([(
"a".into(),
Value::Bool(false)
)])))
);
assert_eq!(
eval("builtins.functionArgs ({a, b ? 1}: 1)"),
Value::AttrSet(AttrSet::new(BTreeMap::from([
("a".into(), Value::Bool(false)),
("b".into(), Value::Bool(true))
])))
);
assert_eq!(
eval("builtins.functionArgs ({a, b ? 1, ...}: 1)"),
Value::AttrSet(AttrSet::new(BTreeMap::from([
("a".into(), Value::Bool(false)),
("b".into(), Value::Bool(true))
])))
);
}
#[test_log::test]
fn builtins_parse_drv_name() {
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("version"),
Some(&Value::String("0.1.0pre".into()))
);
}
+193
View File
@@ -0,0 +1,193 @@
use fix::value::Value;
use crate::utils::eval_result;
#[test_log::test]
fn to_file_simple() {
let result =
eval_result(r#"builtins.toFile "hello.txt" "Hello, World!""#).expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-hello.txt"));
assert!(std::path::Path::new(&path).exists());
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "Hello, World!");
}
_ => panic!("Expected string, got {:?}", result),
}
}
#[test_log::test]
fn to_file_with_references() {
let result = eval_result(
r#"
let
dep = builtins.toFile "dep.txt" "dependency";
in
builtins.toFile "main.txt" "Reference: ${dep}"
"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-main.txt"));
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert!(contents.contains("Reference: "));
assert!(contents.contains("-dep.txt"));
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_invalid_name_with_slash() {
let result = eval_result(r#"builtins.toFile "foo/bar.txt" "content""#);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("name cannot contain '/'")
);
}
#[test_log::test]
fn to_file_invalid_name_dot() {
let result = eval_result(r#"builtins.toFile "." "content""#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid name"));
}
#[test_log::test]
fn to_file_invalid_name_dotdot() {
let result = eval_result(r#"builtins.toFile ".." "content""#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid name"));
}
#[test_log::test]
fn store_path_validation_not_in_store() {
let result = eval_result(r#"builtins.storePath "/tmp/foo""#);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not in the Nix store")
);
}
#[test_log::test]
fn store_path_validation_malformed_hash() {
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
.expect("Failed to create dummy file");
let dummy_path = match dummy_file_result {
Value::String(ref p) => p.clone(),
_ => panic!("Expected string"),
};
let store_dir = std::path::Path::new(&dummy_path)
.parent()
.expect("Failed to get parent dir")
.to_str()
.expect("Failed to convert to string");
let test_path = format!("{}/invalid-hash-hello", store_dir);
let result = eval_result(&format!(r#"builtins.storePath "{}""#, test_path));
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("invalid") || err_str.contains("hash"),
"Expected hash validation error, got: {}",
err_str
);
}
#[test_log::test]
fn store_path_validation_missing_name() {
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
.expect("Failed to create dummy file");
let dummy_path = match dummy_file_result {
Value::String(ref p) => p.clone(),
_ => panic!("Expected string"),
};
let store_dir = std::path::Path::new(&dummy_path)
.parent()
.expect("Failed to get parent dir")
.to_str()
.expect("Failed to convert to string");
let test_path = format!("{}/abcd1234abcd1234abcd1234abcd1234", store_dir);
let result = eval_result(&format!(r#"builtins.storePath "{}""#, test_path));
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("missing name") || err_str.contains("format"),
"Expected missing name error, got: {}",
err_str
);
}
#[test_log::test]
fn to_file_curried_application() {
let result = eval_result(
r#"
let
makeFile = builtins.toFile "test.txt";
in
makeFile "test content"
"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-test.txt"));
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "test content");
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_number_conversion() {
let result = eval_result(r#"builtins.toFile "number.txt" (builtins.toString 42)"#)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "42");
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_list_conversion() {
let result = eval_result(
r#"builtins.toFile "list.txt" (builtins.concatStringsSep "\n" ["line1" "line2" "line3"])"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "line1\nline2\nline3");
}
_ => panic!("Expected string"),
}
}
+74
View File
@@ -0,0 +1,74 @@
use fix::value::{List, Value};
use crate::utils::{eval, eval_result};
#[test_log::test]
fn true_literal() {
assert_eq!(eval("true"), Value::Bool(true));
}
#[test_log::test]
fn false_literal() {
assert_eq!(eval("false"), Value::Bool(false));
}
#[test_log::test]
fn null_literal() {
assert_eq!(eval("null"), Value::Null);
}
#[test_log::test]
fn map_function() {
assert_eq!(
eval("map (x: x * 2) [1 2 3]"),
Value::List(List::new(vec![Value::Int(2), Value::Int(4), Value::Int(6)]))
);
}
#[test_log::test]
fn is_null_function() {
assert_eq!(eval("isNull null"), Value::Bool(true));
assert_eq!(eval("isNull 5"), Value::Bool(false));
}
#[test_log::test]
fn shadow_true() {
assert_eq!(eval("let true = false; in true"), Value::Bool(false));
}
#[test_log::test]
fn shadow_map() {
assert_eq!(eval("let map = x: y: x; in map 1 2"), Value::Int(1));
}
#[test_log::test]
fn mixed_usage() {
assert_eq!(
eval("if true then map (x: x + 1) [1 2] else []"),
Value::List(List::new(vec![Value::Int(2), Value::Int(3)]))
);
}
#[test_log::test]
fn in_let_bindings() {
assert_eq!(
eval("let x = true; y = false; in x && y"),
Value::Bool(false)
);
}
#[test_log::test]
fn shadow_in_function() {
assert_eq!(eval("(true: true) false"), Value::Bool(false));
}
#[test_log::test]
fn throw_function() {
let result = eval_result("throw \"error message\"");
assert!(result.is_err());
}
#[test_log::test]
fn to_string_function() {
assert_eq!(eval("toString 42"), Value::String("42".to_string()));
}
+120
View File
@@ -0,0 +1,120 @@
use fix::value::Value;
use crate::utils::{eval, eval_result};
#[test_log::test]
fn required_parameters() {
assert_eq!(eval("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
}
#[test_log::test]
fn missing_required_parameter() {
let result = eval_result("({ a, b }: a + b) { a = 1; }");
assert!(result.is_err());
}
#[test_log::test]
fn all_required_parameters_present() {
assert_eq!(
eval("({ x, y, z }: x + y + z) { x = 1; y = 2; z = 3; }"),
Value::Int(6)
);
}
#[test_log::test]
fn reject_unexpected_arguments() {
let result = eval_result("({ a, b }: a + b) { a = 1; b = 2; c = 3; }");
assert!(result.is_err());
}
#[test_log::test]
fn ellipsis_accepts_extra_arguments() {
assert_eq!(
eval("({ a, b, ... }: a + b) { a = 1; b = 2; c = 3; }"),
Value::Int(3)
);
}
#[test_log::test]
fn default_parameters() {
assert_eq!(eval("({ a, b ? 5 }: a + b) { a = 1; }"), Value::Int(6));
}
#[test_log::test]
fn override_default_parameter() {
assert_eq!(
eval("({ a, b ? 5 }: a + b) { a = 1; b = 10; }"),
Value::Int(11)
);
}
#[test_log::test]
fn at_pattern_alias() {
assert_eq!(
eval("(args@{ a, b }: args.a + args.b) { a = 1; b = 2; }"),
Value::Int(3)
);
}
#[test_log::test]
fn simple_parameter_no_validation() {
assert_eq!(eval("(x: x.a + x.b) { a = 1; b = 2; }"), Value::Int(3));
}
#[test_log::test]
fn simple_parameter_accepts_any_argument() {
assert_eq!(eval("(x: x) 42"), Value::Int(42));
}
#[test_log::test]
fn nested_function_parameters() {
assert_eq!(
eval("({ a }: { b }: a + b) { a = 5; } { b = 3; }"),
Value::Int(8)
);
}
#[test_log::test]
fn pattern_param_simple_reference_in_default() {
assert_eq!(eval("({ a, b ? a }: b) { a = 10; }"), Value::Int(10));
}
#[test_log::test]
fn pattern_param_multiple_references_in_default() {
assert_eq!(
eval("({ a, b ? a + 5, c ? 1 }: b + c) { a = 10; }"),
Value::Int(16)
);
}
#[test_log::test]
fn pattern_param_mutual_reference() {
assert_eq!(
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; }"),
Value::Int(6)
);
}
#[test_log::test]
fn pattern_param_override_mutual_reference() {
assert_eq!(
eval("({ a, b ? c + 1, c ? 5 }: b) { a = 1; c = 10; }"),
Value::Int(11)
);
}
#[test_log::test]
fn pattern_param_reference_list() {
assert_eq!(
eval("({ a, b ? [ a 2 ] }: builtins.elemAt b 0) { a = 42; }"),
Value::Int(42)
);
}
#[test_log::test]
fn pattern_param_alias_in_default() {
assert_eq!(
eval("(args@{ a, b ? args.a + 10 }: b) { a = 5; }"),
Value::Int(15)
);
}
-190
View File
@@ -366,193 +366,3 @@ fn read_dir_on_file_fails() {
let err_msg = result.unwrap_err().to_string(); let err_msg = result.unwrap_err().to_string();
assert!(err_msg.contains("not a directory")); assert!(err_msg.contains("not a directory"));
} }
#[test_log::test]
fn to_file_simple() {
let result =
eval_result(r#"builtins.toFile "hello.txt" "Hello, World!""#).expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-hello.txt"));
assert!(std::path::Path::new(&path).exists());
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "Hello, World!");
}
_ => panic!("Expected string, got {:?}", result),
}
}
#[test_log::test]
fn to_file_with_references() {
let result = eval_result(
r#"
let
dep = builtins.toFile "dep.txt" "dependency";
in
builtins.toFile "main.txt" "Reference: ${dep}"
"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-main.txt"));
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert!(contents.contains("Reference: "));
assert!(contents.contains("-dep.txt"));
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_invalid_name_with_slash() {
let result = eval_result(r#"builtins.toFile "foo/bar.txt" "content""#);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("name cannot contain '/'")
);
}
#[test_log::test]
fn to_file_invalid_name_dot() {
let result = eval_result(r#"builtins.toFile "." "content""#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid name"));
}
#[test_log::test]
fn to_file_invalid_name_dotdot() {
let result = eval_result(r#"builtins.toFile ".." "content""#);
assert!(result.is_err());
assert!(result.unwrap_err().to_string().contains("invalid name"));
}
#[test_log::test]
fn store_path_validation_not_in_store() {
let result = eval_result(r#"builtins.storePath "/tmp/foo""#);
assert!(result.is_err());
assert!(
result
.unwrap_err()
.to_string()
.contains("not in the Nix store")
);
}
#[test_log::test]
fn store_path_validation_malformed_hash() {
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
.expect("Failed to create dummy file");
let dummy_path = match dummy_file_result {
Value::String(ref p) => p.clone(),
_ => panic!("Expected string"),
};
let store_dir = std::path::Path::new(&dummy_path)
.parent()
.expect("Failed to get parent dir")
.to_str()
.expect("Failed to convert to string");
let test_path = format!("{}/invalid-hash-hello", store_dir);
let result = eval_result(&format!(r#"builtins.storePath "{}""#, test_path));
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("invalid") || err_str.contains("hash"),
"Expected hash validation error, got: {}",
err_str
);
}
#[test_log::test]
fn store_path_validation_missing_name() {
let dummy_file_result = eval_result(r#"builtins.toFile "dummy.txt" "content""#)
.expect("Failed to create dummy file");
let dummy_path = match dummy_file_result {
Value::String(ref p) => p.clone(),
_ => panic!("Expected string"),
};
let store_dir = std::path::Path::new(&dummy_path)
.parent()
.expect("Failed to get parent dir")
.to_str()
.expect("Failed to convert to string");
let test_path = format!("{}/abcd1234abcd1234abcd1234abcd1234", store_dir);
let result = eval_result(&format!(r#"builtins.storePath "{}""#, test_path));
assert!(result.is_err());
let err_str = result.unwrap_err().to_string();
assert!(
err_str.contains("missing name") || err_str.contains("format"),
"Expected missing name error, got: {}",
err_str
);
}
#[test_log::test]
fn to_file_curried_application() {
let result = eval_result(
r#"
let
makeFile = builtins.toFile "test.txt";
in
makeFile "test content"
"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
assert!(path.contains("-test.txt"));
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "test content");
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_number_conversion() {
let result = eval_result(r#"builtins.toFile "number.txt" (builtins.toString 42)"#)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "42");
}
_ => panic!("Expected string"),
}
}
#[test_log::test]
fn to_file_list_conversion() {
let result = eval_result(
r#"builtins.toFile "list.txt" (builtins.concatStringsSep "\n" ["line1" "line2" "line3"])"#,
)
.expect("Failed to evaluate");
match result {
Value::String(path) => {
let contents = std::fs::read_to_string(&path).expect("Failed to read file");
assert_eq!(contents, "line1\nline2\nline3");
}
_ => panic!("Expected string"),
}
}
+11
View File
@@ -1,6 +1,17 @@
mod basic_eval;
mod builtins;
mod builtins_store;
mod derivation; mod derivation;
mod findfile; mod findfile;
mod free_globals;
mod functions;
mod io_operations; mod io_operations;
mod lang; mod lang;
mod numeric_types;
mod operators;
mod path_operations;
mod regex;
mod string_context; mod string_context;
mod thunk_scope;
mod to_string;
mod utils; mod utils;
+138
View File
@@ -0,0 +1,138 @@
use fix::value::Value;
use crate::utils::eval;
#[test_log::test]
fn large_i64_max() {
assert_eq!(eval("9223372036854775807"), Value::Int(9223372036854775807));
}
#[test_log::test]
fn large_i64_negative() {
assert_eq!(
eval("-9223372036854775807"),
Value::Int(-9223372036854775807)
);
}
#[test_log::test]
fn large_number_arithmetic() {
assert_eq!(
eval("5000000000000000000 + 3000000000000000000"),
Value::Int(8000000000000000000i64)
);
}
#[test_log::test]
fn is_int_with_int() {
assert_eq!(eval("builtins.isInt 42"), Value::Bool(true));
}
#[test_log::test]
fn is_int_with_float() {
assert_eq!(eval("builtins.isInt 42.0"), Value::Bool(false));
}
#[test_log::test]
fn is_float_with_int() {
assert_eq!(eval("builtins.isFloat 42"), Value::Bool(false));
}
#[test_log::test]
fn is_float_with_float() {
assert_eq!(eval("builtins.isFloat 42.5"), Value::Bool(true));
assert_eq!(eval("builtins.isFloat 1.0"), Value::Bool(true));
}
#[test_log::test]
fn typeof_int() {
assert_eq!(eval("builtins.typeOf 1"), Value::String("int".to_string()));
}
#[test_log::test]
fn typeof_float() {
assert_eq!(
eval("builtins.typeOf 1.0"),
Value::String("float".to_string())
);
assert_eq!(
eval("builtins.typeOf 3.14"),
Value::String("float".to_string())
);
}
#[test_log::test]
fn int_literal() {
assert_eq!(eval("1"), Value::Int(1));
}
#[test_log::test]
fn float_literal() {
assert_eq!(eval("1."), Value::Float(1.));
}
#[test_log::test]
fn int_plus_int() {
assert_eq!(
eval("builtins.typeOf (1 + 2)"),
Value::String("int".to_string())
);
}
#[test_log::test]
fn int_plus_float() {
assert_eq!(
eval("builtins.typeOf (1 + 2.0)"),
Value::String("float".to_string())
);
}
#[test_log::test]
fn int_times_int() {
assert_eq!(
eval("builtins.typeOf (3 * 4)"),
Value::String("int".to_string())
);
}
#[test_log::test]
fn int_times_float() {
assert_eq!(
eval("builtins.typeOf (3 * 4.0)"),
Value::String("float".to_string())
);
}
#[test_log::test]
fn integer_division() {
assert_eq!(eval("5 / 2"), Value::Int(2));
assert_eq!(eval("7 / 3"), Value::Int(2));
assert_eq!(eval("10 / 3"), Value::Int(3));
}
#[test_log::test]
fn float_division() {
assert_eq!(eval("5 / 2.0"), Value::Float(2.5));
assert_eq!(eval("7.0 / 2"), Value::Float(3.5));
}
#[test_log::test]
fn negative_integer_division() {
assert_eq!(eval("(-7) / 3"), Value::Int(-2));
}
#[test_log::test]
fn builtin_add_with_large_numbers() {
assert_eq!(
eval("builtins.add 5000000000000000000 3000000000000000000"),
Value::Int(8000000000000000000i64)
);
}
#[test_log::test]
fn builtin_mul_with_large_numbers() {
assert_eq!(
eval("builtins.mul 1000000000 1000000000"),
Value::Int(1000000000000000000i64)
);
}
+144
View File
@@ -0,0 +1,144 @@
use std::collections::BTreeMap;
use fix::value::{AttrSet, List, Symbol, Value};
use crate::utils::eval;
#[test_log::test]
fn addition() {
assert_eq!(eval("1 + 1"), Value::Int(2));
}
#[test_log::test]
fn subtraction() {
assert_eq!(eval("2 - 1"), Value::Int(1));
}
#[test_log::test]
fn multiplication() {
assert_eq!(eval("1. * 1"), Value::Float(1.));
}
#[test_log::test]
fn division() {
assert_eq!(eval("1 / 1."), Value::Float(1.));
}
#[test_log::test]
fn equality() {
assert_eq!(eval("1 == 1"), Value::Bool(true));
}
#[test_log::test]
fn inequality() {
assert_eq!(eval("1 != 1"), Value::Bool(false));
}
#[test_log::test]
fn less_than() {
assert_eq!(eval("2 < 1"), Value::Bool(false));
}
#[test_log::test]
fn greater_than() {
assert_eq!(eval("2 > 1"), Value::Bool(true));
}
#[test_log::test]
fn less_than_or_equal() {
assert_eq!(eval("1 <= 1"), Value::Bool(true));
}
#[test_log::test]
fn greater_than_or_equal() {
assert_eq!(eval("1 >= 1"), Value::Bool(true));
}
#[test_log::test]
fn logical_or_short_circuit() {
assert_eq!(eval("true || (1 / 0)"), Value::Bool(true));
}
#[test_log::test]
fn logical_and() {
assert_eq!(eval("true && 1 == 0"), Value::Bool(false));
}
#[test_log::test]
fn list_concatenation() {
assert_eq!(
eval("[ 1 2 3 ] ++ [ 4 5 6 ]"),
Value::List(List::new((1..=6).map(Value::Int).collect()))
);
}
#[test_log::test]
fn attrset_update() {
assert_eq!(
eval("{ a.b = 1; b = 2; } // { a.c = 2; }"),
Value::AttrSet(AttrSet::new(BTreeMap::from([
(
Symbol::from("a"),
Value::AttrSet(AttrSet::new(BTreeMap::from([(
Symbol::from("c"),
Value::Int(2),
)]))),
),
(Symbol::from("b"), Value::Int(2)),
])))
);
}
#[test_log::test]
fn unary_negation() {
assert_eq!(eval("-5"), Value::Int(-5));
}
#[test_log::test]
fn logical_not() {
assert_eq!(eval("!true"), Value::Bool(false));
assert_eq!(eval("!false"), Value::Bool(true));
}
#[test_log::test]
fn select_with_default_lazy_evaluation() {
assert_eq!(eval("{ a = 1; }.a or (1 / 0)"), Value::Int(1));
}
#[test_log::test]
fn select_with_default_nested_lazy() {
assert_eq!(
eval("{ a.b = 42; }.a.b or (builtins.abort \"should not evaluate\")"),
Value::Int(42)
);
}
#[test_log::test]
fn select_with_default_fallback() {
assert_eq!(eval("{ a = 1; }.b or 999"), Value::Int(999));
}
#[test_log::test]
fn implication_false_false() {
assert_eq!(eval("false -> false"), Value::Bool(true));
}
#[test_log::test]
fn implication_false_true() {
assert_eq!(eval("false -> true"), Value::Bool(true));
}
#[test_log::test]
fn implication_true_false() {
assert_eq!(eval("true -> false"), Value::Bool(false));
}
#[test_log::test]
fn implication_true_true() {
assert_eq!(eval("true -> true"), Value::Bool(true));
}
#[test_log::test]
fn implication_short_circuit() {
assert_eq!(eval("false -> (1 / 0)"), Value::Bool(true));
}
+117
View File
@@ -0,0 +1,117 @@
use fix::value::Value;
use crate::utils::{eval, eval_result};
#[test_log::test]
fn path_type_of() {
let result = eval("builtins.typeOf ./foo");
assert_eq!(result, Value::String("path".to_string()));
}
#[test_log::test]
fn is_path_true() {
let result = eval("builtins.isPath ./foo");
assert_eq!(result, Value::Bool(true));
}
#[test_log::test]
fn is_path_false_string() {
let result = eval(r#"builtins.isPath "./foo""#);
assert_eq!(result, Value::Bool(false));
}
#[test_log::test]
fn is_path_false_number() {
let result = eval("builtins.isPath 42");
assert_eq!(result, Value::Bool(false));
}
#[test_log::test]
fn path_concat_type() {
// path + string = path
let result = eval(r#"builtins.typeOf (./foo + "/bar")"#);
assert_eq!(result, Value::String("path".to_string()));
}
#[test_log::test]
fn string_path_concat_type() {
// string + path = string
let result = eval(r#"builtins.typeOf ("prefix-" + ./foo)"#);
assert_eq!(result, Value::String("string".to_string()));
}
#[test_log::test]
fn basename_of_path() {
let result = eval("builtins.baseNameOf ./path/to/file.nix");
assert!(matches!(result, Value::String(s) if s == "file.nix"));
}
#[test_log::test]
fn basename_of_string() {
let result = eval(r#"builtins.baseNameOf "/path/to/file.nix""#);
assert_eq!(result, Value::String("file.nix".to_string()));
}
#[test_log::test]
fn dir_of_path_type() {
// dirOf preserves path type
let result = eval("builtins.typeOf (builtins.dirOf ./path/to/file.nix)");
assert_eq!(result, Value::String("path".to_string()));
}
#[test_log::test]
fn dir_of_string_type() {
// dirOf preserves string type
let result = eval(r#"builtins.typeOf (builtins.dirOf "/path/to/file.nix")"#);
assert_eq!(result, Value::String("string".to_string()));
}
#[test_log::test]
fn path_equality() {
// Same path should be equal
let result = eval("./foo == ./foo");
assert_eq!(result, Value::Bool(true));
}
#[test_log::test]
fn path_not_equal_string() {
// Paths and strings are different types - should not be equal
let result = eval(r#"./foo == "./foo""#);
assert_eq!(result, Value::Bool(false));
}
#[test_log::test]
fn to_path_absolute() {
// toPath with absolute path returns string
let result = eval(r#"builtins.toPath "/foo/bar""#);
assert_eq!(result, Value::String("/foo/bar".to_string()));
}
#[test_log::test]
fn to_path_type_is_string() {
// toPath returns a string, not a path
let result = eval(r#"builtins.typeOf (builtins.toPath "/foo")"#);
assert_eq!(result, Value::String("string".to_string()));
}
#[test_log::test]
fn to_path_relative_fails() {
// toPath with relative path should fail
let result = eval_result(r#"builtins.toPath "foo/bar""#);
assert!(result.is_err());
}
#[test_log::test]
fn to_path_empty_fails() {
// toPath with empty string should fail
let result = eval_result(r#"builtins.toPath """#);
assert!(result.is_err());
}
#[test_log::test]
fn to_path_from_path_value() {
// toPath can accept a path value too (coerces to string first)
let result = eval("builtins.toPath ./foo");
// Should succeed and return the absolute path as a string
assert!(matches!(result, Value::String(s) if s.starts_with("/")));
}
+317
View File
@@ -0,0 +1,317 @@
use fix::value::{List, Value};
use crate::utils::eval;
use crate::utils::eval_result;
#[test_log::test]
fn match_exact_full_string() {
assert_eq!(
eval(r#"builtins.match "foobar" "foobar""#),
Value::List(List::new(vec![]))
);
}
#[test_log::test]
fn match_partial_returns_null() {
assert_eq!(eval(r#"builtins.match "foo" "foobar""#), Value::Null);
}
#[test_log::test]
fn match_with_capture_groups() {
assert_eq!(
eval(r#"builtins.match "(.*)\\.nix" "foobar.nix""#),
Value::List(List::new(vec![Value::String("foobar".into())]))
);
}
#[test_log::test]
fn match_multiple_capture_groups() {
assert_eq!(
eval(r#"builtins.match "((.*)/)?([^/]*)\\.nix" "foobar.nix""#),
Value::List(List::new(vec![
Value::Null,
Value::Null,
Value::String("foobar".into())
]))
);
}
#[test_log::test]
fn match_with_path() {
assert_eq!(
eval(r#"builtins.match "((.*)/)?([^/]*)\\.nix" "/path/to/foobar.nix""#),
Value::List(List::new(vec![
Value::String("/path/to/".into()),
Value::String("/path/to".into()),
Value::String("foobar".into())
]))
);
}
#[test_log::test]
fn match_posix_space_class() {
assert_eq!(
eval(r#"builtins.match "[[:space:]]+([^[:space:]]+)[[:space:]]+" " foo ""#),
Value::List(List::new(vec![Value::String("foo".into())]))
);
}
#[test_log::test]
fn match_posix_upper_class() {
assert_eq!(
eval(r#"builtins.match "[[:space:]]+([[:upper:]]+)[[:space:]]+" " foo ""#),
Value::Null
);
assert_eq!(
eval(r#"builtins.match "[[:space:]]+([[:upper:]]+)[[:space:]]+" " FOO ""#),
Value::List(List::new(vec![Value::String("FOO".into())]))
);
}
#[test_log::test]
fn match_quantifiers() {
assert_eq!(
eval(r#"builtins.match "fo*" "f""#),
Value::List(List::new(vec![]))
);
assert_eq!(eval(r#"builtins.match "fo+" "f""#), Value::Null);
assert_eq!(
eval(r#"builtins.match "fo{1,2}" "foo""#),
Value::List(List::new(vec![]))
);
assert_eq!(eval(r#"builtins.match "fo{1,2}" "fooo""#), Value::Null);
}
#[test_log::test]
fn split_non_capturing() {
assert_eq!(
eval(r#"builtins.split "foobar" "foobar""#),
Value::List(List::new(vec![
Value::String("".into()),
Value::List(List::new(vec![])),
Value::String("".into())
]))
);
}
#[test_log::test]
fn split_no_match() {
assert_eq!(
eval(r#"builtins.split "fo+" "f""#),
Value::List(List::new(vec![Value::String("f".into())]))
);
}
#[test_log::test]
fn split_with_capture_group() {
assert_eq!(
eval(r#"builtins.split "(fo*)" "foobar""#),
Value::List(List::new(vec![
Value::String("".into()),
Value::List(List::new(vec![Value::String("foo".into())])),
Value::String("bar".into())
]))
);
}
#[test_log::test]
fn split_multiple_matches() {
assert_eq!(
eval(r#"builtins.split "(b)" "foobarbaz""#),
Value::List(List::new(vec![
Value::String("foo".into()),
Value::List(List::new(vec![Value::String("b".into())])),
Value::String("ar".into()),
Value::List(List::new(vec![Value::String("b".into())])),
Value::String("az".into())
]))
);
}
#[test_log::test]
fn split_with_multiple_groups() {
assert_eq!(
eval(r#"builtins.split "(f)(o*)" "foo""#),
Value::List(List::new(vec![
Value::String("".into()),
Value::List(List::new(vec![
Value::String("f".into()),
Value::String("oo".into())
])),
Value::String("".into())
]))
);
}
#[test_log::test]
fn split_with_optional_groups() {
assert_eq!(
eval(r#"builtins.split "(a)|(c)" "abc""#),
Value::List(List::new(vec![
Value::String("".into()),
Value::List(List::new(vec![Value::String("a".into()), Value::Null])),
Value::String("b".into()),
Value::List(List::new(vec![Value::Null, Value::String("c".into())])),
Value::String("".into())
]))
);
}
#[test_log::test]
fn split_greedy_matching() {
assert_eq!(
eval(r#"builtins.split "(o+)" "oooofoooo""#),
Value::List(List::new(vec![
Value::String("".into()),
Value::List(List::new(vec![Value::String("oooo".into())])),
Value::String("f".into()),
Value::List(List::new(vec![Value::String("oooo".into())])),
Value::String("".into())
]))
);
}
#[test_log::test]
fn split_posix_classes() {
assert_eq!(
eval(r#"builtins.split "([[:upper:]]+)" " FOO ""#),
Value::List(List::new(vec![
Value::String(" ".into()),
Value::List(List::new(vec![Value::String("FOO".into())])),
Value::String(" ".into())
]))
);
}
#[test_log::test]
fn replace_basic() {
assert_eq!(
eval(r#"builtins.replaceStrings ["o"] ["a"] "foobar""#),
Value::String("faabar".into())
);
}
#[test_log::test]
fn replace_with_empty() {
assert_eq!(
eval(r#"builtins.replaceStrings ["o"] [""] "foobar""#),
Value::String("fbar".into())
);
}
#[test_log::test]
fn replace_multiple_patterns() {
assert_eq!(
eval(r#"builtins.replaceStrings ["oo" "a"] ["a" "oo"] "foobar""#),
Value::String("faboor".into())
);
}
#[test_log::test]
fn replace_first_match_wins() {
assert_eq!(
eval(r#"builtins.replaceStrings ["oo" "oo"] ["u" "i"] "foobar""#),
Value::String("fubar".into())
);
}
#[test_log::test]
fn replace_empty_pattern() {
assert_eq!(
eval(r#"builtins.replaceStrings [""] ["X"] "abc""#),
Value::String("XaXbXcX".into())
);
}
#[test_log::test]
fn replace_empty_pattern_empty_string() {
assert_eq!(
eval(r#"builtins.replaceStrings [""] ["X"] """#),
Value::String("X".into())
);
}
#[test_log::test]
fn replace_simple_char() {
assert_eq!(
eval(r#"builtins.replaceStrings ["-"] ["_"] "a-b""#),
Value::String("a_b".into())
);
}
#[test_log::test]
fn replace_longer_pattern() {
assert_eq!(
eval(r#"builtins.replaceStrings ["oo"] ["u"] "foobar""#),
Value::String("fubar".into())
);
}
#[test_log::test]
fn replace_different_lengths() {
let result = eval_result(r#"builtins.replaceStrings ["a" "b"] ["x"] "test""#);
assert!(result.is_err());
}
#[test_log::test]
fn split_version_simple() {
assert_eq!(
eval(r#"builtins.splitVersion "1.2.3""#),
Value::List(List::new(vec![
Value::String("1".into()),
Value::String("2".into()),
Value::String("3".into())
]))
);
}
#[test_log::test]
fn split_version_with_pre() {
assert_eq!(
eval(r#"builtins.splitVersion "2.3.0pre1234""#),
Value::List(List::new(vec![
Value::String("2".into()),
Value::String("3".into()),
Value::String("0".into()),
Value::String("pre".into()),
Value::String("1234".into())
]))
);
}
#[test_log::test]
fn split_version_with_letters() {
assert_eq!(
eval(r#"builtins.splitVersion "2.3a""#),
Value::List(List::new(vec![
Value::String("2".into()),
Value::String("3".into()),
Value::String("a".into())
]))
);
}
#[test_log::test]
fn split_version_with_dashes() {
assert_eq!(
eval(r#"builtins.splitVersion "2.3-beta1""#),
Value::List(List::new(vec![
Value::String("2".into()),
Value::String("3".into()),
Value::String("beta".into()),
Value::String("1".into())
]))
);
}
#[test_log::test]
fn split_version_empty() {
assert_eq!(
eval(r#"builtins.splitVersion """#),
Value::List(List::new(vec![]))
);
}
+119
View File
@@ -0,0 +1,119 @@
use fix::value::Value;
use crate::utils::eval;
#[test_log::test]
fn non_recursive_bindings() {
assert_eq!(eval("let x = 1; y = 2; z = x + y; in z"), Value::Int(3));
}
#[test_log::test]
fn non_recursive_multiple_bindings() {
assert_eq!(
eval("let a = 10; b = 20; c = 30; d = a + b + c; in d"),
Value::Int(60)
);
}
#[test_log::test]
fn recursive_fibonacci() {
assert_eq!(
eval("let fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); in fib 5"),
Value::Int(8)
);
}
#[test_log::test]
fn recursive_factorial() {
assert_eq!(
eval("let factorial = n: if n == 0 then 1 else n * factorial (n - 1); in factorial 5"),
Value::Int(120)
);
}
#[test_log::test]
fn mutual_recursion_simple() {
assert_eq!(
eval(
"let f = n: if n == 0 then 0 else g (n - 1); g = n: if n == 0 then 1 else f (n - 1); in f 5"
),
Value::Int(1)
);
}
#[test_log::test]
fn mutual_recursion_even_odd() {
assert_eq!(
eval(
"let even = n: if n == 0 then true else odd (n - 1); odd = n: if n == 0 then false else even (n - 1); in even 4"
),
Value::Bool(true)
);
}
#[test_log::test]
fn mixed_recursive_and_non_recursive() {
assert_eq!(
eval("let x = 1; f = n: if n == 0 then x else f (n - 1); in f 5"),
Value::Int(1)
);
}
#[test_log::test]
fn mixed_with_multiple_non_recursive() {
assert_eq!(
eval(
"let a = 10; b = 20; sum = a + b; countdown = n: if n == 0 then sum else countdown (n - 1); in countdown 3"
),
Value::Int(30)
);
}
#[test_log::test]
fn rec_attrset_non_recursive() {
assert_eq!(eval("rec { x = 1; y = 2; z = x + y; }.z"), Value::Int(3));
}
#[test_log::test]
fn rec_attrset_recursive() {
assert_eq!(
eval("rec { f = n: if n == 0 then 0 else f (n - 1); }.f 10"),
Value::Int(0)
);
}
#[test_log::test]
fn nested_let_non_recursive() {
assert_eq!(
eval("let x = 1; in let y = x + 1; z = y + 1; in z"),
Value::Int(3)
);
}
#[test_log::test]
fn nested_let_with_recursive() {
assert_eq!(
eval("let f = n: if n == 0 then 0 else f (n - 1); in let g = m: f m; in g 5"),
Value::Int(0)
);
}
#[test_log::test]
fn three_way_mutual_recursion() {
assert_eq!(
eval(
"let a = n: if n == 0 then 1 else b (n - 1); b = n: if n == 0 then 2 else c (n - 1); c = n: if n == 0 then 3 else a (n - 1); in a 6"
),
Value::Int(1)
);
}
#[test_log::test]
fn complex_mixed_dependencies() {
assert_eq!(
eval(
"let x = 5; y = 10; sum = x + y; fib = n: if n <= 1 then 1 else fib (n - 1) + fib (n - 2); result = sum + fib 5; in result"
),
Value::Int(23)
);
}
+256
View File
@@ -0,0 +1,256 @@
use fix::value::Value;
use crate::utils::{eval, eval_result};
#[test_log::test]
fn string_returns_as_is() {
assert_eq!(
eval(r#"toString "hello""#),
Value::String("hello".to_string())
);
}
#[test_log::test]
fn integer_to_string() {
assert_eq!(eval("toString 42"), Value::String("42".to_string()));
assert_eq!(eval("toString (-5)"), Value::String("-5".to_string()));
assert_eq!(eval("toString 0"), Value::String("0".to_string()));
}
#[test_log::test]
fn float_to_string() {
assert_eq!(eval("toString 3.14"), Value::String("3.14".to_string()));
assert_eq!(eval("toString 0.0"), Value::String("0".to_string()));
assert_eq!(eval("toString (-2.5)"), Value::String("-2.5".to_string()));
}
#[test_log::test]
fn bool_to_string() {
assert_eq!(eval("toString true"), Value::String("1".to_string()));
assert_eq!(eval("toString false"), Value::String("".to_string()));
}
#[test_log::test]
fn null_to_string() {
assert_eq!(eval("toString null"), Value::String("".to_string()));
}
#[test_log::test]
fn simple_list_to_string() {
assert_eq!(eval("toString [1 2 3]"), Value::String("1 2 3".to_string()));
assert_eq!(
eval(r#"toString ["a" "b" "c"]"#),
Value::String("a b c".to_string())
);
}
#[test_log::test]
fn nested_list_flattens() {
assert_eq!(
eval("toString [[1 2] [3 4]]"),
Value::String("1 2 3 4".to_string())
);
assert_eq!(
eval("toString [1 [2 3] 4]"),
Value::String("1 2 3 4".to_string())
);
}
#[test_log::test]
fn empty_list_in_list_no_extra_space() {
assert_eq!(eval("toString [1 [] 2]"), Value::String("1 2".to_string()));
assert_eq!(eval("toString [[] 1 2]"), Value::String("1 2".to_string()));
assert_eq!(eval("toString [1 2 []]"), Value::String("1 2 ".to_string()));
}
#[test_log::test]
fn list_with_multiple_empty_lists() {
assert_eq!(
eval("toString [1 [] [] 2]"),
Value::String("1 2".to_string())
);
assert_eq!(eval("toString [[] [] 1]"), Value::String("1".to_string()));
}
#[test_log::test]
fn list_with_bool_and_null() {
assert_eq!(
eval("toString [true false null]"),
Value::String("1 ".to_string())
);
assert_eq!(
eval("toString [1 true 2 false 3]"),
Value::String("1 1 2 3".to_string())
);
}
#[test_log::test]
fn mixed_type_list() {
assert_eq!(
eval(r#"toString [1 "hello" 2.5 true]"#),
Value::String("1 hello 2.5 1".to_string())
);
}
#[test_log::test]
fn attrs_with_out_path() {
assert_eq!(
eval(r#"toString { outPath = "/nix/store/foo"; }"#),
Value::String("/nix/store/foo".to_string())
);
}
#[test_log::test]
fn attrs_with_to_string_method() {
assert_eq!(
eval(r#"toString { __toString = self: "custom"; }"#),
Value::String("custom".to_string())
);
}
#[test_log::test]
fn attrs_to_string_self_reference() {
assert_eq!(
eval(
r#"let obj = { x = 42; __toString = self: "x is ${toString self.x}"; }; in toString obj"#
),
Value::String("x is 42".to_string())
);
}
#[test_log::test]
fn attrs_to_string_priority() {
assert_eq!(
eval(r#"toString { __toString = self: "custom"; outPath = "/nix/store/foo"; }"#),
Value::String("custom".to_string())
);
}
#[test_log::test]
fn derivation_like_object() {
assert_eq!(
eval(
r#"let drv = { type = "derivation"; outPath = "/nix/store/hash-pkg"; }; in toString drv"#
),
Value::String("/nix/store/hash-pkg".to_string())
);
}
#[test_log::test]
fn string_interpolation_with_int() {
assert_eq!(
eval(r#""value: ${toString 42}""#),
Value::String("value: 42".to_string())
);
}
#[test_log::test]
fn string_interpolation_with_list() {
assert_eq!(
eval(r#""items: ${toString [1 2 3]}""#),
Value::String("items: 1 2 3".to_string())
);
}
#[test_log::test]
fn nested_to_string_calls() {
assert_eq!(
eval(r#"toString (toString 42)"#),
Value::String("42".to_string())
);
}
#[test_log::test]
fn to_string_in_let_binding() {
assert_eq!(
eval(r#"let x = toString 42; y = toString 10; in "${x}-${y}""#),
Value::String("42-10".to_string())
);
}
#[test_log::test]
fn empty_string() {
assert_eq!(eval(r#"toString """#), Value::String("".to_string()));
}
#[test_log::test]
fn empty_list() {
assert_eq!(eval("toString []"), Value::String("".to_string()));
}
#[test_log::test]
fn to_string_preserves_spaces_in_strings() {
assert_eq!(
eval(r#"toString "hello world""#),
Value::String("hello world".to_string())
);
}
#[test_log::test]
fn list_of_empty_strings() {
assert_eq!(
eval(r#"toString ["" "" ""]"#),
Value::String(" ".to_string())
);
}
#[test_log::test]
fn deeply_nested_lists() {
assert_eq!(
eval("toString [[[1] [2]] [[3] [4]]]"),
Value::String("1 2 3 4".to_string())
);
}
#[test_log::test]
fn list_with_nested_empty_lists() {
assert_eq!(
eval("toString [1 [[]] 2]"),
Value::String("1 2".to_string())
);
}
#[test_log::test]
fn attrs_without_out_path_or_to_string_fails() {
let result = eval_result(r#"toString { foo = "bar"; }"#);
assert!(result.is_err());
}
#[test_log::test]
fn function_to_string_fails() {
let result = eval_result("toString (x: x)");
assert!(result.is_err());
}
#[test_log::test]
fn to_string_method_must_return_string() {
assert_eq!(
eval(r#"toString { __toString = self: 42; }"#),
Value::String("42".into())
);
assert_eq!(
eval(r#"toString { __toString = self: true; }"#),
Value::String("1".into())
);
}
#[test_log::test]
fn out_path_can_be_nested() {
assert_eq!(
eval(r#"toString { outPath = { outPath = "/final/path"; }; }"#),
Value::String("/final/path".to_string())
);
}
#[test_log::test]
fn list_spacing_matches_nix_behavior() {
assert_eq!(
eval(r#"toString ["a" "b"]"#),
Value::String("a b".to_string())
);
assert_eq!(
eval(r#"toString ["a" ["b" "c"] "d"]"#),
Value::String("a b c d".to_string())
);
}
-7
View File
@@ -11,13 +11,6 @@ pub fn eval(expr: &str) -> Value {
.unwrap() .unwrap()
} }
pub fn eval_shallow(expr: &str) -> Value {
Runtime::new()
.unwrap()
.eval_shallow(Source::new_eval(expr.into()).unwrap())
.unwrap()
}
pub fn eval_deep(expr: &str) -> Value { pub fn eval_deep(expr: &str) -> Value {
Runtime::new() Runtime::new()
.unwrap() .unwrap()
Generated
+9 -9
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src" "rust-analyzer-src": "rust-analyzer-src"
}, },
"locked": { "locked": {
"lastModified": 1774076307, "lastModified": 1773471952,
"narHash": "sha256-v8axK9HGgVERw9oG3SKdsuE+ri0GPUZDyRBN4GLqQ1c=", "narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=",
"owner": "nix-community", "owner": "nix-community",
"repo": "fenix", "repo": "fenix",
"rev": "556198cc6c69c0a13228a15e33b2360f333b0092", "rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -37,11 +37,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1773821835, "lastModified": 1773282481,
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=", "narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0", "rev": "fe416aaedd397cacb33a610b33d60ff2b431b127",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -61,11 +61,11 @@
"rust-analyzer-src": { "rust-analyzer-src": {
"flake": false, "flake": false,
"locked": { "locked": {
"lastModified": 1774036669, "lastModified": 1773326183,
"narHash": "sha256-EWhsBSh/h1VcyLKXuTEyH8lNVB2ktuKVkqx8dkQ6hxk=", "narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=",
"owner": "rust-lang", "owner": "rust-lang",
"repo": "rust-analyzer", "repo": "rust-analyzer",
"rev": "0cf3e8a07f0e29825f5db78840e646a4bb519742", "rev": "6254616e97f358e67b70dfc0463687f5f7911c1a",
"type": "github" "type": "github"
}, },
"original": { "original": {