Compare commits

...

3 Commits

Author SHA1 Message Date
imxyy1soope1 aec24493e5 ForceMode (WIP) 2026-03-22 17:01:44 +08:00
imxyy1soope1 b3f1f4f6ff remove redundant tests 2026-03-22 17:01:20 +08:00
imxyy1soope1 1950d4de6c init builtins 2026-03-22 16:50:08 +08:00
36 changed files with 2594 additions and 2568 deletions
Generated
+1 -8
View File
@@ -188,13 +188,6 @@ dependencies = [
"generic-array",
]
[[package]]
name = "boxing"
version = "0.1.3"
dependencies = [
"sptr",
]
[[package]]
name = "bstr"
version = "1.12.1"
@@ -798,7 +791,6 @@ version = "0.1.0"
dependencies = [
"anyhow",
"base64",
"boxing",
"bumpalo",
"bzip2",
"clap",
@@ -833,6 +825,7 @@ dependencies = [
"sha2",
"small-map",
"smallvec",
"sptr",
"string-interner",
"tap",
"tar",
-1
View File
@@ -2,7 +2,6 @@
resolver = "3"
members = [
"fix",
"boxing",
]
[profile.profiling]
-8
View File
@@ -1,8 +0,0 @@
[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
@@ -1,2 +0,0 @@
pub mod nan;
mod utils;
-7
View File
@@ -1,7 +0,0 @@
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;
-16
View File
@@ -1,16 +0,0 @@
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"
colored = "3.1"
boxing = { path = "../boxing" }
sptr = "0.3"
sealed = "0.6"
small-map = "0.1"
smallvec = "1.15"
+53 -46
View File
@@ -1,11 +1,28 @@
use super::{NEG_QUIET_NAN, QUIET_NAN, SIGN_MASK};
use crate::utils::ArrayExt;
use sptr::Strict;
use std::fmt;
use std::mem::ManuallyDrop;
use std::num::NonZeroU8;
pub trait RawStore: Sized {
use sptr::Strict;
const SIGN_MASK: u64 = 0x7FFF_FFFF_FFFF_FFFF;
const QUIET_NAN: u64 = 0x7FF8_0000_0000_0000;
const NEG_QUIET_NAN: u64 = 0xFFF8_0000_0000_0000;
pub(crate) trait ArrayExt<const LEN: usize> {
type Elem;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M];
}
impl<T: Default + Copy, const N: usize> ArrayExt<N> for [T; N] {
type Elem = T;
fn truncate_to<const M: usize>(self) -> [Self::Elem; M] {
let copy_len = usize::min(N, M);
let mut out = [T::default(); M];
out[0..copy_len].copy_from_slice(&self[0..copy_len]);
out
}
}
pub(crate) trait RawStore: Sized {
fn to_val(self, value: &mut Value);
fn from_val(value: &Value) -> Self;
}
@@ -138,18 +155,18 @@ enum TagVal {
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub struct RawTag(TagVal);
pub(crate) struct RawTag(TagVal);
impl RawTag {
#[inline]
#[must_use]
pub fn new(neg: bool, val: NonZeroU8) -> RawTag {
pub(crate) fn new(neg: bool, val: NonZeroU8) -> RawTag {
unsafe { Self::new_unchecked(neg, val.get() & 0x07) }
}
#[inline]
#[must_use]
pub fn new_checked(neg: bool, val: u8) -> Option<RawTag> {
pub(crate) fn new_checked(neg: bool, val: u8) -> Option<RawTag> {
Some(RawTag(match (neg, val) {
(false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2,
@@ -176,7 +193,7 @@ impl RawTag {
/// `val` must be in the range `1..8`
#[inline]
#[must_use]
pub unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
pub(crate) unsafe fn new_unchecked(neg: bool, val: u8) -> RawTag {
RawTag(match (neg, val) {
(false, 1) => TagVal::_P1,
(false, 2) => TagVal::_P2,
@@ -200,7 +217,7 @@ impl RawTag {
#[inline]
#[must_use]
pub fn is_neg(self) -> bool {
pub(crate) fn is_neg(self) -> bool {
matches!(self.0, |TagVal::_N1| TagVal::_N2
| TagVal::_N3
| TagVal::_N4
@@ -211,7 +228,7 @@ impl RawTag {
#[inline]
#[must_use]
pub fn val(self) -> NonZeroU8 {
pub(crate) fn val(self) -> NonZeroU8 {
match self.0 {
TagVal::_P1 | TagVal::_N1 => NonZeroU8::MIN,
TagVal::_P2 | TagVal::_N2 => NonZeroU8::MIN.saturating_add(1),
@@ -225,7 +242,7 @@ impl RawTag {
#[inline]
#[must_use]
pub fn neg_val(self) -> (bool, u8) {
pub(crate) fn neg_val(self) -> (bool, u8) {
match self.0 {
TagVal::_P1 => (false, 1),
TagVal::_P2 => (false, 2),
@@ -286,9 +303,9 @@ impl Header {
}
}
#[derive(Clone, Debug, PartialEq)]
#[derive(Copy, Clone, Debug, PartialEq)]
#[repr(C, align(8))]
pub struct Value {
pub(crate) struct Value {
#[cfg(target_endian = "big")]
header: Header,
data: [u8; 6],
@@ -298,7 +315,7 @@ pub struct Value {
impl Value {
#[inline]
pub fn new(tag: RawTag, data: [u8; 6]) -> Value {
pub(crate) fn new(tag: RawTag, data: [u8; 6]) -> Value {
Value {
header: Header::new(tag),
data,
@@ -306,23 +323,23 @@ impl Value {
}
#[inline]
pub fn empty(tag: RawTag) -> Value {
pub(crate) fn empty(tag: RawTag) -> Value {
Value::new(tag, [0; 6])
}
pub fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
pub(crate) fn store<T: RawStore>(tag: RawTag, val: T) -> Value {
let mut v = Value::new(tag, [0; 6]);
T::to_val(val, &mut v);
v
}
pub fn load<T: RawStore>(self) -> T {
pub(crate) fn load<T: RawStore>(self) -> T {
T::from_val(&self)
}
#[inline]
#[must_use]
pub fn tag(&self) -> RawTag {
pub(crate) fn tag(&self) -> RawTag {
self.header.tag()
}
@@ -332,41 +349,42 @@ impl Value {
}
#[inline]
pub fn set_data(&mut self, val: [u8; 6]) {
pub(crate) fn set_data(&mut self, val: [u8; 6]) {
self.data = val;
}
#[inline]
#[must_use]
pub fn data(&self) -> &[u8; 6] {
pub(crate) fn data(&self) -> &[u8; 6] {
&self.data
}
#[inline]
#[must_use]
pub fn data_mut(&mut self) -> &mut [u8; 6] {
pub(crate) fn data_mut(&mut self) -> &mut [u8; 6] {
&mut self.data
}
#[inline]
#[must_use]
pub unsafe fn whole(&self) -> &[u8; 8] {
unsafe fn whole(&self) -> &[u8; 8] {
let ptr = (self as *const Value).cast::<[u8; 8]>();
unsafe { &*ptr }
}
#[inline]
#[must_use]
pub unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
unsafe fn whole_mut(&mut self) -> &mut [u8; 8] {
let ptr = (self as *mut Value).cast::<[u8; 8]>();
unsafe { &mut *ptr }
}
}
#[repr(C)]
pub union RawBox {
#[derive(Copy, Clone)]
pub(crate) union RawBox {
float: f64,
value: ManuallyDrop<Value>,
value: Value,
bits: u64,
#[cfg(target_pointer_width = "64")]
ptr: *const (),
@@ -377,7 +395,7 @@ pub union RawBox {
impl RawBox {
#[inline]
#[must_use]
pub fn from_float(val: f64) -> RawBox {
pub(crate) fn from_float(val: f64) -> RawBox {
match (val.is_nan(), val.is_sign_positive()) {
(true, true) => RawBox {
float: f64::from_bits(QUIET_NAN),
@@ -391,15 +409,13 @@ impl RawBox {
#[inline]
#[must_use]
pub fn from_value(value: Value) -> RawBox {
RawBox {
value: ManuallyDrop::new(value),
}
pub(crate) fn from_value(value: Value) -> RawBox {
RawBox { value }
}
#[inline]
#[must_use]
pub fn tag(&self) -> Option<RawTag> {
pub(crate) fn tag(&self) -> Option<RawTag> {
if self.is_value() {
Some(unsafe { self.value.tag() })
} else {
@@ -409,19 +425,19 @@ impl RawBox {
#[inline]
#[must_use]
pub fn is_float(&self) -> bool {
pub(crate) fn is_float(&self) -> bool {
(unsafe { !self.float.is_nan() } || unsafe { self.bits & SIGN_MASK == QUIET_NAN })
}
#[inline]
#[must_use]
pub fn is_value(&self) -> bool {
pub(crate) fn is_value(&self) -> bool {
(unsafe { self.float.is_nan() } && unsafe { self.bits & SIGN_MASK != QUIET_NAN })
}
#[inline]
#[must_use]
pub fn float(&self) -> Option<&f64> {
pub(crate) fn float(&self) -> Option<&f64> {
if self.is_float() {
Some(unsafe { &self.float })
} else {
@@ -431,7 +447,7 @@ impl RawBox {
#[inline]
#[must_use]
pub fn value(&self) -> Option<&Value> {
pub(crate) fn value(&self) -> Option<&Value> {
if self.is_value() {
Some(unsafe { &self.value })
} else {
@@ -440,20 +456,11 @@ impl RawBox {
}
#[inline]
pub fn into_float_unchecked(self) -> f64 {
pub(crate) fn into_float_unchecked(self) -> f64 {
unsafe { self.float }
}
}
impl Clone for RawBox {
#[inline]
fn clone(&self) -> Self {
RawBox {
ptr: unsafe { self.ptr },
}
}
}
impl fmt::Debug for RawBox {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.float() {
+2 -5
View File
@@ -125,8 +125,6 @@ pub enum Error {
#[label("error occurred here")]
span: Option<SourceSpan>,
message: String,
#[help]
js_backtrace: Option<String>,
#[related]
stack_trace: Vec<StackFrame>,
},
@@ -163,12 +161,11 @@ impl Error {
.into()
}
pub fn eval_error(msg: String, backtrace: Option<String>) -> Box<Self> {
pub fn eval_error(msg: impl Into<String>) -> Box<Self> {
Error::EvalError {
src: None,
span: None,
message: msg,
js_backtrace: backtrace,
message: msg.into(),
stack_trace: Vec::new(),
}
.into()
+1
View File
@@ -1,6 +1,7 @@
#![warn(clippy::unwrap_used)]
#![allow(dead_code)]
mod boxing;
pub mod error;
pub mod logging;
pub mod runtime;
+3 -3
View File
@@ -10,7 +10,7 @@ use rustyline::DefaultEditor;
use rustyline::error::ReadlineError;
#[derive(Parser)]
#[command(name = "nix-js", about = "Nix expression evaluator")]
#[command(name = "fix", about = "Nix expression evaluator")]
struct Cli {
#[command(subcommand)]
command: Command,
@@ -40,8 +40,8 @@ struct ExprSource {
file: Option<PathBuf>,
}
fn run_compile(runtime: &mut Runtime, src: ExprSource, silent: bool) -> Result<()> {
let src = if let Some(expr) = src.expr {
fn run_compile(_runtime: &mut Runtime, src: ExprSource, _silent: bool) -> Result<()> {
let _src = if let Some(expr) = src.expr {
Source::new_eval(expr)?
} else if let Some(file) = src.file {
Source::new_file(file)?
+33 -20
View File
@@ -12,10 +12,12 @@ use crate::downgrade::{Downgrade as _, DowngradeContext};
use crate::error::{Error, Result, Source};
use crate::ir::{ArgId, Ir, IrKey, IrRef, RawIrRef, StringId, ThunkId, ir_content_eq};
use crate::runtime::builtins::new_builtins_env;
use crate::runtime::vm::ForceMode;
use crate::store::{DaemonStore, StoreConfig};
use crate::value::Symbol;
mod builtins;
mod primops;
mod stack;
mod value;
mod vm;
@@ -43,6 +45,7 @@ impl Runtime {
let store = DaemonStore::connect(&config.daemon_socket)?;
Ok(Self {
arena: Arena::new(|mc| VM::new(mc, &mut strings)),
global_env,
store,
strings,
@@ -50,25 +53,19 @@ impl Runtime {
bytecode: Vec::new(),
sources: Vec::new(),
spans: Vec::new(),
arena: Arena::new(|mc| VM::new(mc)),
})
}
pub fn eval(&mut self, source: Source) -> Result<crate::value::Value> {
let root = self.downgrade(source, None)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip)
self.do_eval(source, None, ForceMode::Normal)
}
pub fn eval_shallow(&mut self, _source: Source) -> Result<crate::value::Value> {
todo!()
pub fn eval_shallow(&mut self, source: Source) -> Result<crate::value::Value> {
self.do_eval(source, None, ForceMode::Shallow)
}
pub fn eval_deep(&mut self, source: Source) -> Result<crate::value::Value> {
// FIXME: deep
let root = self.downgrade(source, None)?;
let ip = crate::codegen::compile_bytecode(root.as_ref(), self);
self.run(ip)
self.do_eval(source, None, ForceMode::Deep)
}
pub fn eval_repl(
@@ -76,10 +73,18 @@ impl Runtime {
source: Source,
scope: &HashSet<StringId>,
) -> Result<crate::value::Value> {
// FIXME: shallow
let root = self.downgrade(source, Some(Scope::Repl(scope)))?;
self.do_eval(source, Some(Scope::Repl(scope)), ForceMode::Shallow)
}
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);
self.run(ip)
self.run(ip, force_mode)
}
pub fn add_binding(
@@ -108,8 +113,11 @@ impl Runtime {
bump,
token,
strings,
source: sources.last().unwrap().clone(),
scopes: [Scope::Global(global_env)].into_iter().chain(extra_scope.into_iter()).collect(),
source: sources.last().expect("no current source").clone(),
scopes: [Scope::Global(global_env)]
.into_iter()
.chain(extra_scope)
.collect(),
with_scope_count: 0,
arg_count: 0,
thunk_count,
@@ -117,7 +125,11 @@ impl Runtime {
}
}
fn downgrade<'a>(&'a mut self, source: Source, extra_scope: Option<Scope<'a>>) -> Result<OwnedIr> {
fn downgrade<'a>(
&'a mut self,
source: Source,
extra_scope: Option<Scope<'a>>,
) -> Result<OwnedIr> {
tracing::debug!("Parsing Nix expression");
self.sources.push(source.clone());
@@ -140,8 +152,10 @@ impl Runtime {
})
}
fn run(&mut self, ip: InstructionPtr) -> Result<crate::value::Value> {
fn run(&mut self, ip: InstructionPtr, force_mode: ForceMode) -> Result<crate::value::Value> {
let mut pc = ip.0;
self.arena
.mutate_root(|_, vm| vm.set_force_mode(force_mode));
loop {
let Runtime {
bytecode,
@@ -149,8 +163,7 @@ impl Runtime {
arena,
..
} = self;
let action =
arena.mutate_root(|mc, root| root.run_batch(bytecode, &mut pc, mc, strings));
let action = arena.mutate_root(|mc, vm| vm.run_batch(bytecode, &mut pc, mc, strings));
match action {
Action::NeedGc => {
if self.arena.collection_phase() == CollectionPhase::Sweeping {
@@ -496,7 +509,7 @@ impl OwnedIr {
unsafe fn new(ir: RawIrRef<'_>, bump: Bump) -> Self {
Self {
_bump: bump,
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) }
ir: unsafe { std::mem::transmute::<RawIrRef<'_>, RawIrRef<'static>>(ir) },
}
}
+196
View File
@@ -1,11 +1,204 @@
use gc_arena::Collect;
use hashbrown::HashMap;
use num_enum::TryFromPrimitive;
use string_interner::DefaultStringInterner;
use super::value::*;
use crate::ir::{Ir, RawIrRef, StringId};
/// Generates both the BUILTINS const table and the BuiltinId enum
/// from a single source of truth, preventing index desync.
macro_rules! define_builtins {
($(($name:literal, $variant:ident, $arity:expr)),* $(,)?) => {
/// Builtin function registry.
/// Array index IS the PrimOp id. (name, arity) pairs.
pub(super) const BUILTINS: &[(&str, u8)] = &[
$(($name, $arity),)*
];
#[derive(Debug, Clone, Copy, PartialEq, Eq, TryFromPrimitive, Collect)]
#[repr(u8)]
#[collect(require_static)]
pub(super) enum BuiltinId {
$($variant,)*
}
};
}
define_builtins! {
("abort", Abort, 1),
("add", Add, 2),
("addErrorContext", AddErrorContext, 2),
("all", All, 2),
("any", Any, 2),
("appendContext", AppendContext, 2),
("attrNames", AttrNames, 1),
("attrValues", AttrValues, 1),
("baseNameOf", BaseNameOf, 1),
("bitAnd", BitAnd, 2),
("bitOr", BitOr, 2),
("bitXor", BitXor, 2),
("catAttrs", CatAttrs, 2),
("ceil", Ceil, 1),
("compareVersions", CompareVersions, 2),
("concatLists", ConcatLists, 1),
("concatMap", ConcatMap, 2),
("concatStringsSep", ConcatStringsSep, 2),
("convertHash", ConvertHash, 1),
("deepSeq", DeepSeq, 2),
("derivation", Derivation, 1),
("derivationStrict", DerivationStrict, 1),
("dirOf", DirOf, 1),
("div", Div, 2),
("elem", Elem, 2),
("elemAt", ElemAt, 2),
("fetchGit", FetchGit, 1),
("fetchMercurial", FetchMercurial, 1),
("fetchTarball", FetchTarball, 1),
("fetchTree", FetchTree, 1),
("fetchurl", FetchUrl, 1),
("filter", Filter, 2),
("filterSource", FilterSource, 2),
("findFile", FindFile, 2),
("floor", Floor, 1),
("foldl'", FoldlStrict, 3),
("fromJSON", FromJSON, 1),
("fromTOML", FromTOML, 1),
("functionArgs", FunctionArgs, 1),
("genList", GenList, 2),
("genericClosure", GenericClosure, 1),
("getAttr", GetAttr, 2),
("getContext", GetContext, 1),
("getEnv", GetEnv, 1),
("groupBy", GroupBy, 2),
("hasAttr", HasAttr, 2),
("hasContext", HasContext, 1),
("hashFile", HashFile, 2),
("hashString", HashString, 2),
("head", Head, 1),
("import", Import, 1),
("intersectAttrs", IntersectAttrs, 2),
("isAttrs", IsAttrs, 1),
("isBool", IsBool, 1),
("isFloat", IsFloat, 1),
("isFunction", IsFunction, 1),
("isInt", IsInt, 1),
("isList", IsList, 1),
("isNull", IsNull, 1),
("isPath", IsPath, 1),
("isString", IsString, 1),
("length", Length, 1),
("lessThan", LessThan, 2),
("listToAttrs", ListToAttrs, 1),
("map", Map, 2),
("mapAttrs", MapAttrs, 2),
("match", Match, 2),
("mul", Mul, 2),
("null", Null, 0), // constant, not a function
("parseDrvName", ParseDrvName, 1),
("partition", Partition, 2),
("path", Path, 1),
("pathExists", PathExists, 1),
("placeholder", Placeholder, 1),
("readDir", ReadDir, 1),
("readFile", ReadFile, 1),
("readFileType", ReadFileType, 1),
("removeAttrs", RemoveAttrs, 2),
("replaceStrings", ReplaceStrings, 3),
("scopedImport", ScopedImport, 2),
("seq", Seq, 2),
("sort", Sort, 2),
("split", Split, 2),
("splitVersion", SplitVersion, 1),
("storePath", StorePath, 1),
("stringLength", StringLength, 1),
("sub", Sub, 2),
("substring", Substring, 3),
("tail", Tail, 1),
("throw", Throw, 1),
("toFile", ToFile, 2),
("toJSON", ToJSON, 1),
("toPath", ToPath, 1),
("toString", ToString, 1),
("toXML", ToXML, 1),
("trace", Trace, 2),
("tryEval", TryEval, 1),
("typeOf", TypeOf, 1),
("unsafeDiscardStringContext", UnsafeDiscardStringContext, 1),
("unsafeGetAttrPos", UnsafeGetAttrPos, 2),
("warn", Warn, 2),
("zipAttrsWith", ZipAttrsWith, 2),
("break", Break, 1),
}
/// Names that need to be pre-interned for builtin implementations.
const EXTRA_INTERN_NAMES: &[&str] = &[
"builtins",
"currentSystem",
"langVersion",
"nixVersion",
"storeDir",
"nixPath",
"true",
"false",
// typeOf return values
"int",
"float",
"bool",
"string",
"path",
"null",
"set",
"list",
"lambda",
// attrset keys used by builtins
"name",
"value",
"success",
"right",
"wrong",
"key",
"operator",
"startSet",
"__toString",
"outPath",
"__functor",
"drvPath",
"type",
"derivation",
"version",
];
/// Returns true if this builtin has lazy argument semantics
/// (not all args should be forced before dispatch).
pub(super) fn is_lazy_builtin(id: BuiltinId) -> bool {
matches!(
id,
BuiltinId::Seq
| BuiltinId::DeepSeq
| BuiltinId::Trace
| BuiltinId::Warn
| BuiltinId::TryEval
| BuiltinId::AddErrorContext
| BuiltinId::Break
)
}
/// Intern all builtin names and extra names needed at runtime.
fn intern_all_builtins(interner: &mut DefaultStringInterner) {
for &(name, _) in BUILTINS {
interner.get_or_intern(name);
}
for &name in EXTRA_INTERN_NAMES {
interner.get_or_intern(name);
}
}
pub(super) fn new_builtins_env(
interner: &mut DefaultStringInterner,
) -> HashMap<StringId, Ir<'static, RawIrRef<'static>>> {
intern_all_builtins(interner);
let mut builtins = HashMap::new();
let builtins_sym = StringId(interner.get_or_intern("builtins"));
builtins.insert(builtins_sym, Ir::Builtins);
@@ -49,3 +242,6 @@ pub(super) fn new_builtins_env(
builtins
}
pub(super) type PrimOpArgs<'gc> = [Value<'gc>; 3];
pub(super) type PrimOpStrictArgs<'gc> = [StrictValue<'gc>; 3];
+929
View File
@@ -0,0 +1,929 @@
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)
}
}
}
}
+8 -2
View File
@@ -3,14 +3,13 @@ use std::mem::MaybeUninit;
use gc_arena::Collect;
use smallvec::SmallVec;
// FIXME: Drop???
pub(super) struct Stack<const N: usize, T> {
inner: Box<[MaybeUninit<T>; N]>,
len: usize,
}
unsafe impl<'gc, const N: usize, T: Collect<'gc> + 'gc> Collect<'gc> for Stack<N, T> {
const NEEDS_TRACE: bool = true;
const NEEDS_TRACE: bool = T::NEEDS_TRACE;
fn trace<U: gc_arena::collect::Trace<'gc>>(&self, cc: &mut U) {
for item in self.inner[..self.len].iter() {
unsafe {
@@ -34,6 +33,13 @@ impl<const N: usize, T> Stack<N, T> {
}
}
pub(super) unsafe fn push_unchecked(&mut self, val: T) {
unsafe {
self.inner.get_unchecked_mut(self.len).write(val);
}
self.len += 1;
}
pub(super) fn push(&mut self, val: T) -> Result<(), T> {
if self.len == N {
return Err(val);
+39 -37
View File
@@ -3,16 +3,19 @@ use std::marker::PhantomData;
use std::mem::size_of;
use std::ops::Deref;
use boxing::nan::raw::{RawBox, RawStore, RawTag, Value as RawValue};
use gc_arena::{Collect, Gc, Mutation, RefLock, collect::Trace};
use num_enum::TryFromPrimitive;
use sealed::sealed;
use smallvec::SmallVec;
use string_interner::{Symbol, symbol::SymbolU32};
use crate::ir::StringId;
use crate::boxing::{RawBox, RawStore, RawTag, Value as RawValue};
use crate::{ir::StringId, runtime::builtins::BuiltinId};
#[sealed]
pub(crate) trait Storable {
/// # Safety
/// TAG must be unique among all implementors.
pub(crate) unsafe trait Storable {
const TAG: (bool, u8);
}
pub(crate) trait InlineStorable: Storable + RawStore {}
@@ -25,14 +28,14 @@ macro_rules! define_value_types {
) => {
$(
#[sealed]
impl Storable for $itype {
unsafe impl Storable for $itype {
const TAG: (bool, u8) = $itag;
}
impl InlineStorable for $itype {}
)*
$(
#[sealed]
impl Storable for $gtype {
unsafe impl Storable for $gtype {
const TAG: (bool, u8) = $gtag;
}
impl GcStorable for $gtype {}
@@ -116,22 +119,13 @@ define_value_types! {
/// # Nix runtime value representation
///
/// NaN-boxed value fitting in 8 bytes.
#[derive(Copy, Clone)]
#[repr(transparent)]
pub(crate) struct Value<'gc> {
raw: RawBox,
_marker: PhantomData<Gc<'gc, ()>>,
}
impl Clone for Value<'_> {
#[inline]
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
_marker: PhantomData,
}
}
}
impl Default for Value<'_> {
#[inline(always)]
fn default() -> Self {
@@ -291,14 +285,23 @@ impl fmt::Debug for NixString {
}
}
#[derive(Collect, Debug)]
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub(crate) struct AttrSet<'gc> {
pub(crate) entries: SmallVec<[(StringId, Value<'gc>); 4]>,
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> {
pub(crate) fn from_sorted(entries: SmallVec<[(StringId, Value<'gc>); 4]>) -> Self {
pub(crate) unsafe fn from_sorted_unchecked(
entries: SmallVec<[(StringId, Value<'gc>); 4]>,
) -> Self {
debug_assert!(entries.is_sorted_by_key(|(key, _)| *key));
Self { entries }
}
@@ -307,13 +310,11 @@ impl<'gc> AttrSet<'gc> {
self.entries
.binary_search_by_key(&key, |(k, _)| *k)
.ok()
.map(|i| self.entries[i].1.clone())
.map(|i| self.entries[i].1)
}
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> {
@@ -328,15 +329,15 @@ impl<'gc> AttrSet<'gc> {
while i < self.entries.len() && j < other.entries.len() {
match self.entries[i].0.cmp(&other.entries[j].0) {
Less => {
entries.push(self.entries[i].clone());
entries.push(self.entries[i]);
i += 1;
}
Greater => {
entries.push(other.entries[j].clone());
entries.push(other.entries[j]);
j += 1;
}
Equal => {
entries.push(other.entries[j].clone());
entries.push(other.entries[j]);
i += 1;
j += 1;
}
@@ -351,7 +352,7 @@ impl<'gc> AttrSet<'gc> {
}
}
#[derive(Collect, Debug)]
#[derive(Collect, Debug, Default)]
#[collect(no_drop)]
pub(crate) struct List<'gc> {
pub(crate) inner: SmallVec<[Value<'gc>; 4]>,
@@ -366,6 +367,10 @@ pub(crate) enum ThunkState<'gc> {
ip: u32,
env: Gc<'gc, RefLock<Env<'gc>>>,
},
Apply {
func: Value<'gc>,
arg: Value<'gc>,
},
Blackhole,
Evaluated(Value<'gc>),
}
@@ -420,17 +425,20 @@ pub(crate) struct PatternInfo {
#[derive(Clone, Copy, Debug, Collect)]
#[collect(require_static)]
pub(crate) struct PrimOp {
pub(crate) id: u8,
pub(crate) id: BuiltinId,
pub(crate) arity: u8,
}
impl RawStore for PrimOp {
fn to_val(self, value: &mut RawValue) {
value.set_data([0, 0, 0, 0, self.id, self.arity]);
value.set_data([0, 0, 0, 0, self.id as u8, self.arity]);
}
fn from_val(value: &RawValue) -> Self {
let [.., id, arity] = *value.data();
Self { id, arity }
Self {
id: BuiltinId::try_from_primitive(id).expect("invalid BuiltinId"),
arity,
}
}
}
@@ -441,6 +449,7 @@ pub(crate) struct PrimOpApp<'gc> {
pub(crate) args: SmallVec<[Value<'gc>; 2]>,
}
#[derive(Copy, Clone, Default)]
#[repr(transparent)]
pub(crate) struct StrictValue<'gc>(Value<'gc>);
@@ -455,7 +464,7 @@ impl<'gc> StrictValue<'gc> {
}
#[inline]
pub(crate) fn into_relaxed(self) -> Value<'gc> {
pub(crate) fn relax(self) -> Value<'gc> {
self.0
}
}
@@ -468,13 +477,6 @@ impl<'gc> Deref for StrictValue<'gc> {
}
}
impl Clone for StrictValue<'_> {
#[inline]
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl fmt::Debug for StrictValue<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Debug::fmt(&self.0, f)
+1027 -426
View File
File diff suppressed because it is too large Load Diff
-1
View File
@@ -7,7 +7,6 @@ mod validation;
pub use config::StoreConfig;
pub use daemon::DaemonStore;
pub use validation::validate_store_path;
pub trait Store: Send + Sync {
fn get_store_dir(&self) -> &str;
+2 -5
View File
@@ -69,13 +69,10 @@ impl Store for DaemonStore {
fn ensure_path(&self, path: &str) -> Result<()> {
self.block_on(async {
self.connection.ensure_path(path).await.map_err(|e| {
Error::eval_error(
format!(
Error::eval_error(format!(
"builtins.storePath: path '{}' is not valid in nix store: {}",
path, e
),
None,
)
))
})
})
}
+31 -34
View File
@@ -2,75 +2,72 @@ use crate::error::{Error, Result};
pub fn validate_store_path(store_dir: &str, path: &str) -> Result<()> {
if !path.starts_with(store_dir) {
return Err(Error::eval_error(
format!("path '{}' is not in the Nix store", path),
None,
));
return Err(Error::eval_error(format!(
"path '{}' is not in the Nix store",
path
)));
}
let relative = path
.strip_prefix(store_dir)
.and_then(|s| s.strip_prefix('/'))
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path), None))?;
.ok_or_else(|| Error::eval_error(format!("invalid store path format: {}", path)))?;
if relative.is_empty() {
return Err(Error::eval_error(
format!("store path cannot be store directory itself: {}", path),
None,
));
return Err(Error::eval_error(format!(
"store path cannot be store directory itself: {}",
path
)));
}
let parts: Vec<&str> = relative.splitn(2, '-').collect();
if parts.len() != 2 {
return Err(Error::eval_error(
format!("invalid store path format (missing name): {}", path),
None,
));
return Err(Error::eval_error(format!(
"invalid store path format (missing name): {}",
path
)));
}
let hash = parts[0];
let name = parts[1];
if hash.len() != 32 {
return Err(Error::eval_error(
format!(
return Err(Error::eval_error(format!(
"invalid store path hash length (expected 32, got {}): {}",
hash.len(),
hash
),
None,
));
)));
}
for ch in hash.chars() {
if !matches!(ch, '0'..='9' | 'a'..='d' | 'f'..='n' | 'p'..='s' | 'v'..='z') {
return Err(Error::eval_error(
format!("invalid character '{}' in store path hash: {}", ch, hash),
None,
));
return Err(Error::eval_error(format!(
"invalid character '{}' in store path hash: {}",
ch, hash
)));
}
}
if name.is_empty() {
return Err(Error::eval_error(
format!("store path has empty name: {}", path),
None,
));
return Err(Error::eval_error(format!(
"store path has empty name: {}",
path
)));
}
if name.starts_with('.') {
return Err(Error::eval_error(
format!("store path name cannot start with '.': {}", name),
None,
));
return Err(Error::eval_error(format!(
"store path name cannot start with '.': {}",
name
)));
}
for ch in name.chars() {
if !matches!(ch, '0'..='9' | 'a'..='z' | 'A'..='Z' | '+' | '-' | '.' | '_' | '?' | '=') {
return Err(Error::eval_error(
format!("invalid character '{}' in store path name: {}", ch, name),
None,
));
return Err(Error::eval_error(format!(
"invalid character '{}' in store path name: {}",
ch, name
)));
}
}
+9
View File
@@ -267,6 +267,15 @@ fn escape_quote_string(s: &str) -> String {
ret
}
/// Wrapper to format a float in Nix style (C printf `%g` with precision 6).
pub(crate) struct NixFloat(pub f64);
impl Display for NixFloat {
fn fmt(&self, f: &mut Formatter<'_>) -> FmtResult {
fmt_nix_float(f, self.0)
}
}
/// Format a float matching C's `printf("%g", x)` with default precision 6.
fn fmt_nix_float(f: &mut Formatter<'_>, x: f64) -> FmtResult {
if !x.is_finite() {
-69
View File
@@ -1,69 +0,0 @@
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
@@ -1,326 +0,0 @@
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
@@ -1,193 +0,0 @@
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
@@ -1,74 +0,0 @@
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
@@ -1,120 +0,0 @@
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,3 +366,193 @@ fn read_dir_on_file_fails() {
let err_msg = result.unwrap_err().to_string();
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,17 +1,6 @@
mod basic_eval;
mod builtins;
mod builtins_store;
mod derivation;
mod findfile;
mod free_globals;
mod functions;
mod io_operations;
mod lang;
mod numeric_types;
mod operators;
mod path_operations;
mod regex;
mod string_context;
mod thunk_scope;
mod to_string;
mod utils;
-138
View File
@@ -1,138 +0,0 @@
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
@@ -1,144 +0,0 @@
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
@@ -1,117 +0,0 @@
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
@@ -1,317 +0,0 @@
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
@@ -1,119 +0,0 @@
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
@@ -1,256 +0,0 @@
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,6 +11,13 @@ pub fn eval(expr: &str) -> Value {
.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 {
Runtime::new()
.unwrap()
Generated
+9 -9
View File
@@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1773471952,
"narHash": "sha256-kIRggXyT8RzijtfvyRIzj+zIDWM2fnCp8t0X4BkkTVc=",
"lastModified": 1774076307,
"narHash": "sha256-v8axK9HGgVERw9oG3SKdsuE+ri0GPUZDyRBN4GLqQ1c=",
"owner": "nix-community",
"repo": "fenix",
"rev": "a1b770adbc3f6c27485d03b90462ec414d4e1ce5",
"rev": "556198cc6c69c0a13228a15e33b2360f333b0092",
"type": "github"
},
"original": {
@@ -37,11 +37,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1773282481,
"narHash": "sha256-b/GV2ysM8mKHhinse2wz+uP37epUrSE+sAKXy/xvBY4=",
"lastModified": 1773821835,
"narHash": "sha256-TJ3lSQtW0E2JrznGVm8hOQGVpXjJyXY2guAxku2O9A4=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "fe416aaedd397cacb33a610b33d60ff2b431b127",
"rev": "b40629efe5d6ec48dd1efba650c797ddbd39ace0",
"type": "github"
},
"original": {
@@ -61,11 +61,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1773326183,
"narHash": "sha256-tj3piRd9RnnP36HwHmQD4O4XZeowsH/rvMeyp9Pmot0=",
"lastModified": 1774036669,
"narHash": "sha256-EWhsBSh/h1VcyLKXuTEyH8lNVB2ktuKVkqx8dkQ6hxk=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "6254616e97f358e67b70dfc0463687f5f7911c1a",
"rev": "0cf3e8a07f0e29825f5db78840e646a4bb519742",
"type": "github"
},
"original": {