use std::cell::{Cell, OnceCell, RefCell, UnsafeCell};
use std::fmt;
pub use std::rc::Rc;
use rustc_hash::FxBuildHasher;
use smallvec::SmallVec;
pub use smol_str::SmolStr;
use rowan::ast::AstNode;
use sui_intern::Symbol;
pub type FxHashMap<K, V> = im_rc::HashMap<K, V, FxBuildHasher>;
pub type AttrsMap<K, V> = std::collections::HashMap<K, V, FxBuildHasher>;
pub mod census {
use std::sync::atomic::{AtomicI64, Ordering::Relaxed};
use std::sync::OnceLock;
pub static ATTRS_LIVE: AtomicI64 = AtomicI64::new(0);
pub static ATTRS_MADE: AtomicI64 = AtomicI64::new(0);
pub static THUNK_LIVE: AtomicI64 = AtomicI64::new(0);
pub static THUNK_MADE: AtomicI64 = AtomicI64::new(0);
pub static THUNK_EVALUATED: AtomicI64 = AtomicI64::new(0);
pub static ENV_LIVE: AtomicI64 = AtomicI64::new(0);
pub static ENV_MADE: AtomicI64 = AtomicI64::new(0);
pub static NIXSTR_LIVE: AtomicI64 = AtomicI64::new(0);
pub static NIXSTR_MADE: AtomicI64 = AtomicI64::new(0);
pub static LIST_LIVE: AtomicI64 = AtomicI64::new(0);
pub static LIST_MADE: AtomicI64 = AtomicI64::new(0);
pub static SCOPE_THUNKS_NARROWED: AtomicI64 = AtomicI64::new(0);
pub static SCOPE_THUNKS_PINNED: AtomicI64 = AtomicI64::new(0);
#[inline(always)]
pub fn scope_narrowed() {
if enabled() {
SCOPE_THUNKS_NARROWED.fetch_add(1, Relaxed);
}
}
#[inline(always)]
pub fn scope_pinned() {
if enabled() {
SCOPE_THUNKS_PINNED.fetch_add(1, Relaxed);
}
}
#[inline]
pub fn enabled() -> bool {
static ON: OnceLock<bool> = OnceLock::new();
*ON.get_or_init(|| std::env::var("SUI_LIVE_CENSUS").as_deref() == Ok("1"))
}
#[inline(always)]
pub fn made(made: &AtomicI64, live: &AtomicI64) {
if enabled() {
made.fetch_add(1, Relaxed);
live.fetch_add(1, Relaxed);
}
}
#[inline(always)]
pub fn dropped(live: &AtomicI64) {
if enabled() {
live.fetch_sub(1, Relaxed);
}
}
#[inline(always)]
pub fn evaluated() {
if enabled() {
THUNK_EVALUATED.fetch_add(1, Relaxed);
}
}
pub fn rss_bytes() -> u64 {
#[cfg(target_os = "macos")]
unsafe {
let mut info: libc::mach_task_basic_info = std::mem::zeroed();
let mut count = (std::mem::size_of::<libc::mach_task_basic_info>()
/ std::mem::size_of::<libc::natural_t>()) as libc::mach_msg_type_number_t;
let kr = libc::task_info(
libc::mach_task_self(),
libc::MACH_TASK_BASIC_INFO,
std::ptr::addr_of_mut!(info).cast(),
&mut count,
);
if kr == libc::KERN_SUCCESS {
return info.resident_size;
}
0
}
#[cfg(not(target_os = "macos"))]
{
std::fs::read_to_string("/proc/self/statm")
.ok()
.and_then(|s| s.split_whitespace().nth(1).map(String::from))
.and_then(|pages| pages.parse::<u64>().ok())
.map(|pages| pages * 4096)
.unwrap_or(0)
}
}
pub fn dump(tag: &str) {
if !enabled() {
return;
}
let rss = rss_bytes();
eprintln!(
"[census {tag}] rss={rss_mb:.1}MB \
attrs_live={al} attrs_made={am} \
thunk_live={tl} thunk_made={tm} thunk_eval={te} \
env_live={el} env_made={em} \
nixstr_live={sl} nixstr_made={sm} \
list_live={ll} list_made={lm} \
scope_narrowed={sn} scope_pinned={sp}",
rss_mb = rss as f64 / (1024.0 * 1024.0),
al = ATTRS_LIVE.load(Relaxed),
am = ATTRS_MADE.load(Relaxed),
tl = THUNK_LIVE.load(Relaxed),
tm = THUNK_MADE.load(Relaxed),
te = THUNK_EVALUATED.load(Relaxed),
el = ENV_LIVE.load(Relaxed),
em = ENV_MADE.load(Relaxed),
sl = NIXSTR_LIVE.load(Relaxed),
sm = NIXSTR_MADE.load(Relaxed),
ll = LIST_LIVE.load(Relaxed),
lm = LIST_MADE.load(Relaxed),
sn = SCOPE_THUNKS_NARROWED.load(Relaxed),
sp = SCOPE_THUNKS_PINNED.load(Relaxed),
);
let (src_files, src_bytes) = crate::pos::source_text_census();
eprintln!(
"[census {tag}] src_files={src_files} src_bytes={src_mb:.1}MB",
src_mb = src_bytes as f64 / (1024.0 * 1024.0),
);
}
pub fn spawn_poller() {
if !enabled() {
return;
}
std::thread::spawn(|| loop {
std::thread::sleep(std::time::Duration::from_millis(2000));
dump("periodic");
});
}
}
pub fn intern(s: &str) -> Symbol {
sui_intern::intern(s)
}
pub fn resolve(sym: Symbol) -> String {
sui_intern::resolve(sym)
}
pub fn resolve_rc(sym: Symbol) -> std::rc::Rc<str> {
sui_intern::resolve_rc(sym)
}
pub fn with_resolved<F, R>(sym: Symbol, f: F) -> R
where
F: FnOnce(&str) -> R,
{
sui_intern::with_resolved(sym, f)
}
thread_local! {
static SOURCE_GEN: Cell<u32> = const { Cell::new(1) };
static IDENT_CACHE: RefCell<rustc_hash::FxHashMap<u64, Symbol>> =
RefCell::new(rustc_hash::FxHashMap::default());
}
pub fn next_source_id() -> u32 {
SOURCE_GEN.with(|g| {
let id = g.get();
g.set(id.wrapping_add(1));
id
})
}
pub fn intern_cached(name: &str, source_id: u32, text_offset: u32) -> Symbol {
intern_cached_with(source_id, text_offset, || intern(name))
}
pub fn intern_cached_with<F>(source_id: u32, text_offset: u32, cold: F) -> Symbol
where
F: FnOnce() -> Symbol,
{
let key = (u64::from(source_id) << 32) | u64::from(text_offset);
IDENT_CACHE.with(|c| {
let mut cache = c.borrow_mut();
*cache.entry(key).or_insert_with(cold)
})
}
pub fn clear_ident_cache() {
IDENT_CACHE.with(|c| c.borrow_mut().clear());
}
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub enum ContextElement {
Plain(SmolStr),
Output { drv: SmolStr, output: SmolStr },
DrvDeep(SmolStr),
}
impl fmt::Display for ContextElement {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
ContextElement::Plain(p) => write!(f, "{p}"),
ContextElement::Output { drv, output } => write!(f, "{drv}!{output}"),
ContextElement::DrvDeep(d) => write!(f, "={d}"),
}
}
}
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct StringContext(SmallVec<[ContextElement; 2]>);
impl StringContext {
pub fn new() -> Self {
Self(SmallVec::new())
}
pub fn merge(&mut self, other: &StringContext) {
for elem in &other.0 {
if !self.0.contains(elem) {
self.0.push(elem.clone());
}
}
}
pub fn add_plain(&mut self, path: impl Into<SmolStr>) {
let elem = ContextElement::Plain(path.into());
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
pub fn add_output(&mut self, drv: impl Into<SmolStr>, output: impl Into<SmolStr>) {
let elem = ContextElement::Output { drv: drv.into(), output: output.into() };
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
pub fn add_drv_deep(&mut self, drv: impl Into<SmolStr>) {
let elem = ContextElement::DrvDeep(drv.into());
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
#[must_use]
pub fn len(&self) -> usize {
self.0.len()
}
pub fn iter(&self) -> impl Iterator<Item = &ContextElement> {
self.0.iter()
}
pub fn insert(&mut self, elem: ContextElement) {
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
pub fn elements(&self) -> &[ContextElement] {
&self.0
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct NixString {
pub chars: SmolStr,
pub context: StringContext,
}
impl Clone for NixString {
fn clone(&self) -> Self {
census::made(&census::NIXSTR_MADE, &census::NIXSTR_LIVE);
Self {
chars: self.chars.clone(),
context: self.context.clone(),
}
}
}
impl Drop for NixString {
fn drop(&mut self) {
census::dropped(&census::NIXSTR_LIVE);
}
}
impl NixString {
pub fn plain(s: impl Into<SmolStr>) -> Self {
census::made(&census::NIXSTR_MADE, &census::NIXSTR_LIVE);
Self {
chars: s.into(),
context: StringContext::default(),
}
}
pub fn with_context(s: impl Into<SmolStr>, ctx: StringContext) -> Self {
census::made(&census::NIXSTR_MADE, &census::NIXSTR_LIVE);
Self {
chars: s.into(),
context: ctx,
}
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.chars
}
#[must_use]
pub fn has_context(&self) -> bool {
!self.context.is_empty()
}
}
impl AsRef<str> for NixString {
fn as_ref(&self) -> &str {
&self.chars
}
}
#[repr(transparent)]
#[derive(Debug, PartialEq)]
pub struct NixList(pub Vec<Value>);
impl NixList {
#[inline]
pub fn new(v: Vec<Value>) -> Self {
census::made(&census::LIST_MADE, &census::LIST_LIVE);
NixList(v)
}
#[inline]
pub fn into_vec(mut self) -> Vec<Value> {
std::mem::take(&mut self.0)
}
}
impl From<Vec<Value>> for NixList {
#[inline]
fn from(v: Vec<Value>) -> Self {
NixList::new(v)
}
}
impl<T: AsRef<[Value]>> PartialEq<T> for NixList {
#[inline]
fn eq(&self, other: &T) -> bool {
self.0.as_slice() == other.as_ref()
}
}
impl Clone for NixList {
fn clone(&self) -> Self {
census::made(&census::LIST_MADE, &census::LIST_LIVE);
NixList(self.0.clone())
}
}
impl Drop for NixList {
fn drop(&mut self) {
census::dropped(&census::LIST_LIVE);
}
}
impl FromIterator<Value> for NixList {
#[inline]
fn from_iter<I: IntoIterator<Item = Value>>(iter: I) -> Self {
NixList::new(iter.into_iter().collect())
}
}
impl std::ops::Deref for NixList {
type Target = Vec<Value>;
#[inline]
fn deref(&self) -> &Vec<Value> {
&self.0
}
}
impl std::ops::DerefMut for NixList {
#[inline]
fn deref_mut(&mut self) -> &mut Vec<Value> {
&mut self.0
}
}
impl<'a> IntoIterator for &'a NixList {
type Item = &'a Value;
type IntoIter = std::slice::Iter<'a, Value>;
#[inline]
fn into_iter(self) -> Self::IntoIter {
self.0.iter()
}
}
impl IntoIterator for NixList {
type Item = Value;
type IntoIter = std::vec::IntoIter<Value>;
#[inline]
fn into_iter(mut self) -> Self::IntoIter {
std::mem::take(&mut self.0).into_iter()
}
}
impl std::ops::Deref for NixString {
type Target = str;
fn deref(&self) -> &str {
&self.chars
}
}
impl fmt::Display for NixString {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", self.chars)
}
}
#[derive(Debug, Clone)]
#[derive(Default)]
pub enum Value {
#[default]
Null,
Bool(bool),
Int(i64),
Float(f64),
String(Rc<NixString>),
Path(Box<SmolStr>),
List(Rc<NixList>),
Attrs(Rc<NixAttrs>),
Lambda(Rc<Closure>),
Builtin(Box<BuiltinFn>),
Thunk(Thunk),
}
#[derive(Debug, Clone)]
pub enum Concrete {
Null,
Bool(bool),
Int(i64),
Float(f64),
String(Rc<NixString>),
Path(Box<SmolStr>),
List(Rc<NixList>), Attrs(Rc<NixAttrs>), Lambda(Rc<Closure>),
Builtin(Box<BuiltinFn>),
}
impl Concrete {
#[inline]
pub fn into_value(self) -> Value {
match self {
Concrete::Null => Value::Null,
Concrete::Bool(b) => Value::Bool(b),
Concrete::Int(n) => Value::Int(n),
Concrete::Float(f) => Value::Float(f),
Concrete::String(s) => Value::String(s),
Concrete::Path(p) => Value::Path(p),
Concrete::List(l) => Value::List(l),
Concrete::Attrs(a) => Value::Attrs(a),
Concrete::Lambda(c) => Value::Lambda(c),
Concrete::Builtin(b) => Value::Builtin(b),
}
}
pub fn to_value(&self) -> Value {
self.clone().into_value()
}
pub fn as_bool(&self) -> Result<bool, EvalError> {
match self {
Concrete::Bool(b) => Ok(*b),
other => Err(EvalError::TypeMismatch { expected: "bool", got: other.type_name() }),
}
}
pub fn as_int(&self) -> Result<i64, EvalError> {
match self {
Concrete::Int(n) => Ok(*n),
other => Err(EvalError::TypeMismatch { expected: "int", got: other.type_name() }),
}
}
pub fn as_str(&self) -> Result<&str, EvalError> {
match self {
Concrete::String(s) => Ok(&s.chars),
other => Err(EvalError::TypeMismatch { expected: "string", got: other.type_name() }),
}
}
pub fn as_nix_string(&self) -> Result<&NixString, EvalError> {
match self {
Concrete::String(s) => Ok(s),
other => Err(EvalError::TypeMismatch { expected: "string", got: other.type_name() }),
}
}
pub fn as_list(&self) -> Result<&[Value], EvalError> {
match self {
Concrete::List(l) => Ok(l.as_slice()),
other => Err(EvalError::TypeMismatch { expected: "list", got: other.type_name() }),
}
}
pub fn as_attrs(&self) -> Result<&NixAttrs, EvalError> {
match self {
Concrete::Attrs(a) => Ok(a),
other => Err(EvalError::TypeMismatch { expected: "set", got: other.type_name() }),
}
}
pub fn as_float(&self) -> Result<f64, EvalError> {
match self {
Concrete::Float(f) => Ok(*f),
Concrete::Int(n) => Ok(*n as f64),
other => Err(EvalError::TypeMismatch { expected: "float", got: other.type_name() }),
}
}
pub fn type_name(&self) -> &'static str {
match self {
Concrete::Null => "null",
Concrete::Bool(_) => "bool",
Concrete::Int(_) => "int",
Concrete::Float(_) => "float",
Concrete::String(_) => "string",
Concrete::Path(_) => "path",
Concrete::List(_) => "list",
Concrete::Attrs(_) => "set",
Concrete::Lambda(_) | Concrete::Builtin(_) => "lambda",
}
}
pub fn as_string(&self) -> Result<&str, EvalError> {
self.as_str()
}
pub fn to_attrs(&self) -> Result<NixAttrs, EvalError> {
match self {
Concrete::Attrs(a) => Ok((**a).clone()),
other => Err(EvalError::TypeMismatch { expected: "set", got: other.type_name() }),
}
}
pub fn to_list(&self) -> Result<Vec<Value>, EvalError> {
match self {
Concrete::List(l) => Ok((**l).0.clone()),
other => Err(EvalError::TypeMismatch { expected: "list", got: other.type_name() }),
}
}
pub fn coerce_to_path(&self, context: &str) -> Result<String, EvalError> {
match self {
Concrete::Path(p) => Ok(p.to_string()),
Concrete::String(ns) => Ok(ns.chars.to_string()),
Concrete::Attrs(attrs) => {
if let Some(out_path) = attrs.get("outPath") {
let forced = crate::eval::force_value(out_path)?;
forced.coerce_to_path(context)
} else {
Err(EvalError::type_error(format!(
"{context}: expected path or string, got set without outPath"
)))
}
}
other => Err(EvalError::type_error(format!(
"{context}: expected path or string, got {}", other.type_name()
))),
}
}
pub fn to_str(&self) -> Result<String, EvalError> {
match self {
Concrete::String(s) => Ok(s.chars.to_string()),
other => Err(EvalError::TypeMismatch { expected: "string", got: other.type_name() }),
}
}
pub fn to_nix_string(&self) -> Result<NixString, EvalError> {
match self {
Concrete::String(s) => Ok((**s).clone()),
other => Err(EvalError::TypeMismatch { expected: "string", got: other.type_name() }),
}
}
pub fn is_function(&self) -> bool {
matches!(self, Concrete::Lambda(_) | Concrete::Builtin(_))
}
}
impl From<Concrete> for Value {
fn from(c: Concrete) -> Value {
c.into_value()
}
}
impl PartialEq for Concrete {
fn eq(&self, other: &Self) -> bool {
match (self, other) {
(Concrete::Null, Concrete::Null) => true,
(Concrete::Bool(a), Concrete::Bool(b)) => a == b,
(Concrete::Int(a), Concrete::Int(b)) => a == b,
(Concrete::Float(a), Concrete::Float(b)) => a == b,
(Concrete::Int(a), Concrete::Float(b)) | (Concrete::Float(b), Concrete::Int(a)) => (*a as f64) == *b,
(Concrete::String(a), Concrete::String(b)) => Rc::ptr_eq(a, b) || a.chars == b.chars,
(Concrete::Path(a), Concrete::Path(b)) => a == b,
(Concrete::List(a), Concrete::List(b)) => Rc::ptr_eq(a, b) || a == b,
(Concrete::Attrs(a), Concrete::Attrs(b)) => {
if Rc::ptr_eq(a, b) {
return true;
}
if let (Some(pa), Some(pb)) =
(derivation_out_path(a), derivation_out_path(b))
{
return pa == pb;
}
let (fa, fb) = (a.as_flat(), b.as_flat());
if crate::perf::enabled() {
crate::perf::inc(crate::perf::Counter::AttrsEqStructuralCalls);
crate::perf::add(
crate::perf::Counter::AttrsEqEntriesCloneElided,
(fa.len() + fb.len()) as u64,
);
}
fa == fb
}
(Concrete::Lambda(a), Concrete::Lambda(b)) => Rc::ptr_eq(a, b),
_ => false,
}
}
}
pub fn concat_lists(left: Value, right_elems: &[Value]) -> Result<Value, EvalError> {
let mut la = match left {
Value::List(rc) => {
let reused = Rc::strong_count(&rc) == 1;
let vec: Vec<Value> = match Rc::try_unwrap(rc) {
Ok(v) => v.into_vec(), Err(rc) => (*rc).0.clone(), };
if crate::perf::enabled() {
crate::perf::inc(crate::perf::Counter::ListConcatCalls);
if reused {
crate::perf::add(
crate::perf::Counter::ListConcatElemsReused,
vec.len() as u64,
);
} else {
crate::perf::add(
crate::perf::Counter::ListConcatElemsCopied,
vec.len() as u64,
);
}
}
vec
}
other => {
return Err(EvalError::TypeMismatch {
expected: "list",
got: other.type_name(),
});
}
};
la.extend_from_slice(right_elems);
Ok(Value::list(la))
}
fn derivation_out_path(attrs: &NixAttrs) -> Option<String> {
match attrs.get("type")?.demand().ok()? {
Concrete::String(s) if s.chars == "derivation" => {}
_ => return None,
}
match attrs.get("outPath")?.demand().ok()? {
Concrete::String(s) => Some(s.chars.to_string()),
_ => None,
}
}
fn derivation_drv_and_out(
attrs: &NixAttrs,
) -> Result<Option<(String, String)>, EvalError> {
match attrs.get("type") {
Some(t) => match crate::eval::force_value(t)? {
Value::String(s) if s.chars == "derivation" => {}
_ => return Ok(None),
},
None => return Ok(None),
}
let drv_path = match attrs.get("drvPath") {
Some(d) => crate::eval::force_value(d)?.coerce_to_path("drvPath")?,
None => return Ok(None),
};
let out_path = match attrs.get("outPath") {
Some(o) => crate::eval::force_value(o)?.coerce_to_path("outPath")?,
None => return Ok(None),
};
Ok(Some((drv_path, out_path)))
}
fn out_path_needs_realize(out_path: &str, ctx: &StringContext) -> Option<String> {
if !out_path.starts_with("/nix/store/") {
return None;
}
for elem in ctx.iter() {
if let ContextElement::Output { drv, output } = elem {
let _ = output; return Some(drv.to_string());
}
}
None
}
impl Value {
pub(crate) fn demand_unchecked(self) -> Concrete {
match self {
Value::Null => Concrete::Null,
Value::Bool(b) => Concrete::Bool(b),
Value::Int(n) => Concrete::Int(n),
Value::Float(f) => Concrete::Float(f),
Value::String(s) => Concrete::String(s),
Value::Path(p) => Concrete::Path(p),
Value::List(l) => Concrete::List(l),
Value::Attrs(a) => Concrete::Attrs(a),
Value::Lambda(c) => Concrete::Lambda(c),
Value::Builtin(b) => Concrete::Builtin(b),
Value::Thunk(_) => panic!("demand_unchecked called on Thunk"),
}
}
}
impl Value {
pub fn demand(&self) -> Result<Concrete, EvalError> {
let v = match self {
Value::Thunk(_) => crate::eval::force_value(self)?,
other => other.clone(),
};
match v {
Value::Null => Ok(Concrete::Null),
Value::Bool(b) => Ok(Concrete::Bool(b)),
Value::Int(n) => Ok(Concrete::Int(n)),
Value::Float(f) => Ok(Concrete::Float(f)),
Value::String(s) => Ok(Concrete::String(s)),
Value::Path(p) => Ok(Concrete::Path(p)),
Value::List(l) => Ok(Concrete::List(l)),
Value::Attrs(a) => Ok(Concrete::Attrs(a)),
Value::Lambda(c) => Ok(Concrete::Lambda(c)),
Value::Builtin(b) => Ok(Concrete::Builtin(b)),
Value::Thunk(_) => {
let re_forced = crate::eval::force_value(&v)?;
match re_forced {
Value::Null => Ok(Concrete::Null),
Value::Bool(b) => Ok(Concrete::Bool(b)),
Value::Int(n) => Ok(Concrete::Int(n)),
Value::Float(f) => Ok(Concrete::Float(f)),
Value::String(s) => Ok(Concrete::String(s)),
Value::Path(p) => Ok(Concrete::Path(p)),
Value::List(l) => Ok(Concrete::List(l)),
Value::Attrs(a) => Ok(Concrete::Attrs(a)),
Value::Lambda(c) => Ok(Concrete::Lambda(c)),
Value::Builtin(b) => Ok(Concrete::Builtin(b)),
Value::Thunk(_) => Err(EvalError::InfiniteRecursion(
"demand: thunk chain could not be resolved".to_string(),
)),
}
}
}
}
}
#[cfg(target_pointer_width = "64")]
const _: () = assert!(std::mem::size_of::<Value>() <= 16);
const FIXPOINT_PROMOTE_NEST_CAP: u32 = 32;
const PROMOTION_RUNAWAY_FORCE_DEPTH: usize = 500;
thread_local! {
pub(crate) static IN_PROMISE_EVAL: std::cell::Cell<u32> = const { std::cell::Cell::new(0) };
pub(crate) static PROMOTION_OCCURRED: std::cell::Cell<bool> = const { std::cell::Cell::new(false) };
}
#[inline(always)]
pub fn promotion_occurred() -> bool {
PROMOTION_OCCURRED.with(|c| c.get())
}
#[inline(always)]
pub fn in_promise_eval() -> bool {
IN_PROMISE_EVAL.with(|c| c.get() > 0)
}
pub enum ThunkRepr {
Suspended {
expr: rnix::ast::Expr,
env: Env,
},
InheritSelect {
source_thunk: Thunk,
name: SmolStr,
},
Native(Box<dyn FnOnce() -> Result<Value, EvalError>>),
WithIdent {
name: SmolStr,
scope_cache: Rc<RefCell<Option<NixAttrs>>>,
scope_value: Value,
env: Env,
},
Blackhole,
Promise(Rc<RefCell<Value>>),
Failed(EvalError),
Evaluated(Box<Value>),
EvaluatedConcrete,
}
struct ThunkInner {
cache: OnceCell<Box<Concrete>>,
repr: UnsafeCell<ThunkRepr>,
recursive: bool,
}
impl Drop for ThunkInner {
fn drop(&mut self) {
census::dropped(&census::THUNK_LIVE);
}
}
#[derive(Clone)]
pub struct Thunk(pub(crate) Rc<ThunkInner>);
impl Thunk {
pub fn new_suspended(expr: rnix::ast::Expr, env: Env) -> Self {
crate::trace::inc_thunks_created();
census::made(&census::THUNK_MADE, &census::THUNK_LIVE);
Self(Rc::new(ThunkInner {
cache: OnceCell::new(),
repr: UnsafeCell::new(ThunkRepr::Suspended { expr, env }),
recursive: false,
}))
}
pub fn new_suspended_recursive(expr: rnix::ast::Expr, env: Env) -> Self {
crate::trace::inc_thunks_created();
census::made(&census::THUNK_MADE, &census::THUNK_LIVE);
crate::perf::inc(crate::perf::Counter::ThunkSiteLetForward);
Self(Rc::new(ThunkInner {
cache: OnceCell::new(),
repr: UnsafeCell::new(ThunkRepr::Suspended { expr, env }),
recursive: true,
}))
}
pub fn new_inherit_select(source_thunk: Thunk, name: impl Into<SmolStr>) -> Self {
crate::trace::inc_thunks_created();
census::made(&census::THUNK_MADE, &census::THUNK_LIVE);
crate::perf::inc(crate::perf::Counter::ThunkSiteInheritSrc);
Self(Rc::new(ThunkInner {
cache: OnceCell::new(),
repr: UnsafeCell::new(ThunkRepr::InheritSelect {
source_thunk,
name: name.into(),
}),
recursive: false,
}))
}
pub fn new_with_ident(
name: SmolStr,
scope_cache: Rc<RefCell<Option<NixAttrs>>>,
scope_value: Value,
env: Env,
) -> Self {
crate::trace::inc_thunks_created();
census::made(&census::THUNK_MADE, &census::THUNK_LIVE);
crate::perf::inc(crate::perf::Counter::ThunkSiteOther);
Self(Rc::new(ThunkInner {
cache: OnceCell::new(),
repr: UnsafeCell::new(ThunkRepr::WithIdent {
name,
scope_cache,
scope_value,
env,
}),
recursive: false,
}))
}
pub fn new_native(f: impl FnOnce() -> Result<Value, EvalError> + 'static) -> Self {
crate::trace::inc_thunks_created();
census::made(&census::THUNK_MADE, &census::THUNK_LIVE);
crate::perf::inc(crate::perf::Counter::ThunkSiteNative);
Self(Rc::new(ThunkInner {
cache: OnceCell::new(),
repr: UnsafeCell::new(ThunkRepr::Native(Box::new(f))),
recursive: false,
}))
}
pub fn new_evaluated(value: Value) -> Self {
crate::trace::inc_thunks_created();
census::made(&census::THUNK_MADE, &census::THUNK_LIVE);
crate::perf::inc(crate::perf::Counter::ThunkSiteEvaluated);
let cache = OnceCell::new();
let repr = if matches!(value, Value::Thunk(_)) {
ThunkRepr::Evaluated(Box::new(value))
} else {
let _ = cache.set(Box::new(value.demand_unchecked()));
ThunkRepr::EvaluatedConcrete
};
Self(Rc::new(ThunkInner {
cache,
repr: UnsafeCell::new(repr),
recursive: false,
}))
}
pub fn is_evaluated(&self) -> bool {
self.0.cache.get().is_some()
}
pub fn is_native(&self) -> bool {
matches!(unsafe { &*self.0.repr.get() }, ThunkRepr::Native(_))
}
pub fn peek(&self) -> Option<&Concrete> {
self.0.cache.get().map(|v| &**v)
}
pub fn update_env(&self, new_env: &Env) {
let repr = unsafe { &mut *self.0.repr.get() };
match repr {
ThunkRepr::Suspended { env, .. } => {
*env = new_env.clone();
}
ThunkRepr::InheritSelect { source_thunk, .. } => {
source_thunk.update_env(new_env);
}
_ => {}
}
}
#[inline]
unsafe fn store_evaluated(&self, value: &Value) {
census::evaluated();
if matches!(value, Value::Thunk(_)) {
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Evaluated(Box::new(value.clone()));
} else {
let _ = self.0.cache.set(Box::new(value.clone().demand_unchecked()));
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::EvaluatedConcrete;
}
}
#[inline]
unsafe fn store_evaluated_owned(&self, value: Value) -> Value {
census::evaluated();
let concrete = value.demand_unchecked();
let ret = concrete.clone().into_value();
let _ = self.0.cache.set(Box::new(concrete));
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::EvaluatedConcrete;
ret
}
pub fn force(
&self,
evaluator: &dyn Fn(&rnix::ast::Expr, &Env) -> Result<Value, EvalError>,
) -> Result<Value, EvalError> {
if let Some(cached) = self.0.cache.get() {
crate::perf::inc(crate::perf::Counter::ThunkHit);
return Ok((**cached).clone().into_value());
}
stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || {
self.force_inner(evaluator)
})
}
fn force_inner(
&self,
evaluator: &dyn Fn(&rnix::ast::Expr, &Env) -> Result<Value, EvalError>,
) -> Result<Value, EvalError> {
if let Some(cached) = self.0.cache.get() {
crate::perf::inc(crate::perf::Counter::ThunkHit);
return Ok((**cached).clone().into_value());
}
let thunk_id = Rc::as_ptr(&self.0) as usize;
if let ThunkRepr::Promise(cell) = unsafe { &*self.0.repr.get() } {
return Ok(cell.borrow().clone());
}
let new_repr_on_force = if self.0.recursive {
ThunkRepr::Promise(Rc::new(RefCell::new(
Value::Attrs(Rc::new(NixAttrs::new())),
)))
} else {
ThunkRepr::Blackhole
};
let is_promise = self.0.recursive;
let repr = std::mem::replace(unsafe { &mut *self.0.repr.get() }, new_repr_on_force);
match repr {
ThunkRepr::Suspended { expr, env } => {
crate::perf::inc(crate::perf::Counter::ThunkForce);
crate::trace::inc_thunks_forced_unique();
let tracing = crate::trace::trace_enabled();
let desc: String = if tracing {
expr.syntax().text().to_string().chars().take(60).collect()
} else {
String::new()
};
crate::trace::push_force(crate::trace::ForceFrame {
defined_in: env.eval_file().cloned(),
description: desc.clone(),
thunk_id,
});
if crate::value::promotion_occurred()
&& crate::trace::current_force_depth() as usize
> PROMOTION_RUNAWAY_FORCE_DEPTH
{
crate::trace::pop_force();
*unsafe { &mut *self.0.repr.get() } =
ThunkRepr::Suspended { expr, env };
return Err(EvalError::InfiniteRecursion(
"overlay-fixpoint promotion runaway (force depth exceeded)".into(),
));
}
if tracing {
crate::trace::trace_force_enter(
env.eval_file().map(|p| p.as_path()),
&desc,
);
if let Err(msg) = crate::trace::check_force_depth() {
crate::trace::dump_trace_on_error();
crate::trace::pop_force();
crate::trace::trace_force_exit();
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Suspended {
expr,
env,
};
return Err(EvalError::InfiniteRecursion(msg));
}
}
let _file_guard = env.eval_file().cloned().map(crate::eval::push_eval_file);
let _srcid_guard = crate::eval::push_source_id(env.source_id());
if is_promise {
IN_PROMISE_EVAL.with(|c| c.set(c.get() + 1));
}
let result = evaluator(&expr, &env);
if is_promise {
IN_PROMISE_EVAL.with(|c| c.set(c.get().saturating_sub(1)));
}
let became_promise = !is_promise
&& matches!(unsafe { &*self.0.repr.get() }, ThunkRepr::Promise(_));
if became_promise {
IN_PROMISE_EVAL.with(|c| c.set(c.get().saturating_sub(1)));
}
match result {
Ok(mut value) => {
crate::perf::inc(crate::perf::Counter::ThunkStoreWrites);
if is_promise || became_promise {
if let ThunkRepr::Promise(cell) = unsafe { &*self.0.repr.get() } {
*cell.borrow_mut() = value.clone();
}
}
let was_thunk_before_loop = matches!(value, Value::Thunk(_));
if !was_thunk_before_loop {
crate::perf::inc(crate::perf::Counter::ThunkStoreRedundant);
let ret = unsafe { self.store_evaluated_owned(value) };
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
return Ok(ret);
}
unsafe { self.store_evaluated(&value) };
while let Value::Thunk(ref inner) = value {
match inner.peek() {
Some(cached) => value = cached.clone().into_value(),
None => break,
}
}
if !matches!(value, Value::Thunk(_)) {
crate::perf::inc(crate::perf::Counter::ThunkStoreLoopMutated);
}
unsafe { self.store_evaluated(&value) };
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
Ok(value)
}
Err(e) => {
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Suspended { expr, env };
if tracing { crate::trace::dump_trace_on_error(); }
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
Err(e)
}
}
}
ThunkRepr::InheritSelect { source_thunk, name } => {
let tracing = crate::trace::trace_enabled();
let desc = if tracing { format!("inherit (..) {name}") } else { String::new() };
crate::trace::push_force(crate::trace::ForceFrame {
defined_in: None,
description: desc.clone(),
thunk_id,
});
if tracing {
crate::trace::trace_force_enter(None, &desc);
}
crate::trace::inc_thunks_forced_unique();
if tracing {
if let Err(msg) = crate::trace::check_force_depth() {
crate::trace::dump_trace_on_error();
crate::trace::pop_force();
crate::trace::trace_force_exit();
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::InheritSelect {
source_thunk,
name,
};
return Err(EvalError::InfiniteRecursion(msg));
}
}
let attempt = (|| -> Result<Value, EvalError> {
let mut forced = source_thunk.force(evaluator)?;
while let Value::Thunk(inner) = forced {
forced = inner.force(evaluator)?;
}
let attrs = match &forced {
Value::Attrs(a) => a,
_ => {
return Err(EvalError::TypeError(format!(
"inherit (source) {name}: source is {}, not a set",
forced.type_name()
)))
}
};
attrs
.get(&name)
.cloned()
.ok_or_else(|| EvalError::AttrNotFound(name.to_string()))
})();
match attempt {
Ok(mut value) => {
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Evaluated(Box::new(value.clone()));
while let Value::Thunk(ref inner) = value {
match inner.peek() { Some(c) => value = c.clone().into_value(), None => break }
}
unsafe { self.store_evaluated(&value) };
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
Ok(value)
}
Err(e) => {
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::InheritSelect { source_thunk, name };
if tracing { crate::trace::dump_trace_on_error(); }
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
Err(e)
}
}
}
ThunkRepr::Native(f) => {
let tracing = crate::trace::trace_enabled();
crate::trace::push_force(crate::trace::ForceFrame {
defined_in: None,
description: if tracing { "<native-thunk>".into() } else { String::new() },
thunk_id,
});
if tracing {
crate::trace::trace_force_enter(None, "<native-thunk>");
}
crate::trace::inc_thunks_forced_unique();
match f() {
Ok(mut value) => {
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Evaluated(Box::new(value.clone()));
while let Value::Thunk(ref inner) = value {
match inner.peek() { Some(c) => value = c.clone().into_value(), None => break }
}
unsafe { self.store_evaluated(&value) };
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
Ok(value)
}
Err(e) => {
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Failed(e.clone());
if tracing { crate::trace::dump_trace_on_error(); }
crate::trace::pop_force();
if tracing { crate::trace::trace_force_exit(); }
Err(e)
}
}
}
ThunkRepr::WithIdent { name, scope_cache, scope_value, env } => {
crate::perf::inc(crate::perf::Counter::ThunkForce);
crate::trace::inc_thunks_forced_unique();
{
let cache = scope_cache.borrow();
if let Some(ref attrs) = *cache {
if let Some(v) = attrs.get(&name) {
let value = v.clone();
unsafe { self.store_evaluated(&value) };
return Ok(value);
}
}
}
if let Ok(forced) = crate::eval::force_value(&scope_value) {
if let Value::Attrs(ref attrs) = forced {
*scope_cache.borrow_mut() = Some((**attrs).clone());
if let Some(v) = attrs.get(&name) {
let value = v.clone();
unsafe { self.store_evaluated(&value) };
return Ok(value);
}
}
}
let result = match env.lookup(&name) {
Some(v) => v,
None => match env.lookup_fresh(&name) {
Some(v) => v,
None if in_promise_eval() => Value::Null,
None => return Err(EvalError::UndefinedVar(format!("'{name}'"))),
},
};
unsafe { self.store_evaluated(&result) };
Ok(result)
}
ThunkRepr::Blackhole => {
if std::env::var_os("SUI_BLACKHOLE_AS_NULL").is_some() {
return Ok(Value::Null);
}
if std::env::var_os("SUI_BLACKHOLE_AS_EMPTY_LIST").is_some() {
return Ok(Value::List(Rc::new(NixList::new(Vec::new()))));
}
if std::env::var_os("SUI_BLACKHOLE_AS_EMPTY_ATTRS").is_some() {
return Ok(Value::Attrs(Rc::new(NixAttrs::new())));
}
if std::env::var_os("SUI_DEBUG_CYCLE").is_some() {
let same = crate::trace::force_stack_contains(thunk_id);
eprintln!(
"[SUI_DEBUG_CYCLE] blackhole re-entry thunk_id={thunk_id:#x} same_thunk_on_stack={same} recursive_flag={}",
self.0.recursive
);
crate::trace::dump_force_stack_ids();
}
if crate::trace::force_stack_contains(thunk_id)
&& IN_PROMISE_EVAL.with(|c| c.get()) < FIXPOINT_PROMOTE_NEST_CAP
{
if std::env::var_os("SUI_DEBUG_CYCLE").is_some() {
let chain = crate::trace::capture_cycle(thunk_id);
let nest = IN_PROMISE_EVAL.with(|c| c.get());
let fdepth = crate::trace::current_force_depth();
eprintln!("[SUI_PROMOTE] thunk_id={thunk_id:#x} cycle_len={} nest={nest} fdepth={fdepth}", chain.0.len());
}
let cell = Rc::new(RefCell::new(
Value::Attrs(Rc::new(NixAttrs::new())),
));
*unsafe { &mut *self.0.repr.get() } =
ThunkRepr::Promise(cell.clone());
IN_PROMISE_EVAL.with(|c| c.set(c.get() + 1));
PROMOTION_OCCURRED.with(|c| c.set(true));
return Ok(cell.borrow().clone());
}
let chain = crate::trace::capture_cycle(thunk_id);
crate::trace::dump_trace_on_error();
Err(EvalError::InfiniteRecursion(chain.to_string()))
}
ThunkRepr::Promise(cell) => {
Ok(cell.borrow().clone())
}
ThunkRepr::Evaluated(v) => {
crate::perf::inc(crate::perf::Counter::ThunkHit);
let cloned = (*v).clone();
if !matches!(cloned, Value::Thunk(_)) {
if !matches!(cloned, Value::Thunk(_)) { let _ = self.0.cache.set(Box::new(cloned.clone().demand_unchecked())); }
}
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Evaluated(v);
Ok(cloned)
}
ThunkRepr::EvaluatedConcrete => {
crate::perf::inc(crate::perf::Counter::ThunkHit);
let value = self
.0
.cache
.get()
.expect("EvaluatedConcrete implies a populated cache")
.as_ref()
.clone()
.into_value();
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::EvaluatedConcrete;
Ok(value)
}
ThunkRepr::Failed(e) => {
let err = e.clone();
*unsafe { &mut *self.0.repr.get() } = ThunkRepr::Failed(e);
Err(err)
}
}
}
}
impl fmt::Debug for Thunk {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match unsafe { &*self.0.repr.get() } {
ThunkRepr::Suspended { .. } => write!(f, "<thunk>"),
ThunkRepr::InheritSelect { name, .. } => write!(f, "<inherit-select {name}>"),
ThunkRepr::Native(_) => write!(f, "<native-thunk>"),
ThunkRepr::WithIdent { name, .. } => write!(f, "<with-ident {name}>"),
ThunkRepr::Blackhole => write!(f, "<blackhole>"),
ThunkRepr::Promise(_) => write!(f, "<promise>"),
ThunkRepr::Failed(e) => write!(f, "<failed-thunk: {e}>"),
ThunkRepr::Evaluated(v) => write!(f, "{v:?}"),
ThunkRepr::EvaluatedConcrete => match self.0.cache.get() {
Some(c) => write!(f, "{:?}", c.as_ref().clone().into_value()),
None => write!(f, "<evaluated-concrete>"),
},
}
}
}
pub struct NixAttrs(AttrsInner, Option<Rc<crate::pos::AttrPositions>>);
impl Clone for NixAttrs {
fn clone(&self) -> Self {
census::made(&census::ATTRS_MADE, &census::ATTRS_LIVE);
NixAttrs(self.0.clone(), self.1.clone())
}
}
impl Drop for NixAttrs {
fn drop(&mut self) {
census::dropped(&census::ATTRS_LIVE);
}
}
#[derive(Clone)]
enum AttrsInner {
Flat(AttrsMap<Symbol, Value>),
Overlay {
left: RefCell<Rc<NixAttrs>>,
right: RefCell<Rc<NixAttrs>>,
cache: Rc<OnceCell<AttrsMap<Symbol, Value>>>,
},
}
impl fmt::Debug for NixAttrs {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "NixAttrs({})", self.len())
}
}
impl Default for NixAttrs {
fn default() -> Self {
census::made(&census::ATTRS_MADE, &census::ATTRS_LIVE);
Self(AttrsInner::Flat(AttrsMap::default()), None)
}
}
impl NixAttrs {
pub fn new() -> Self {
Self::default()
}
pub fn with_capacity(_capacity: usize) -> Self {
Self::default()
}
pub fn set_positions(&mut self, pos: Rc<crate::pos::AttrPositions>) {
self.1 = Some(pos);
}
#[must_use]
pub fn positions(&self) -> Option<&Rc<crate::pos::AttrPositions>> {
self.1.as_ref()
}
#[must_use]
pub fn pos_for(&self, key: &str) -> Option<crate::pos::ResolvedPos> {
let sym = intern(key);
let (file, offset) = self.pos_entry(sym)?;
crate::pos::resolve(file.as_deref(), offset)
}
fn pos_entry(&self, sym: Symbol) -> Option<(Option<std::path::PathBuf>, u32)> {
if let Some(table) = self.1.as_ref() {
if let Some(offset) = table.keys.get(&sym) {
return Some((table.file.clone(), *offset));
}
}
match &self.0 {
AttrsInner::Overlay { left, right, .. } => {
let r = right.borrow().pos_entry(sym);
if r.is_some() {
return r;
}
let l = left.borrow().pos_entry(sym);
l
}
_ => None,
}
}
#[must_use]
pub fn inner(&self) -> AttrsMap<Symbol, Value> {
self.as_flat().clone()
}
fn as_flat(&self) -> &AttrsMap<Symbol, Value> {
match &self.0 {
AttrsInner::Flat(m) => m,
AttrsInner::Overlay { left, right, cache } => {
crate::perf::inc(crate::perf::Counter::OverlayFlattenAttempt);
let flat = cache.get_or_init(|| {
crate::perf::inc(crate::perf::Counter::OverlayFlattenBuild);
let timed = crate::perf::enabled();
let t0 = if timed { Some(std::time::Instant::now()) } else { None };
let mut result = left.borrow().as_flat().clone();
for (k, v) in right.borrow().as_flat().iter() {
result.insert(*k, v.clone());
}
crate::perf::add(
crate::perf::Counter::OverlayFlattenEntries,
result.len() as u64,
);
if let Some(t0) = t0 {
crate::trace::add_overlay_flatten_nanos(t0.elapsed().as_nanos());
}
result
});
{
let mut l = left.borrow_mut();
if !l.is_empty() { *l = Rc::new(l.position_husk()); }
}
{
let mut r = right.borrow_mut();
if !r.is_empty() { *r = Rc::new(r.position_husk()); }
}
flat
}
}
}
fn position_husk(&self) -> NixAttrs {
match &self.0 {
AttrsInner::Overlay { left, right, .. } => {
let (l, r) = (left.borrow().position_husk(), right.borrow().position_husk());
if l.1.is_none() && r.1.is_none() && !matches!(l.0, AttrsInner::Overlay { .. })
&& !matches!(r.0, AttrsInner::Overlay { .. })
{
return NixAttrs(AttrsInner::Flat(AttrsMap::default()), self.1.clone());
}
NixAttrs(
AttrsInner::Overlay {
left: RefCell::new(Rc::new(l)),
right: RefCell::new(Rc::new(r)),
cache: Rc::new(OnceCell::new()),
},
self.1.clone(),
)
}
AttrsInner::Flat(_) => NixAttrs(AttrsInner::Flat(AttrsMap::default()), self.1.clone()),
}
}
fn sorted_entries(&self) -> Vec<(String, &Value)> {
crate::perf::inc(crate::perf::Counter::SortedEntriesCalls);
let m = self.as_flat();
crate::perf::add(crate::perf::Counter::SortedEntriesRows, m.len() as u64);
let timed = crate::perf::enabled();
let t0 = if timed { Some(std::time::Instant::now()) } else { None };
let mut pairs: Vec<(String, &Value)> = m.iter()
.map(|(sym, v)| (resolve(*sym), v))
.collect();
pairs.sort_by(|(a, _), (b, _)| a.cmp(b));
if let Some(t0) = t0 {
crate::trace::add_sorted_entries_nanos(t0.elapsed().as_nanos());
}
pairs
}
#[must_use]
pub fn get(&self, key: &str) -> Option<&Value> {
let sym = intern(key);
self.get_sym(&sym)
}
#[must_use]
pub fn get_sym(&self, sym: &Symbol) -> Option<&Value> {
match &self.0 {
AttrsInner::Flat(m) => m.get(sym),
AttrsInner::Overlay { .. } => self.as_flat().get(sym),
}
}
pub fn insert(&mut self, key: String, value: Value) {
self.ensure_flat();
if let AttrsInner::Flat(ref mut m) = self.0 {
m.insert(intern(&key), value);
}
}
fn ensure_flat(&mut self) {
if matches!(self.0, AttrsInner::Overlay { .. }) {
self.0 = AttrsInner::Flat(self.as_flat().clone());
}
}
#[must_use]
pub fn contains_key(&self, key: &str) -> bool {
let sym = intern(key);
self.contains_key_sym(&sym)
}
#[must_use]
pub fn contains_key_sym(&self, sym: &Symbol) -> bool {
match &self.0 {
AttrsInner::Flat(m) => m.contains_key(sym),
AttrsInner::Overlay { .. } => self.as_flat().contains_key(sym),
}
}
pub fn keys(&self) -> impl Iterator<Item = String> {
self.sorted_entries().into_iter().map(|(k, _)| k)
}
pub fn iter(&self) -> impl Iterator<Item = (String, &Value)> {
self.sorted_entries().into_iter()
}
pub fn iter_unsorted(&self) -> impl Iterator<Item = (String, &Value)> {
self.as_flat().iter().map(|(sym, v)| (resolve(*sym), v)).collect::<Vec<_>>().into_iter()
}
pub fn iter_syms(&self) -> impl Iterator<Item = (Symbol, &Value)> {
self.as_flat().iter().map(|(sym, v)| (*sym, v))
}
pub fn insert_sym(&mut self, sym: Symbol, value: Value) {
self.ensure_flat();
if let AttrsInner::Flat(ref mut m) = self.0 {
m.insert(sym, value);
}
}
pub fn values(&self) -> impl Iterator<Item = &Value> {
self.sorted_entries().into_iter().map(|(_, v)| v)
}
pub fn remove(&mut self, key: &str) -> Option<Value> {
self.ensure_flat();
if let AttrsInner::Flat(ref mut m) = self.0 {
m.remove(&intern(key))
} else {
None
}
}
#[must_use]
pub fn len(&self) -> usize {
match &self.0 {
AttrsInner::Flat(m) => m.len(),
AttrsInner::Overlay { .. } => {
self.as_flat().len()
}
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
match &self.0 {
AttrsInner::Flat(m) => m.is_empty(),
AttrsInner::Overlay { .. } => self.as_flat().is_empty(),
}
}
#[must_use]
pub fn overlay(self, other: NixAttrs) -> NixAttrs {
if other.is_empty() { return self; }
if self.is_empty() { return other; }
crate::perf::inc(crate::perf::Counter::OverlayCreated);
census::made(&census::ATTRS_MADE, &census::ATTRS_LIVE);
NixAttrs(AttrsInner::Overlay {
left: RefCell::new(Rc::new(self)),
right: RefCell::new(Rc::new(other)),
cache: Rc::new(OnceCell::new()),
}, None)
}
#[must_use]
pub fn update(&self, other: &NixAttrs) -> NixAttrs {
match (&self.0, &other.0) {
(AttrsInner::Flat(l), AttrsInner::Flat(r)) => {
let mut result = l.clone();
for (k, v) in r.iter() {
result.insert(*k, v.clone());
}
census::made(&census::ATTRS_MADE, &census::ATTRS_LIVE);
NixAttrs(AttrsInner::Flat(result), None)
}
_ => {
let mut result = self.as_flat().clone();
let other_flat = other.as_flat();
for (k, v) in other_flat.iter() {
result.insert(*k, v.clone());
}
census::made(&census::ATTRS_MADE, &census::ATTRS_LIVE);
NixAttrs(AttrsInner::Flat(result), None)
}
}
}
}
impl FromIterator<(String, Value)> for NixAttrs {
fn from_iter<I: IntoIterator<Item = (String, Value)>>(iter: I) -> Self {
census::made(&census::ATTRS_MADE, &census::ATTRS_LIVE);
NixAttrs(AttrsInner::Flat(iter.into_iter().map(|(k, v)| (intern(&k), v)).collect()), None)
}
}
impl IntoIterator for NixAttrs {
type Item = (String, Value);
type IntoIter = Box<dyn Iterator<Item = (String, Value)>>;
fn into_iter(self) -> Self::IntoIter {
let flat = self.as_flat().clone();
Box::new(flat.into_iter().map(|(sym, v)| (resolve(sym), v)))
}
}
#[derive(Debug, Clone)]
pub struct Closure {
pub param: rnix::ast::Param,
pub body: rnix::ast::Expr,
pub env: Env,
}
pub type BuiltinFunc = dyn Fn(&[Value]) -> Result<Value, EvalError>;
#[derive(Clone)]
pub struct BuiltinFn {
pub name: &'static str,
pub func: Rc<BuiltinFunc>,
}
impl fmt::Debug for BuiltinFn {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "<builtin {}>", self.name)
}
}
#[derive(Clone)]
struct WithScope {
value: Value,
cached: Rc<RefCell<Option<NixAttrs>>>,
}
impl fmt::Debug for WithScope {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("WithScope")
.field("value", &self.value)
.field("cached", &self.cached.borrow().is_some())
.finish()
}
}
#[derive(Debug, Clone, Default)]
struct EnvInner {
bindings: FxHashMap<Symbol, Value>,
with_scopes: Vec<WithScope>,
eval_file: Option<std::path::PathBuf>,
source_id: u32,
}
#[derive(Clone, Default)]
pub struct Env(Rc<EnvInner>);
impl fmt::Debug for Env {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
self.0.fmt(f)
}
}
impl Drop for EnvInner {
fn drop(&mut self) {
census::dropped(&census::ENV_LIVE);
}
}
impl Env {
#[must_use]
pub fn new() -> Self {
census::made(&census::ENV_MADE, &census::ENV_LIVE);
Self(Rc::new(EnvInner {
bindings: FxHashMap::default(),
with_scopes: Vec::new(),
eval_file: None,
source_id: 0,
}))
}
#[must_use]
pub fn child(&self) -> Self {
crate::perf::inc(crate::perf::Counter::EnvClone);
census::made(&census::ENV_MADE, &census::ENV_LIVE);
Self(Rc::new(EnvInner {
bindings: self.0.bindings.clone(), with_scopes: self.0.with_scopes.clone(),
eval_file: self.0.eval_file.clone(),
source_id: self.0.source_id,
}))
}
#[must_use]
pub fn with_scope(mut self, value: Value) -> Self {
let pre_cached = match &value {
Value::Attrs(attrs) => Some((**attrs).clone()),
Value::Thunk(thunk) => thunk.peek().and_then(|v| {
if let Concrete::Attrs(attrs) = v { Some((**attrs).clone()) } else { None }
}),
_ => None,
};
Rc::make_mut(&mut self.0).with_scopes.push(WithScope {
value,
cached: Rc::new(RefCell::new(pre_cached)),
});
self
}
pub fn bind(&mut self, name: String, value: Value) {
Rc::make_mut(&mut self.0).bindings.insert(intern(&name), value);
}
pub fn bind_many(&mut self, pairs: impl IntoIterator<Item = (String, Value)>) {
let inner = Rc::make_mut(&mut self.0);
for (name, value) in pairs {
inner.bindings.insert(intern(&name), value);
}
}
#[must_use]
pub fn eval_file(&self) -> Option<&std::path::PathBuf> {
self.0.eval_file.as_ref()
}
pub fn set_eval_file(&mut self, file: Option<std::path::PathBuf>) {
Rc::make_mut(&mut self.0).eval_file = file;
}
#[must_use]
pub fn source_id(&self) -> u32 {
self.0.source_id
}
pub fn set_source_id(&mut self, id: u32) {
Rc::make_mut(&mut self.0).source_id = id;
}
#[must_use]
pub fn binding_count(&self) -> usize {
self.0.bindings.len()
}
#[must_use]
pub fn binding_names_preview(&self, n: usize) -> Vec<String> {
self.0.bindings.keys().take(n).map(|s| resolve(*s)).collect()
}
#[must_use]
pub fn with_scope_count(&self) -> usize {
self.0.with_scopes.len()
}
#[must_use]
pub fn lookup_lexical(&self, name: &str) -> Option<Value> {
let sym = intern(name);
self.0.bindings.get(&sym).cloned()
}
#[must_use]
pub fn lookup_lexical_sym(&self, sym: Symbol) -> Option<Value> {
self.0.bindings.get(&sym).cloned()
}
#[must_use]
pub fn lookup_with_cache_only(&self, name: &str) -> Option<Value> {
for scope in self.0.with_scopes.iter().rev() {
let cache = scope.cached.borrow();
if let Some(ref attrs) = *cache {
if let Some(v) = attrs.get(name) {
return Some(v.clone());
}
}
drop(cache);
if let Value::Thunk(ref thunk) = scope.value {
if let Some(cached_val) = thunk.peek() {
if let Concrete::Attrs(ref attrs) = *cached_val {
*scope.cached.borrow_mut() = Some((**attrs).clone());
if let Some(v) = attrs.get(name) {
return Some(v.clone());
}
}
}
} else if let Value::Attrs(ref attrs) = scope.value {
*scope.cached.borrow_mut() = Some((**attrs).clone());
if let Some(v) = attrs.get(name) {
return Some(v.clone());
}
}
}
None
}
#[must_use]
pub fn innermost_with_scope(&self) -> Option<(Rc<RefCell<Option<NixAttrs>>>, Value)> {
self.0.with_scopes.last().map(|scope| {
(scope.cached.clone(), scope.value.clone())
})
}
#[must_use]
pub fn lookup(&self, name: &str) -> Option<Value> {
self.lookup_fast(intern(name), name)
}
#[must_use]
pub fn lookup_fresh(&self, name: &str) -> Option<Value> {
let sym = intern(name);
if let Some(v) = self.0.bindings.get(&sym) {
return Some(v.clone());
}
for scope in self.0.with_scopes.iter().rev() {
if let Ok(Value::Attrs(attrs)) = crate::eval::force_value(&scope.value) {
if let Some(v) = attrs.get_sym(&sym) {
*scope.cached.borrow_mut() = Some((*attrs).clone());
return Some(v.clone());
}
}
}
None
}
#[must_use]
pub fn lookup_fast(&self, sym: Symbol, name: &str) -> Option<Value> {
crate::perf::inc(crate::perf::Counter::EnvLookup);
if let Some(v) = self.0.bindings.get(&sym) {
return Some(v.clone());
}
for scope in self.0.with_scopes.iter().rev() {
{
let cache = scope.cached.borrow();
if let Some(ref attrs) = *cache {
if let Some(v) = attrs.get_sym(&sym) {
return Some(v.clone());
}
continue;
}
}
let resolved = match &scope.value {
Value::Attrs(attrs) => {
crate::perf::inc(crate::perf::Counter::WithScopeCacheClone);
*scope.cached.borrow_mut() = Some((**attrs).clone());
Some((**attrs).clone())
}
Value::Thunk(thunk) => {
if let Some(cached_val) = thunk.peek() {
if let Concrete::Attrs(ref attrs) = *cached_val {
crate::perf::inc(crate::perf::Counter::WithScopeCacheClone);
*scope.cached.borrow_mut() = Some((**attrs).clone());
Some((**attrs).clone())
} else {
None
}
} else {
match crate::eval::force_value(&scope.value) {
Ok(forced) => {
if let Value::Attrs(ref attrs) = forced {
crate::perf::inc(crate::perf::Counter::WithScopeCacheClone);
*scope.cached.borrow_mut() = Some((**attrs).clone());
Some((**attrs).clone())
} else {
None
}
}
Err(_) => None, }
}
}
_ => {
match crate::eval::force_value(&scope.value) {
Ok(forced) => {
if let Value::Attrs(ref attrs) = forced {
crate::perf::inc(crate::perf::Counter::WithScopeCacheClone);
*scope.cached.borrow_mut() = Some((**attrs).clone());
Some((**attrs).clone())
} else {
None
}
}
Err(_) => None,
}
}
};
if let Some(ref attrs) = resolved {
if let Some(v) = attrs.get(name) {
return Some(v.clone());
}
}
}
None
}
#[must_use]
pub fn lookup_sym(&self, sym: Symbol) -> Option<Value> {
crate::perf::inc(crate::perf::Counter::EnvLookup);
if let Some(v) = self.0.bindings.get(&sym) {
return Some(v.clone());
}
for scope in self.0.with_scopes.iter().rev() {
{
let cache = scope.cached.borrow();
if let Some(ref attrs) = *cache {
if let Some(v) = attrs.get_sym(&sym) {
return Some(v.clone());
}
continue;
}
}
if let Ok(forced) = crate::eval::force_value_tracked(&scope.value, "with_scope") {
if let Value::Attrs(ref attrs) = forced {
let result = attrs.get_sym(&sym).cloned();
crate::perf::inc(crate::perf::Counter::WithScopeCacheClone);
*scope.cached.borrow_mut() = Some((**attrs).clone());
if result.is_some() {
return result;
}
}
}
}
None
}
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum EvalError {
#[error("undefined variable: {0}")]
UndefinedVar(String),
#[error("type error: {0}")]
TypeError(String),
#[error("attribute not found: {0}")]
AttrNotFound(String),
#[error("type error: expected {expected}, got {got}")]
TypeMismatch {
expected: &'static str,
got: &'static str,
},
#[error("assertion failed{0}")]
AssertionFailed(String),
#[error("division by zero")]
DivisionByZero,
#[error("infinite recursion ({0})")]
InfiniteRecursion(String),
#[error("I/O error: {context}: {message}")]
IoError { context: String, message: String },
#[error("{0}")]
Throw(String),
#[error("{0}")]
Abort(String),
#[error("not yet implemented: {0}")]
NotImplemented(String),
#[error("parse error: {0}")]
ParseError(String),
#[error("recursion limit: {0}")]
RecursionLimit(String),
}
impl EvalError {
#[must_use]
pub fn type_error(msg: impl Into<String>) -> Self {
EvalError::TypeError(msg.into())
}
#[must_use]
pub fn type_mismatch(expected: &'static str, got: &'static str) -> Self {
EvalError::TypeMismatch { expected, got }
}
#[must_use]
pub fn builtin_type(builtin: &str, expected: &str, got: &str) -> Self {
EvalError::TypeError(format!("{builtin}: expected {expected}, got {got}"))
}
#[must_use]
pub fn op_type(op: &str, lhs: &str, rhs: &str) -> Self {
EvalError::TypeError(format!(
"cannot {op} {lhs} and {rhs}{}",
crate::eval::eval_file_ctx()
))
}
#[must_use]
pub fn is_throw(&self) -> bool {
matches!(self, EvalError::Throw(_))
}
#[must_use]
pub fn is_infinite_recursion(&self) -> bool {
matches!(self, EvalError::InfiniteRecursion(_))
}
}
impl Value {
#[must_use]
pub fn string(s: impl Into<SmolStr>) -> Self {
Value::String(Rc::new(NixString::plain(s)))
}
#[must_use]
pub fn list(items: Vec<Value>) -> Self {
Value::List(Rc::new(NixList::new(items)))
}
#[must_use]
pub fn is_uniquely_owned_list(&self) -> bool {
matches!(self, Value::List(rc) if Rc::strong_count(rc) == 1)
}
#[must_use]
pub fn to_json(&self) -> serde_json::Value {
match self {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Int(n) => serde_json::json!(n),
Value::Float(f) => serde_json::json!(f),
Value::String(s) => serde_json::Value::String(s.chars.to_string()),
Value::Path(p) => serde_json::Value::String(p.to_string()),
Value::List(items) => {
serde_json::Value::Array(items.iter().map(|v| v.to_json()).collect())
}
Value::Attrs(attrs) => {
if attrs.get("__toString").is_some() || attrs.get("outPath").is_some() {
if let Ok((s, _ctx)) = self.coerce_to_string() {
return serde_json::Value::String(s);
}
}
let map: serde_json::Map<String, serde_json::Value> = attrs
.iter()
.map(|(k, v)| (k.clone(), v.to_json()))
.collect();
serde_json::Value::Object(map)
}
Value::Lambda(_) => serde_json::Value::String("<lambda>".to_string()),
Value::Builtin(b) => serde_json::Value::String(format!("<builtin {}>", b.name)),
Value::Thunk(thunk) => {
match thunk.force(&|expr, env| crate::eval::eval_expr(expr, env)) {
Ok(v) => v.to_json(),
Err(_) => serde_json::Value::String("<thunk:error>".to_string()),
}
}
}
}
pub fn try_to_json(&self) -> Result<serde_json::Value, EvalError> {
Ok(match self {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Int(n) => serde_json::json!(n),
Value::Float(f) => serde_json::json!(f),
Value::String(s) => serde_json::Value::String(s.chars.to_string()),
Value::Path(p) => serde_json::Value::String(p.to_string()),
Value::List(items) => {
let mut out = Vec::with_capacity(items.len());
for v in items.iter() {
out.push(v.try_to_json()?);
}
serde_json::Value::Array(out)
}
Value::Attrs(attrs) => {
if let Some(v) = attrs.get("outPath").or_else(|| attrs.get("__toString")) {
return v.try_to_json();
}
let mut map = serde_json::Map::new();
for (k, v) in attrs.iter() {
map.insert(k.clone(), v.try_to_json()?);
}
serde_json::Value::Object(map)
}
Value::Lambda(_) => {
return Err(EvalError::TypeError(
"cannot convert a function to JSON".to_string(),
))
}
Value::Builtin(b) => {
return Err(EvalError::TypeError(format!(
"cannot convert a function to JSON (builtin '{}')",
b.name
)))
}
Value::Thunk(thunk) => {
let forced = thunk.force(&|expr, env| crate::eval::eval_expr(expr, env))?;
forced.try_to_json()?
}
})
}
pub fn to_json_with_context(
&self,
ctx: &mut StringContext,
) -> Result<serde_json::Value, EvalError> {
Ok(match self {
Value::Null => serde_json::Value::Null,
Value::Bool(b) => serde_json::Value::Bool(*b),
Value::Int(n) => serde_json::json!(n),
Value::Float(f) => serde_json::json!(f),
Value::String(s) => {
ctx.merge(&s.context);
serde_json::Value::String(s.chars.to_string())
}
Value::Path(_) => {
let (str, c) = self.coerce_to_string_copy_to_store()?;
ctx.merge(&c);
serde_json::Value::String(str)
}
Value::List(items) => {
let mut arr = Vec::with_capacity(items.len());
for v in items.iter() {
let fv = crate::eval::force_value(v)?;
arr.push(fv.to_json_with_context(ctx)?);
}
serde_json::Value::Array(arr)
}
Value::Attrs(attrs) => {
if attrs.get("__toString").is_some() || attrs.get("outPath").is_some() {
let (s, c) = self.coerce_to_string_copy_to_store()?;
ctx.merge(&c);
return Ok(serde_json::Value::String(s));
}
let mut map = serde_json::Map::new();
for (k, v) in attrs.iter() {
let fv = crate::eval::force_value(v)?;
map.insert(k.clone(), fv.to_json_with_context(ctx)?);
}
serde_json::Value::Object(map)
}
Value::Thunk(_) => {
let forced = crate::eval::force_value(self)?;
forced.to_json_with_context(ctx)?
}
other => {
return Err(EvalError::TypeError(format!(
"cannot serialize {} to JSON (__structuredAttrs)",
other.type_name()
)));
}
})
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
Value::Null => "null",
Value::Bool(_) => "bool",
Value::Int(_) => "int",
Value::Float(_) => "float",
Value::String(_) => "string",
Value::Path(_) => "path",
Value::List(_) => "list",
Value::Attrs(_) => "set",
Value::Lambda(_) => "lambda",
Value::Builtin(_) => "lambda",
Value::Thunk(thunk) => {
match thunk.force(&|expr, env| crate::eval::eval_expr(expr, env)) {
Ok(v) => v.type_name(),
Err(_) => "thunk",
}
}
}
}
pub fn as_bool(&self) -> Result<bool, EvalError> {
match self {
Value::Bool(b) => Ok(*b),
Value::Thunk(thunk) => {
thunk.force(&|e, env| crate::eval::eval_expr(e, env))?.as_bool()
}
_ if in_promise_eval() => Ok(false),
_ => Err(EvalError::TypeMismatch { expected: "bool", got: self.type_name() }),
}
}
pub fn as_int(&self) -> Result<i64, EvalError> {
match self {
Value::Int(n) => Ok(*n),
Value::Thunk(thunk) => {
thunk.force(&|e, env| crate::eval::eval_expr(e, env))?.as_int()
}
_ if in_promise_eval() => Ok(0),
_ => Err(EvalError::TypeMismatch { expected: "int", got: self.type_name() }),
}
}
pub fn as_string(&self) -> Result<&str, EvalError> {
match self {
Value::String(s) => Ok(&s.chars),
Value::Thunk(_) => Err(EvalError::TypeError(
"thunk in as_string: force first via force_value()".into(),
)),
_ if in_promise_eval() => Ok(""),
_ => Err(EvalError::TypeMismatch { expected: "string", got: self.type_name() }),
}
}
pub fn as_nix_string(&self) -> Result<&NixString, EvalError> {
match self {
Value::String(ns) => Ok(ns),
Value::Thunk(_) => Err(EvalError::TypeError(
"thunk in as_nix_string: force first via force_value()".into(),
)),
_ => Err(EvalError::TypeMismatch { expected: "string", got: self.type_name() }),
}
}
pub fn to_str(&self) -> Result<String, EvalError> {
match self {
Value::String(s) => Ok(s.chars.to_string()),
Value::Thunk(thunk) => {
let forced = thunk.force(&|e, env| crate::eval::eval_expr(e, env))?;
forced.to_str()
}
_ if in_promise_eval() => Ok(String::new()),
_ => Err(EvalError::TypeMismatch { expected: "string", got: self.type_name() }),
}
}
pub fn to_nix_string(&self) -> Result<NixString, EvalError> {
match self {
Value::String(s) => Ok((**s).clone()),
Value::Thunk(thunk) => {
let forced = thunk.force(&|e, env| crate::eval::eval_expr(e, env))?;
forced.to_nix_string()
}
_ if in_promise_eval() => Ok(NixString::plain("")),
_ => Err(EvalError::TypeMismatch { expected: "string", got: self.type_name() }),
}
}
pub fn as_attrs(&self) -> Result<&NixAttrs, EvalError> {
match self {
Value::Attrs(a) => Ok(a),
Value::Thunk(_) => Err(EvalError::TypeError(
"thunk in as_attrs: force first via force_value() or use to_attrs()".into(),
)),
_ => Err(EvalError::TypeMismatch { expected: "set", got: self.type_name() }),
}
}
pub fn as_list(&self) -> Result<&[Value], EvalError> {
match self {
Value::List(l) => Ok(l.as_slice()),
Value::Thunk(_) => Err(EvalError::TypeError(
"thunk in as_list: force first via force_value()".into(),
)),
_ => Err(crate::eval::attach_trace(
EvalError::TypeMismatch { expected: "list", got: self.type_name() }
)),
}
}
pub fn to_attrs(&self) -> Result<NixAttrs, EvalError> {
match self {
Value::Attrs(a) => Ok((**a).clone()),
Value::Thunk(thunk) => {
let forced = thunk.force(&|e, env| crate::eval::eval_expr(e, env))?;
forced.to_attrs()
}
_ if in_promise_eval() => Ok(NixAttrs::new()),
_ => Err(EvalError::TypeMismatch { expected: "set", got: self.type_name() }),
}
}
pub fn to_list(&self) -> Result<Vec<Value>, EvalError> {
match self {
Value::List(l) => Ok((**l).0.clone()),
Value::Thunk(thunk) => {
let forced = thunk.force(&|e, env| crate::eval::eval_expr(e, env))?;
forced.to_list()
}
_ if in_promise_eval() => Ok(Vec::new()),
_ => Err(EvalError::TypeMismatch { expected: "list", got: self.type_name() }),
}
}
pub fn coerce_to_path(&self, context: &str) -> Result<String, EvalError> {
match self {
Value::Path(p) => Ok(p.to_string()),
Value::String(ns) => Ok(ns.chars.to_string()),
Value::Attrs(attrs) => {
if let Some(out_path) = attrs.get("outPath") {
let forced = crate::eval::force_value(out_path)?;
forced.coerce_to_path(context)
} else {
Err(EvalError::TypeError(format!(
"{context}: expected path or string, got set without outPath"
)))
}
}
_ => Err(EvalError::TypeError(format!(
"{context}: expected path or string, got {}",
self.type_name()
))),
}
}
pub fn coerce_to_realized_path(&self, context: &str) -> Result<String, EvalError> {
match self {
Value::Attrs(attrs) => {
if let Some((drv_path, out_path)) = derivation_drv_and_out(attrs)? {
self.realize_if_absent(&drv_path, &out_path, context)?;
return Ok(out_path);
}
}
Value::String(ns) => {
let out_path = ns.chars.to_string();
if let Some(drv_path) = out_path_needs_realize(&out_path, &ns.context) {
self.realize_if_absent(&drv_path, &out_path, context)?;
}
return Ok(out_path);
}
_ => {}
}
self.coerce_to_path(context)
}
fn realize_if_absent(
&self,
drv_path: &str,
out_path: &str,
context: &str,
) -> Result<(), EvalError> {
let read_path = crate::path::materialize_str(out_path);
if std::path::Path::new(&read_path).exists() {
return Ok(());
}
match crate::realize::realize_output(drv_path, out_path) {
Ok(true) | Ok(false) => Ok(()),
Err(msg) => Err(EvalError::IoError {
context: context.to_string(),
message: format!(
"import-from-derivation: realizing {drv_path} -> {out_path}: {msg}"
),
}),
}
}
pub fn to_float(&self) -> Result<f64, EvalError> {
match self {
Value::Float(f) => Ok(*f),
Value::Int(n) => Ok(*n as f64),
Value::Thunk(thunk) => {
thunk.force(&|e, env| crate::eval::eval_expr(e, env))?.to_float()
}
_ => Err(EvalError::TypeMismatch { expected: "number", got: self.type_name() }),
}
}
pub fn coerce_to_string(&self) -> Result<(String, StringContext), EvalError> {
self.coerce_to_string_impl(false)
}
pub fn coerce_to_string_copy_to_store(
&self,
) -> Result<(String, StringContext), EvalError> {
self.coerce_to_string_impl(true)
}
fn coerce_to_string_impl(
&self,
copy_to_store: bool,
) -> Result<(String, StringContext), EvalError> {
let mut ctx = StringContext::new();
let s = match self {
Value::String(ns) => {
ctx.merge(&ns.context);
ns.chars.to_string()
}
Value::Path(p) => {
let raw: &str = &**p;
if copy_to_store {
let pb = std::path::Path::new(raw);
let abs = if pb.is_absolute() {
pb.to_path_buf()
} else if let Some(dir) = crate::eval::current_eval_dir() {
dir.join(pb)
} else {
std::env::current_dir()
.map_err(|e| EvalError::IoError {
context: format!("copy-to-store coercion of {raw}"),
message: e.to_string(),
})?
.join(pb)
};
let read_abs = crate::path::materialize(&abs);
let canon = read_abs.canonicalize().map_err(|_| {
EvalError::TypeError(format!(
"path '{}' does not exist",
abs.display()
))
})?;
let name = crate::path::source_name_for_read_dir(&canon)
.or_else(|| {
canon
.file_name()
.map(|n| sui_compat::source::strip_store_hash_prefix(
&n.to_string_lossy()).to_string())
})
.unwrap_or_else(|| "source".to_string());
let src = sui_compat::source::nar_hash_source_tree(&canon, &name)
.map_err(|e| {
EvalError::TypeError(format!(
"copy-to-store coercion of '{}': {e}",
canon.display()
))
})?;
ctx.add_plain(src.store_path.clone());
src.store_path
} else {
ctx.add_plain(raw.to_string());
raw.to_string()
}
}
Value::Int(n) => n.to_string(),
Value::Float(f) => format!("{f:.6}"),
Value::Bool(true) => "1".to_string(),
Value::Bool(false) => String::new(),
Value::Null => String::new(),
Value::Attrs(attrs) => {
if let Some(to_str) = attrs.get("__toString") {
let result =
crate::eval::apply(to_str.clone(), Value::Attrs(attrs.clone()))?;
let forced = crate::eval::force_value(&result)?;
let (s, c) = forced.coerce_to_string_impl(copy_to_store)?;
ctx.merge(&c);
s
} else if let Some(out_path) = attrs.get("outPath") {
let forced = crate::eval::force_value(out_path)?;
let (s, c) = forced.coerce_to_string_impl(copy_to_store)?;
ctx.merge(&c);
s
} else {
return Err(EvalError::TypeError(
"cannot coerce set to string (no __toString or outPath)".into(),
));
}
}
Value::List(items) => {
let mut parts = Vec::new();
for item in items.iter() {
let forced = crate::eval::force_value(item)?;
let (s, c) = forced.coerce_to_string_impl(copy_to_store)?;
ctx.merge(&c);
parts.push(s);
}
parts.join(" ")
}
Value::Thunk(_) => {
let forced = crate::eval::force_value(self)?;
let (s, c) = forced.coerce_to_string_impl(copy_to_store)?;
ctx.merge(&c);
s
}
other => {
return Err(EvalError::TypeError(format!(
"cannot coerce {} to string",
other.type_name()
)));
}
};
Ok((s, ctx))
}
}
impl From<&serde_json::Value> for Value {
fn from(json: &serde_json::Value) -> Self {
match json {
serde_json::Value::Null => Value::Null,
serde_json::Value::Bool(b) => Value::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
Value::Int(i)
} else {
Value::Float(n.as_f64().unwrap_or(0.0))
}
}
serde_json::Value::String(s) => Value::string(s.clone()),
serde_json::Value::Array(arr) => {
Value::List(Rc::new(NixList::new(arr.iter().map(Value::from).collect())))
}
serde_json::Value::Object(obj) => {
let mut attrs = NixAttrs::new();
for (k, v) in obj {
attrs.insert(k.clone(), Value::from(v));
}
Value::Attrs(Rc::new(attrs))
}
}
}
}
impl From<&toml::Value> for Value {
fn from(v: &toml::Value) -> Self {
match v {
toml::Value::String(s) => Value::string(s.clone()),
toml::Value::Integer(n) => Value::Int(*n),
toml::Value::Float(f) => Value::Float(*f),
toml::Value::Boolean(b) => Value::Bool(*b),
toml::Value::Array(arr) => {
Value::List(Rc::new(NixList::new(arr.iter().map(Value::from).collect())))
}
toml::Value::Table(t) => {
let mut attrs = NixAttrs::new();
for (k, val) in t {
attrs.insert(k.clone(), Value::from(val));
}
Value::Attrs(Rc::new(attrs))
}
toml::Value::Datetime(dt) => Value::string(dt.to_string()),
}
}
}
impl From<bool> for Value {
fn from(b: bool) -> Self {
Value::Bool(b)
}
}
impl From<i64> for Value {
fn from(n: i64) -> Self {
Value::Int(n)
}
}
impl From<f64> for Value {
fn from(f: f64) -> Self {
Value::Float(f)
}
}
impl From<NixString> for Value {
fn from(s: NixString) -> Self {
Value::String(Rc::new(s))
}
}
impl From<NixAttrs> for Value {
fn from(attrs: NixAttrs) -> Self {
Value::Attrs(Rc::new(attrs))
}
}
impl From<Vec<Value>> for Value {
fn from(list: Vec<Value>) -> Self {
Value::List(Rc::new(NixList::new(list)))
}
}
impl PartialEq for Value {
fn eq(&self, other: &Self) -> bool {
if let (Value::Thunk(a), Value::Thunk(b)) = (self, other) {
if Rc::ptr_eq(&a.0, &b.0) { return true; }
}
let l = self.demand().unwrap_or(Concrete::Null);
let r = other.demand().unwrap_or(Concrete::Null);
l == r
}
}
impl fmt::Display for Value {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Value::Null => write!(f, "null"),
Value::Bool(b) => write!(f, "{b}"),
Value::Int(n) => write!(f, "{n}"),
Value::Float(n) => write!(f, "{}", sui_compat::versions::cppnix_format_float(*n)),
Value::String(s) => write!(f, "\"{}\"", s.chars.replace('\\', "\\\\").replace('"', "\\\"")),
Value::Path(p) => write!(f, "{p}"),
Value::List(items) => {
write!(f, "[ ")?;
for item in items.iter() {
write!(f, "{item} ")?;
}
write!(f, "]")
}
Value::Attrs(attrs) => {
write!(f, "{{ ")?;
for (k, v) in attrs.iter() {
write!(f, "{k} = {v}; ")?;
}
write!(f, "}}")
}
Value::Lambda(_) => write!(f, "<<lambda>>"),
Value::Builtin(b) => write!(f, "<<builtin {}>>" , b.name),
Value::Thunk(thunk) => {
match thunk.force(&|e, env| crate::eval::eval_expr(e, env)) {
Ok(v) => write!(f, "{v}"),
Err(_) => write!(f, "<<thunk:error>>"),
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::rc::Rc;
#[test]
#[ignore = "measurement, not a gate: run with --ignored --nocapture"]
fn measure_hamt_vs_flat_attrset_cost() {
use crate::value::census::rss_bytes;
const N: usize = 300_000;
const ENTRIES: usize = 4;
let syms: Vec<Symbol> = (0..ENTRIES).map(|i| intern(&format!("k{i}"))).collect();
let base = rss_bytes();
let mut hamts: Vec<FxHashMap<Symbol, Value>> = Vec::with_capacity(N);
for _ in 0..N {
let mut m = FxHashMap::default();
for s in &syms { m.insert(*s, Value::Int(1)); }
hamts.push(m);
}
let after_hamt = rss_bytes();
let mut flats: Vec<std::collections::HashMap<Symbol, Value>> = Vec::with_capacity(N);
for _ in 0..N {
let mut m = std::collections::HashMap::with_capacity(ENTRIES);
for s in &syms { m.insert(*s, Value::Int(1)); }
flats.push(m);
}
let after_flat = rss_bytes();
let hamt_cost = after_hamt.saturating_sub(base);
let flat_cost = after_flat.saturating_sub(after_hamt);
eprintln!("N={N} entries={ENTRIES}");
eprintln!(" im_rc HAMT : {} B total, {} B/map", hamt_cost, hamt_cost / N as u64);
eprintln!(" std flat : {} B total, {} B/map", flat_cost, flat_cost / N as u64);
if flat_cost > 0 {
eprintln!(" ratio : {:.2}x", hamt_cost as f64 / flat_cost as f64);
}
std::hint::black_box((&hamts, &flats));
}
#[test]
fn value_is_16_bytes() {
assert_eq!(std::mem::size_of::<Value>(), 16);
}
#[test]
fn overlay_carries_attr_positions_from_both_sides() {
let tbl = |file: &str, key: &str, off: u32| {
let mut t = crate::pos::AttrPositions::new(Some(std::path::PathBuf::from(file)));
t.insert(intern(key), off);
Rc::new(t)
};
let mk = |file: &str, key: &str, off: u32| {
let mut a = NixAttrs::new();
a.insert(key.to_string(), Value::Int(1));
a.set_positions(tbl(file, key, off));
a
};
let left_only = mk("/l.nix", "modules", 11).overlay(mk("/r.nix", "other", 22));
assert_eq!(
left_only.pos_entry(intern("modules")),
Some((Some(std::path::PathBuf::from("/l.nix")), 11)),
);
let both = mk("/l.nix", "modules", 11).overlay(mk("/r.nix", "modules", 22));
assert_eq!(
both.pos_entry(intern("modules")),
Some((Some(std::path::PathBuf::from("/r.nix")), 22)),
);
assert_eq!(both.pos_entry(intern("nope")), None);
}
#[test]
fn to_json_null() {
assert_eq!(Value::Null.to_json(), serde_json::Value::Null);
}
#[test]
fn to_json_bool() {
assert_eq!(Value::Bool(true).to_json(), serde_json::Value::Bool(true));
assert_eq!(Value::Bool(false).to_json(), serde_json::Value::Bool(false));
}
#[test]
fn to_json_int() {
assert_eq!(Value::Int(42).to_json(), serde_json::json!(42));
}
#[test]
fn to_json_float() {
assert_eq!(Value::Float(3.14).to_json(), serde_json::json!(3.14));
}
#[test]
fn to_json_string() {
assert_eq!(
Value::string("hello").to_json(),
serde_json::Value::String("hello".to_string()),
);
}
#[test]
fn to_json_path() {
assert_eq!(
Value::Path(Box::new(SmolStr::from("/nix/store"))).to_json(),
serde_json::Value::String("/nix/store".to_string()),
);
}
#[test]
fn to_json_list() {
let v = Value::list(vec![Value::Int(1), Value::Bool(true)]);
assert_eq!(v.to_json(), serde_json::json!([1, true]));
}
#[test]
fn to_json_attrs() {
let mut attrs = NixAttrs::new();
attrs.insert("a".to_string(), Value::Int(1));
let v = Value::Attrs(Rc::new(attrs));
assert_eq!(v.to_json(), serde_json::json!({"a": 1}));
}
fn mk_drv_attrs(out_path: &str, extra_key: &str, extra_val: i64) -> Value {
let mut a = NixAttrs::new();
a.insert("type".to_string(), Value::string("derivation"));
a.insert("outPath".to_string(), Value::string(out_path));
a.insert(extra_key.to_string(), Value::Int(extra_val));
Value::Attrs(Rc::new(a))
}
#[test]
fn derivations_same_outpath_differing_attrs_are_equal() {
let a = mk_drv_attrs("/nix/store/x-foo", "foo", 1);
let b = mk_drv_attrs("/nix/store/x-foo", "bar", 2);
assert!(a == b, "same-outPath derivations must compare equal");
assert!(!(a != b));
}
#[test]
fn derivations_differing_outpath_are_unequal() {
let a = mk_drv_attrs("/nix/store/x-foo", "foo", 1);
let b = mk_drv_attrs("/nix/store/y-foo", "foo", 1);
assert!(a != b, "different-outPath derivations must compare unequal");
}
#[test]
fn non_derivation_attrs_with_outpath_use_structural_eq() {
let mut a = NixAttrs::new();
a.insert("outPath".to_string(), Value::string("/nix/store/x"));
a.insert("foo".to_string(), Value::Int(1));
let mut b = NixAttrs::new();
b.insert("outPath".to_string(), Value::string("/nix/store/x"));
b.insert("foo".to_string(), Value::Int(2));
assert!(
Value::Attrs(Rc::new(a)) != Value::Attrs(Rc::new(b)),
"non-derivation attrs with equal outPath but differing foo must be unequal",
);
}
#[test]
fn attrs_eq_borrow_result_matches_multi_key() {
let mk = || {
let mut inner = NixAttrs::new();
inner.insert("n".to_string(), Value::Int(7));
let mut a = NixAttrs::new();
a.insert("a".to_string(), Value::Int(1));
a.insert("b".to_string(), Value::string("two"));
a.insert("c".to_string(), Value::Attrs(Rc::new(inner)));
Value::Attrs(Rc::new(a))
};
assert!(mk() == mk(), "equal multi-key attrsets must compare equal (borrow path)");
let mut b = NixAttrs::new();
b.insert("a".to_string(), Value::Int(1));
b.insert("b".to_string(), Value::string("TWO"));
let mut a2 = NixAttrs::new();
a2.insert("a".to_string(), Value::Int(1));
a2.insert("b".to_string(), Value::string("two"));
assert!(
Value::Attrs(Rc::new(a2)) != Value::Attrs(Rc::new(b)),
"attrsets differing in one value must be unequal (borrow path)",
);
let mut a3 = NixAttrs::new();
a3.insert("a".to_string(), Value::Int(1));
let mut b3 = NixAttrs::new();
b3.insert("a".to_string(), Value::Int(1));
b3.insert("extra".to_string(), Value::Int(9));
assert!(
Value::Attrs(Rc::new(a3)) != Value::Attrs(Rc::new(b3)),
"attrsets differing in key set must be unequal (borrow path)",
);
}
#[test]
fn attrs_eq_borrow_does_not_force_or_throw_on_shared_thunk() {
let boom = Value::Thunk(Thunk::new_native(|| {
Err(EvalError::Throw("kaboom".to_string()))
}));
let mut a = NixAttrs::new();
a.insert("x".to_string(), Value::Int(1));
a.insert("t".to_string(), boom.clone()); let mut b = NixAttrs::new();
b.insert("x".to_string(), Value::Int(2)); b.insert("t".to_string(), boom);
let va = Value::Attrs(Rc::new(a));
let vb = Value::Attrs(Rc::new(b));
assert!(va != vb, "differ on x → unequal, throwing thunk must not abort eq");
}
#[test]
fn attrs_eq_borrow_overlay_still_compares() {
let mut base = NixAttrs::new();
base.insert("a".to_string(), Value::Int(1));
let mut over = NixAttrs::new();
over.insert("b".to_string(), Value::Int(2));
let merged = base.overlay(over);
let mut flat = NixAttrs::new();
flat.insert("a".to_string(), Value::Int(1));
flat.insert("b".to_string(), Value::Int(2));
assert!(
Value::Attrs(Rc::new(merged)) == Value::Attrs(Rc::new(flat)),
"overlay and equivalent flat attrset must compare equal (borrow path)",
);
}
#[test]
fn to_json_lambda() {
let root = rnix::Root::parse("x: x");
let expr = root.tree().expr().unwrap();
let lambda = match expr {
rnix::ast::Expr::Lambda(l) => l,
_ => panic!("expected lambda"),
};
let closure = Closure {
param: lambda.param().unwrap(),
body: lambda.body().unwrap(),
env: Env::new(),
};
assert_eq!(
Value::Lambda(Rc::new(closure)).to_json(),
serde_json::Value::String("<lambda>".to_string()),
);
}
#[test]
fn to_json_builtin() {
let b = BuiltinFn {
name: "test",
func: Rc::new(|_| Ok(Value::Null)),
};
assert_eq!(
Value::Builtin(Box::new(b)).to_json(),
serde_json::Value::String("<builtin test>".to_string()),
);
}
#[test]
fn type_name_null() { assert_eq!(Value::Null.type_name(), "null"); }
#[test]
fn type_name_bool() { assert_eq!(Value::Bool(false).type_name(), "bool"); }
#[test]
fn type_name_int() { assert_eq!(Value::Int(0).type_name(), "int"); }
#[test]
fn type_name_float() { assert_eq!(Value::Float(0.0).type_name(), "float"); }
#[test]
fn type_name_string() { assert_eq!(Value::string("").type_name(), "string"); }
#[test]
fn type_name_path() { assert_eq!(Value::Path(Box::new(SmolStr::from(""))).type_name(), "path"); }
#[test]
fn type_name_list() { assert_eq!(Value::list(vec![]).type_name(), "list"); }
#[test]
fn type_name_set() { assert_eq!(Value::Attrs(Rc::new(NixAttrs::new())).type_name(), "set"); }
#[test]
fn type_name_lambda() {
let root = rnix::Root::parse("x: x");
let expr = root.tree().expr().unwrap();
let lambda = match expr {
rnix::ast::Expr::Lambda(l) => l,
_ => panic!("expected lambda"),
};
let closure = Closure {
param: lambda.param().unwrap(),
body: lambda.body().unwrap(),
env: Env::new(),
};
assert_eq!(Value::Lambda(Rc::new(closure)).type_name(), "lambda");
}
#[test]
fn type_name_builtin() {
let b = BuiltinFn {
name: "t",
func: Rc::new(|_| Ok(Value::Null)),
};
assert_eq!(Value::Builtin(Box::new(b)).type_name(), "lambda");
}
#[test]
fn as_bool_error_on_non_bool() {
assert!(Value::Int(1).as_bool().is_err());
assert!(Value::string("true").as_bool().is_err());
}
#[test]
fn as_int_error_on_non_int() {
assert!(Value::Bool(true).as_int().is_err());
assert!(Value::Float(1.0).as_int().is_err());
}
#[test]
fn as_string_error_on_non_string() {
assert!(Value::Int(42).as_string().is_err());
assert!(Value::Null.as_string().is_err());
}
#[test]
fn as_attrs_error_on_non_attrs() {
assert!(Value::Int(1).as_attrs().is_err());
assert!(Value::list(vec![]).as_attrs().is_err());
}
#[test]
fn as_list_error_on_non_list() {
assert!(Value::Int(1).as_list().is_err());
assert!(Value::Attrs(Rc::new(NixAttrs::new())).as_list().is_err());
}
#[test]
fn concat_lists_uniquely_owned_reuses_and_is_correct() {
let left = Value::list(vec![Value::Int(1), Value::Int(2)]);
assert!(left.is_uniquely_owned_list());
let right = [Value::Int(3), Value::Int(4)];
let out = super::concat_lists(left, &right).unwrap();
assert_eq!(
out.as_list().unwrap(),
&[Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]
);
}
#[test]
fn concat_lists_shared_left_is_left_untouched_and_correct() {
let shared = Rc::new(NixList::new(vec![Value::Int(1), Value::Int(2)]));
let left = Value::List(Rc::clone(&shared));
assert!(!left.is_uniquely_owned_list());
let right = [Value::Int(3)];
let out = super::concat_lists(left, &right).unwrap();
assert_eq!(
out.as_list().unwrap(),
&[Value::Int(1), Value::Int(2), Value::Int(3)]
);
assert_eq!(&*shared, &[Value::Int(1), Value::Int(2)]);
}
#[test]
fn concat_lists_empty_operands() {
let out = super::concat_lists(Value::list(vec![]), &[]).unwrap();
assert!(out.as_list().unwrap().is_empty());
let out2 = super::concat_lists(Value::list(vec![Value::Int(9)]), &[]).unwrap();
assert_eq!(out2.as_list().unwrap(), &[Value::Int(9)]);
let out3 = super::concat_lists(Value::list(vec![]), &[Value::Int(9)]).unwrap();
assert_eq!(out3.as_list().unwrap(), &[Value::Int(9)]);
}
#[test]
fn concat_lists_non_list_left_errors() {
assert!(super::concat_lists(Value::Int(1), &[]).is_err());
}
#[test]
fn concat_lists_preserves_element_identity() {
let inner = Rc::new(NixString::plain("x"));
let a = Value::String(Rc::clone(&inner));
let left = Value::list(vec![a]);
let out = super::concat_lists(left, &[]).unwrap();
if let Value::String(rc) = &out.as_list().unwrap()[0] {
assert!(Rc::ptr_eq(rc, &inner), "element Rc identity preserved");
} else {
panic!("expected string element");
}
}
#[test]
fn to_float_coerces_int() {
assert_eq!(Value::Int(5).to_float().unwrap(), 5.0);
assert_eq!(Value::Float(2.5).to_float().unwrap(), 2.5);
assert!(Value::string("x").to_float().is_err());
}
#[test]
fn partial_eq_int_float_cross() {
assert_eq!(Value::Int(3), Value::Float(3.0));
assert_eq!(Value::Float(3.0), Value::Int(3));
assert_ne!(Value::Int(3), Value::Float(3.5));
}
#[test]
fn partial_eq_different_types_not_equal() {
assert_ne!(Value::Int(1), Value::string("1"));
assert_ne!(Value::Bool(true), Value::Int(1));
assert_ne!(Value::Null, Value::Bool(false));
assert_ne!(Value::list(vec![]), Value::Attrs(Rc::new(NixAttrs::new())));
}
#[test]
fn display_null() { assert_eq!(format!("{}", Value::Null), "null"); }
#[test]
fn display_bool() {
assert_eq!(format!("{}", Value::Bool(true)), "true");
assert_eq!(format!("{}", Value::Bool(false)), "false");
}
#[test]
fn display_int() { assert_eq!(format!("{}", Value::Int(42)), "42"); }
#[test]
fn display_float() {
let s = format!("{}", Value::Float(3.14));
assert!(s.contains("3.14"));
}
#[test]
fn display_string() {
assert_eq!(format!("{}", Value::string("hi")), "\"hi\"");
}
#[test]
fn display_string_with_escapes() {
let v = Value::string("a\"b\\c");
let s = format!("{v}");
assert!(s.contains("\\\""));
assert!(s.contains("\\\\"));
}
#[test]
fn display_path() {
assert_eq!(format!("{}", Value::Path(Box::new(SmolStr::from("/foo")))), "/foo");
}
#[test]
fn display_list() {
let v = Value::list(vec![Value::Int(1), Value::Int(2)]);
assert_eq!(format!("{v}"), "[ 1 2 ]");
}
#[test]
fn display_attrs() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(1));
let v = Value::Attrs(Rc::new(attrs));
assert_eq!(format!("{v}"), "{ x = 1; }");
}
#[test]
fn display_lambda() {
let root = rnix::Root::parse("x: x");
let expr = root.tree().expr().unwrap();
let lambda = match expr {
rnix::ast::Expr::Lambda(l) => l,
_ => panic!("expected lambda"),
};
let closure = Closure {
param: lambda.param().unwrap(),
body: lambda.body().unwrap(),
env: Env::new(),
};
assert_eq!(format!("{}", Value::Lambda(Rc::new(closure))), "<<lambda>>");
}
#[test]
fn display_builtin() {
let b = BuiltinFn {
name: "add",
func: Rc::new(|_| Ok(Value::Null)),
};
assert_eq!(format!("{}", Value::Builtin(Box::new(b))), "<<builtin add>>");
}
#[test]
fn nixattrs_update_merging() {
let mut a = NixAttrs::new();
a.insert("x".to_string(), Value::Int(1));
a.insert("y".to_string(), Value::Int(2));
let mut b = NixAttrs::new();
b.insert("y".to_string(), Value::Int(99));
b.insert("z".to_string(), Value::Int(3));
let merged = a.update(&b);
assert_eq!(merged.get("x"), Some(&Value::Int(1)));
assert_eq!(merged.get("y"), Some(&Value::Int(99)));
assert_eq!(merged.get("z"), Some(&Value::Int(3)));
assert_eq!(merged.len(), 3);
}
#[test]
fn nixattrs_contains_key() {
let mut a = NixAttrs::new();
a.insert("foo".to_string(), Value::Null);
assert!(a.contains_key("foo"));
assert!(!a.contains_key("bar"));
}
#[test]
fn env_lookup_through_parent_chain() {
let mut root = Env::new();
root.bind("a".to_string(), Value::Int(1));
let mut child = root.child();
child.bind("b".to_string(), Value::Int(2));
let grandchild = child.child();
assert_eq!(grandchild.lookup("a"), Some(Value::Int(1)));
assert_eq!(grandchild.lookup("b"), Some(Value::Int(2)));
assert_eq!(grandchild.lookup("c"), None);
}
#[test]
fn env_with_scope_lookup() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(42));
let env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
assert_eq!(env.lookup("x"), Some(Value::Int(42)));
assert_eq!(env.lookup("y"), None);
}
#[test]
fn env_local_shadows_with_scope() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(1));
let mut env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
env.bind("x".to_string(), Value::Int(99));
assert_eq!(env.lookup("x"), Some(Value::Int(99)));
}
#[test]
fn string_context_merge_combines_elements() {
let mut ctx_a = StringContext::new();
ctx_a.add_plain("/nix/store/aaa".to_string());
let mut ctx_b = StringContext::new();
ctx_b.add_plain("/nix/store/bbb".to_string());
ctx_a.merge(&ctx_b);
assert_eq!(ctx_a.len(), 2);
assert!(ctx_a.elements().contains(&ContextElement::Plain(SmolStr::from("/nix/store/aaa"))));
assert!(ctx_a.elements().contains(&ContextElement::Plain(SmolStr::from("/nix/store/bbb"))));
}
#[test]
fn string_context_merge_deduplicates() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/same".to_string());
ctx.add_plain("/nix/store/same".to_string());
assert_eq!(ctx.len(), 1);
}
#[test]
fn string_context_mixed_element_types() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/foo".to_string());
ctx.add_output("/nix/store/bar.drv".to_string(), "out".to_string());
ctx.add_drv_deep("/nix/store/baz.drv".to_string());
assert_eq!(ctx.len(), 3);
assert!(!ctx.is_empty());
}
#[test]
fn string_context_new_is_empty() {
let ctx = StringContext::new();
assert!(ctx.is_empty());
assert_eq!(ctx.len(), 0);
}
#[test]
fn string_context_merge_zero_elements() {
let mut ctx_a = StringContext::new();
let ctx_b = StringContext::new();
ctx_a.merge(&ctx_b);
assert!(ctx_a.is_empty());
}
#[test]
fn string_context_merge_one_element() {
let mut ctx = StringContext::new();
let mut other = StringContext::new();
other.add_plain("/nix/store/only".to_string());
ctx.merge(&other);
assert_eq!(ctx.len(), 1);
assert!(ctx.elements().contains(&ContextElement::Plain(SmolStr::from("/nix/store/only"))));
}
#[test]
fn string_context_merge_two_elements() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/a".to_string());
let mut other = StringContext::new();
other.add_plain("/nix/store/b".to_string());
ctx.merge(&other);
assert_eq!(ctx.len(), 2);
}
#[test]
fn string_context_merge_five_elements() {
let mut ctx = StringContext::new();
for i in 0..5 {
ctx.add_plain(format!("/nix/store/path-{i}"));
}
assert_eq!(ctx.len(), 5);
for i in 0..5 {
assert!(ctx.elements().contains(&ContextElement::Plain(SmolStr::from(format!("/nix/store/path-{i}").as_str()))));
}
}
#[test]
fn string_context_insert_deduplicates() {
let mut ctx = StringContext::new();
ctx.insert(ContextElement::Plain(SmolStr::from("/nix/store/dup")));
ctx.insert(ContextElement::Plain(SmolStr::from("/nix/store/dup")));
ctx.insert(ContextElement::Output { drv: SmolStr::from("/nix/store/x.drv"), output: SmolStr::from("out") });
ctx.insert(ContextElement::Output { drv: SmolStr::from("/nix/store/x.drv"), output: SmolStr::from("out") });
assert_eq!(ctx.len(), 2);
}
#[test]
fn nix_string_plain_has_no_context() {
let s = NixString::plain("hello");
assert!(!s.has_context());
assert_eq!(s.as_str(), "hello");
}
#[test]
fn nix_string_with_context_reports_context() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/xyz".to_string());
let s = NixString::with_context("hello", ctx);
assert!(s.has_context());
assert_eq!(s.as_str(), "hello");
}
#[test]
fn nix_string_display_shows_chars_only() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/abc".to_string());
let s = NixString::with_context("visible", ctx);
assert_eq!(format!("{s}"), "visible");
}
#[test]
fn nix_string_struct_eq_includes_context() {
let plain = NixString::plain("hello");
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/xxx".to_string());
let with_ctx = NixString::with_context("hello", ctx);
assert_ne!(plain, with_ctx);
}
#[test]
fn value_string_eq_ignores_context() {
let plain = Value::String(Rc::new(NixString::plain("hello")));
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/xxx".to_string());
let with_ctx = Value::String(Rc::new(NixString::with_context("hello", ctx)));
assert_eq!(plain, with_ctx);
}
#[test]
fn env_nested_with_inner_wins() {
let mut outer_attrs = NixAttrs::new();
outer_attrs.insert("x".to_string(), Value::Int(1));
let outer = Env::new().with_scope(Value::Attrs(Rc::new(outer_attrs)));
let mut inner_attrs = NixAttrs::new();
inner_attrs.insert("x".to_string(), Value::Int(2));
let inner = outer.child().with_scope(Value::Attrs(Rc::new(inner_attrs)));
assert_eq!(inner.lookup("x"), Some(Value::Int(2)));
}
#[test]
fn env_nested_with_fallback_to_outer() {
let mut outer_attrs = NixAttrs::new();
outer_attrs.insert("x".to_string(), Value::Int(1));
let outer = Env::new().with_scope(Value::Attrs(Rc::new(outer_attrs)));
let mut inner_attrs = NixAttrs::new();
inner_attrs.insert("y".to_string(), Value::Int(2));
let inner = outer.child().with_scope(Value::Attrs(Rc::new(inner_attrs)));
assert_eq!(inner.lookup("x"), Some(Value::Int(1)));
assert_eq!(inner.lookup("y"), Some(Value::Int(2)));
}
#[test]
fn env_lexical_binding_wins_over_all_with_scopes() {
let mut outer_attrs = NixAttrs::new();
outer_attrs.insert("x".to_string(), Value::Int(1));
let outer = Env::new().with_scope(Value::Attrs(Rc::new(outer_attrs)));
let mut inner_attrs = NixAttrs::new();
inner_attrs.insert("x".to_string(), Value::Int(2));
let mut inner = outer.child().with_scope(Value::Attrs(Rc::new(inner_attrs)));
inner.bind("x".to_string(), Value::Int(99));
assert_eq!(inner.lookup("x"), Some(Value::Int(99)));
}
#[test]
fn env_parent_lexical_wins_over_child_with_scope() {
let mut root = Env::new();
root.bind("x".to_string(), Value::Int(10));
let mut child_attrs = NixAttrs::new();
child_attrs.insert("x".to_string(), Value::Int(20));
let child = root.child().with_scope(Value::Attrs(Rc::new(child_attrs)));
assert_eq!(child.lookup("x"), Some(Value::Int(10)));
}
#[test]
fn env_deeply_nested_with_scopes_three_levels() {
let mut a = NixAttrs::new();
a.insert("x".to_string(), Value::Int(1));
let env1 = Env::new().with_scope(Value::Attrs(Rc::new(a)));
let mut b = NixAttrs::new();
b.insert("y".to_string(), Value::Int(2));
let env2 = env1.child().with_scope(Value::Attrs(Rc::new(b)));
let mut c = NixAttrs::new();
c.insert("z".to_string(), Value::Int(3));
let env3 = env2.child().with_scope(Value::Attrs(Rc::new(c)));
assert_eq!(env3.lookup("x"), Some(Value::Int(1)));
assert_eq!(env3.lookup("y"), Some(Value::Int(2)));
assert_eq!(env3.lookup("z"), Some(Value::Int(3)));
assert_eq!(env3.lookup("w"), None);
}
#[test]
fn env_with_scope_does_not_pollute_bindings() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(42));
let env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
assert!(env.0.bindings.get(&intern("x")).is_none());
assert_eq!(env.lookup("x"), Some(Value::Int(42)));
}
#[test]
fn env_lexical_binding_not_in_with_scopes() {
let mut env = Env::new();
env.bind("x".to_string(), Value::Int(42));
assert!(env.0.with_scopes.is_empty());
assert_eq!(env.lookup("x"), Some(Value::Int(42)));
}
#[test]
fn env_child_inherits_eval_file() {
let mut env = Env::new();
env.set_eval_file(Some(std::path::PathBuf::from("/foo/bar.nix")));
let child = env.child();
assert_eq!(child.eval_file().cloned(), Some(std::path::PathBuf::from("/foo/bar.nix")));
}
#[test]
fn env_new_has_no_parent_no_with() {
let env = Env::new();
assert_eq!(env.lookup("anything"), None);
assert!(env.eval_file().is_none());
}
#[test]
fn thunk_new_suspended_is_not_evaluated() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
assert!(!thunk.is_evaluated());
}
#[test]
fn thunk_new_evaluated_is_evaluated() {
let thunk = Thunk::new_evaluated(Value::Int(42));
assert!(thunk.is_evaluated());
}
#[test]
fn thunk_force_evaluates_suspended() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let result = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert!(result.is_ok());
assert_eq!(result.unwrap(), Value::Int(42));
assert!(thunk.is_evaluated());
}
#[test]
fn thunk_force_memoizes_result() {
let root = rnix::Root::parse("1 + 2");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let r1 = thunk.force(&|e, env| crate::eval::eval_expr(e, env)).unwrap();
let r2 = thunk.force(&|e, env| crate::eval::eval_expr(e, env)).unwrap();
assert_eq!(r1, Value::Int(3));
assert_eq!(r2, Value::Int(3));
}
#[test]
fn thunk_force_already_evaluated_returns_value() {
let thunk = Thunk::new_evaluated(Value::Bool(true));
let result = thunk.force(&|_, _| panic!("should not be called"));
assert_eq!(result.unwrap(), Value::Bool(true));
}
#[test]
fn thunk_force_concrete_skips_redundant_store_but_caches() {
let root = rnix::Root::parse("1 + 2");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let r1 = thunk.force(&|e, env| crate::eval::eval_expr(e, env)).unwrap();
assert_eq!(r1, Value::Int(3));
assert!(thunk.is_evaluated());
assert_eq!(thunk.peek().map(|c| c.clone().into_value()), Some(Value::Int(3)));
let r2 = thunk.force(&|_, _| panic!("re-force must hit the cache, not re-eval")).unwrap();
assert_eq!(r2, Value::Int(3));
}
#[test]
fn thunk_blackhole_detects_infinite_recursion() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
*unsafe { &mut *thunk.0.repr.get() } = ThunkRepr::Blackhole;
let result = thunk.force(&|_, _| Ok(Value::Null));
assert!(result.is_err());
let err_msg = format!("{}", result.unwrap_err());
assert!(err_msg.contains("infinite recursion"));
}
#[test]
fn thunk_update_env_replaces_suspended_env() {
let root = rnix::Root::parse("x");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let mut new_env = Env::new();
new_env.bind("x".to_string(), Value::Int(99));
thunk.update_env(&new_env);
let result = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert_eq!(result.unwrap(), Value::Int(99));
}
#[test]
fn thunk_update_env_noop_when_evaluated() {
let thunk = Thunk::new_evaluated(Value::Int(1));
let mut new_env = Env::new();
new_env.bind("x".to_string(), Value::Int(99));
thunk.update_env(&new_env);
assert_eq!(
thunk.force(&|_, _| panic!("should not be called")).unwrap(),
Value::Int(1),
);
}
#[test]
fn thunk_debug_suspended() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
assert_eq!(format!("{thunk:?}"), "<thunk>");
}
#[test]
fn thunk_debug_evaluated() {
let thunk = Thunk::new_evaluated(Value::Int(42));
let dbg = format!("{thunk:?}");
assert!(dbg.contains("42"));
}
#[test]
fn thunk_error_restores_suspended_state() {
let root = rnix::Root::parse("nonexistent_var");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let result = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert!(result.is_err());
assert!(!thunk.is_evaluated());
let dbg = format!("{thunk:?}");
assert_eq!(dbg, "<thunk>");
}
#[test]
fn thunk_inherit_select_forces_and_selects() {
let root = rnix::Root::parse(r#"{ x = 42; }"#);
let expr = root.tree().expr().unwrap();
let source = Thunk::new_suspended(expr, Env::new());
let thunk = Thunk::new_inherit_select(source, "x".to_string());
let result = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert_eq!(result.unwrap(), Value::Int(42));
assert!(thunk.is_evaluated());
}
#[test]
fn thunk_inherit_select_missing_attr_errors() {
let root = rnix::Root::parse(r#"{ x = 42; }"#);
let expr = root.tree().expr().unwrap();
let source = Thunk::new_suspended(expr, Env::new());
let thunk = Thunk::new_inherit_select(source, "y".to_string());
let result = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert!(result.is_err());
assert!(!thunk.is_evaluated());
}
#[test]
fn thunk_inherit_select_non_attrs_source_errors() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let source = Thunk::new_suspended(expr, Env::new());
let thunk = Thunk::new_inherit_select(source, "x".to_string());
let result = thunk.force(&|e, env| crate::eval::eval_expr(e, env));
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("not a set"));
}
#[test]
fn thunk_inherit_select_shares_source_thunk() {
let root = rnix::Root::parse(r#"{ a = 1; b = 2; }"#);
let expr = root.tree().expr().unwrap();
let source = Thunk::new_suspended(expr, Env::new());
let thunk_a = Thunk::new_inherit_select(source.clone(), "a".to_string());
let thunk_b = Thunk::new_inherit_select(source.clone(), "b".to_string());
let result_a = thunk_a.force(&|e, env| crate::eval::eval_expr(e, env));
assert_eq!(result_a.unwrap(), Value::Int(1));
assert!(source.is_evaluated());
let result_b = thunk_b.force(&|e, env| crate::eval::eval_expr(e, env));
assert_eq!(result_b.unwrap(), Value::Int(2));
}
#[test]
fn nixattrs_empty_operations() {
let a = NixAttrs::new();
assert!(a.is_empty());
assert_eq!(a.len(), 0);
assert_eq!(a.get("x"), None);
assert!(!a.contains_key("x"));
assert_eq!(a.keys().count(), 0);
assert_eq!(a.iter().count(), 0);
}
#[test]
fn nixattrs_update_with_empty() {
let mut a = NixAttrs::new();
a.insert("x".to_string(), Value::Int(1));
let b = NixAttrs::new();
let merged = a.update(&b);
assert_eq!(merged.len(), 1);
assert_eq!(merged.get("x"), Some(&Value::Int(1)));
}
#[test]
fn nixattrs_update_empty_with_nonempty() {
let a = NixAttrs::new();
let mut b = NixAttrs::new();
b.insert("x".to_string(), Value::Int(1));
let merged = a.update(&b);
assert_eq!(merged.len(), 1);
assert_eq!(merged.get("x"), Some(&Value::Int(1)));
}
#[test]
fn nixattrs_keys_sorted_order() {
let mut a = NixAttrs::new();
a.insert("c".to_string(), Value::Int(3));
a.insert("a".to_string(), Value::Int(1));
a.insert("b".to_string(), Value::Int(2));
let keys: Vec<String> = a.keys().collect();
assert_eq!(keys, vec!["a", "b", "c"]);
}
#[test]
fn value_to_str_forces_thunks() {
let root = rnix::Root::parse(r#""hello""#);
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert_eq!(val.to_str().unwrap(), "hello");
}
#[test]
fn value_to_nix_string_forces_thunks() {
let root = rnix::Root::parse(r#""world""#);
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
let ns = val.to_nix_string().unwrap();
assert_eq!(ns.as_str(), "world");
assert!(!ns.has_context());
}
#[test]
fn value_to_attrs_forces_thunks() {
let root = rnix::Root::parse("{ x = 1; }");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
let attrs = val.to_attrs().unwrap();
assert_eq!(attrs.len(), 1);
}
#[test]
fn value_to_list_forces_thunks() {
let root = rnix::Root::parse("[1 2 3]");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
let list = val.to_list().unwrap();
assert_eq!(list.len(), 3);
}
#[test]
fn value_to_float_on_thunk() {
let root = rnix::Root::parse("3.14");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
let f = val.to_float().unwrap();
assert!((f - 3.14).abs() < f64::EPSILON);
}
#[test]
fn value_as_bool_on_thunk() {
let root = rnix::Root::parse("true");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert!(val.as_bool().unwrap());
}
#[test]
fn value_as_int_on_thunk() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert_eq!(val.as_int().unwrap(), 42);
}
#[test]
fn value_string_constructor() {
let v = Value::string("test");
assert_eq!(v, Value::String(Rc::new(NixString::plain("test"))));
}
#[test]
fn value_partial_eq_null_null() {
assert_eq!(Value::Null, Value::Null);
}
#[test]
fn value_partial_eq_lists_deep() {
let a = Value::list(vec![Value::Int(1), Value::list(vec![Value::Int(2)])]);
let b = Value::list(vec![Value::Int(1), Value::list(vec![Value::Int(2)])]);
assert_eq!(a, b);
}
#[test]
fn value_partial_eq_attrs_deep() {
let mut a = NixAttrs::new();
a.insert("x".to_string(), Value::Int(1));
let mut b = NixAttrs::new();
b.insert("x".to_string(), Value::Int(1));
assert_eq!(Value::Attrs(Rc::new(a)), Value::Attrs(Rc::new(b)));
}
#[test]
fn eval_error_type_error_constructor() {
let e = EvalError::type_error("oops");
assert!(matches!(e, EvalError::TypeError(ref s) if s == "oops"));
}
#[test]
fn eval_error_type_mismatch_constructor() {
let e = EvalError::type_mismatch("int", "string");
match e {
EvalError::TypeMismatch { expected, got } => {
assert_eq!(expected, "int");
assert_eq!(got, "string");
}
_ => panic!("expected TypeMismatch"),
}
}
#[test]
fn eval_error_is_throw_yes_no() {
assert!(EvalError::Throw("oops".into()).is_throw());
assert!(!EvalError::TypeError("oops".into()).is_throw());
assert!(!EvalError::AssertionFailed(String::new()).is_throw());
}
#[test]
fn eval_error_is_infinite_recursion_yes_no() {
assert!(EvalError::InfiniteRecursion("loop".into()).is_infinite_recursion());
assert!(!EvalError::DivisionByZero.is_infinite_recursion());
assert!(!EvalError::Throw("x".into()).is_infinite_recursion());
}
#[test]
fn eval_error_display_undefined_var() {
let s = format!("{}", EvalError::UndefinedVar("foo".into()));
assert!(s.contains("undefined variable"));
assert!(s.contains("foo"));
}
#[test]
fn eval_error_display_type_error() {
let s = format!("{}", EvalError::TypeError("bad".into()));
assert!(s.contains("type error"));
assert!(s.contains("bad"));
}
#[test]
fn eval_error_display_attr_not_found() {
let s = format!("{}", EvalError::AttrNotFound("x".into()));
assert!(s.contains("attribute not found"));
assert!(s.contains("x"));
}
#[test]
fn eval_error_display_type_mismatch() {
let s = format!(
"{}",
EvalError::TypeMismatch { expected: "int", got: "string" }
);
assert!(s.contains("expected int"));
assert!(s.contains("got string"));
}
#[test]
fn eval_error_display_assertion_failed() {
let s = format!("{}", EvalError::AssertionFailed(String::new()));
assert!(s.contains("assertion"));
}
#[test]
fn eval_error_display_division_by_zero() {
let s = format!("{}", EvalError::DivisionByZero);
assert!(s.contains("division by zero"));
}
#[test]
fn eval_error_display_infinite_recursion() {
let s = format!("{}", EvalError::InfiniteRecursion("loop".into()));
assert!(s.contains("infinite recursion"));
assert!(s.contains("loop"));
}
#[test]
fn eval_error_display_io_error() {
let s = format!(
"{}",
EvalError::IoError {
context: "ctx".into(),
message: "no such file".into(),
}
);
assert!(s.contains("I/O"));
assert!(s.contains("ctx"));
assert!(s.contains("no such file"));
}
#[test]
fn eval_error_display_throw() {
let s = format!("{}", EvalError::Throw("boom".into()));
assert_eq!(s, "boom");
}
#[test]
fn eval_error_display_not_implemented() {
let s = format!("{}", EvalError::NotImplemented("frob".into()));
assert!(s.contains("not yet implemented"));
assert!(s.contains("frob"));
}
#[test]
fn eval_error_display_parse_error() {
let s = format!("{}", EvalError::ParseError("syntax".into()));
assert!(s.contains("parse error"));
assert!(s.contains("syntax"));
}
#[test]
fn eval_error_display_recursion_limit() {
let s = format!(
"{}",
EvalError::RecursionLimit("max depth exceeded".into())
);
assert!(s.contains("recursion limit"));
assert!(s.contains("max depth exceeded"));
}
#[test]
fn eval_error_partial_eq_same_variant() {
assert_eq!(
EvalError::UndefinedVar("x".into()),
EvalError::UndefinedVar("x".into()),
);
assert_ne!(
EvalError::UndefinedVar("x".into()),
EvalError::UndefinedVar("y".into()),
);
assert_ne!(
EvalError::UndefinedVar("x".into()),
EvalError::AttrNotFound("x".into()),
);
}
#[test]
fn context_element_display_plain() {
let e = ContextElement::Plain("/nix/store/xyz".into());
assert_eq!(format!("{e}"), "/nix/store/xyz");
}
#[test]
fn context_element_display_output() {
let e = ContextElement::Output {
drv: "/nix/store/abc.drv".into(),
output: "out".into(),
};
assert_eq!(format!("{e}"), "/nix/store/abc.drv!out");
}
#[test]
fn context_element_display_drv_deep() {
let e = ContextElement::DrvDeep("/nix/store/abc.drv".into());
assert_eq!(format!("{e}"), "=/nix/store/abc.drv");
}
#[test]
fn string_context_iter_yields_all() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/aaa");
ctx.add_plain("/nix/store/bbb");
let count = ctx.iter().count();
assert_eq!(count, 2);
}
#[test]
fn string_context_len_matches_set_size() {
let mut ctx = StringContext::new();
assert_eq!(ctx.len(), 0);
ctx.add_plain("/nix/store/x");
assert_eq!(ctx.len(), 1);
ctx.add_output("/nix/store/y.drv", "out");
assert_eq!(ctx.len(), 2);
}
#[test]
fn string_context_insert_raw_element() {
let mut ctx = StringContext::new();
ctx.insert(ContextElement::Plain("/nix/store/foo".into()));
assert_eq!(ctx.len(), 1);
}
#[test]
fn string_context_default_is_empty() {
let ctx = StringContext::default();
assert!(ctx.is_empty());
}
#[test]
fn nix_string_as_ref_str() {
let s = NixString::plain("hello");
let r: &str = s.as_ref();
assert_eq!(r, "hello");
}
#[test]
fn nix_string_deref_to_str_methods() {
let s = NixString::plain("Hello World");
assert_eq!(s.len(), 11);
assert!(s.starts_with("Hello"));
assert_eq!(s.to_uppercase(), "HELLO WORLD");
}
#[test]
fn nixattrs_remove_returns_value() {
let mut a = NixAttrs::new();
a.insert("x".into(), Value::Int(1));
let removed = a.remove("x");
assert_eq!(removed, Some(Value::Int(1)));
assert!(!a.contains_key("x"));
assert_eq!(a.remove("y"), None);
}
#[test]
fn nixattrs_values_iter() {
let mut a = NixAttrs::new();
a.insert("a".into(), Value::Int(1));
a.insert("b".into(), Value::Int(2));
let mut vs: Vec<&Value> = a.values().collect();
vs.sort_by_key(|v| match v {
Value::Int(n) => *n,
_ => 0,
});
assert_eq!(vs, vec![&Value::Int(1), &Value::Int(2)]);
}
#[test]
fn nixattrs_iter_returns_sorted_pairs() {
let mut a = NixAttrs::new();
a.insert("zeta".into(), Value::Int(3));
a.insert("alpha".into(), Value::Int(1));
a.insert("mu".into(), Value::Int(2));
let pairs: Vec<(String, &Value)> = a.iter().collect();
assert_eq!(pairs[0].0, "alpha");
assert_eq!(pairs[1].0, "mu");
assert_eq!(pairs[2].0, "zeta");
}
#[test]
fn nixattrs_from_iterator() {
let pairs = vec![
("a".to_string(), Value::Int(1)),
("b".to_string(), Value::Int(2)),
];
let attrs: NixAttrs = pairs.into_iter().collect();
assert_eq!(attrs.len(), 2);
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
}
#[test]
fn nixattrs_into_iterator_yields_owned() {
let mut a = NixAttrs::new();
a.insert("x".into(), Value::Int(42));
let pairs: Vec<(String, Value)> = a.into_iter().collect();
assert_eq!(pairs.len(), 1);
assert_eq!(pairs[0].0, "x");
assert_eq!(pairs[0].1, Value::Int(42));
}
#[test]
fn nixattrs_default_is_empty() {
let a = NixAttrs::default();
assert!(a.is_empty());
}
#[test]
fn value_from_bool() {
assert_eq!(Value::from(true), Value::Bool(true));
assert_eq!(Value::from(false), Value::Bool(false));
}
#[test]
fn value_from_i64() {
assert_eq!(Value::from(42_i64), Value::Int(42));
assert_eq!(Value::from(-1_i64), Value::Int(-1));
}
#[test]
fn value_from_f64() {
assert_eq!(Value::from(2.5_f64), Value::Float(2.5));
}
#[test]
fn value_from_nix_string() {
let v: Value = NixString::plain("hi").into();
assert_eq!(v, Value::string("hi"));
}
#[test]
fn value_from_nix_attrs() {
let mut a = NixAttrs::new();
a.insert("x".into(), Value::Int(1));
let v: Value = a.into();
match v {
Value::Attrs(_) => {}
_ => panic!("expected Attrs"),
}
}
#[test]
fn value_from_vec() {
let v: Value = vec![Value::Int(1), Value::Int(2)].into();
assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2)]));
}
#[test]
fn value_default_is_null() {
let v: Value = Value::default();
assert_eq!(v, Value::Null);
}
#[test]
fn value_from_json_null() {
let v = Value::from(&serde_json::Value::Null);
assert_eq!(v, Value::Null);
}
#[test]
fn value_from_json_bool() {
let v = Value::from(&serde_json::Value::Bool(true));
assert_eq!(v, Value::Bool(true));
}
#[test]
fn value_from_json_int() {
let v = Value::from(&serde_json::json!(42));
assert_eq!(v, Value::Int(42));
}
#[test]
fn value_from_json_float() {
let v = Value::from(&serde_json::json!(3.14));
match v {
Value::Float(f) => assert!((f - 3.14).abs() < f64::EPSILON),
_ => panic!("expected Float"),
}
}
#[test]
fn value_from_json_string() {
let v = Value::from(&serde_json::Value::String("hi".into()));
assert_eq!(v, Value::string("hi"));
}
#[test]
fn value_from_json_array() {
let v = Value::from(&serde_json::json!([1, true, "x"]));
match v {
Value::List(items) => {
assert_eq!(items.len(), 3);
assert_eq!(items[0], Value::Int(1));
assert_eq!(items[1], Value::Bool(true));
assert_eq!(items[2], Value::string("x"));
}
_ => panic!("expected List"),
}
}
#[test]
fn value_from_json_object() {
let v = Value::from(&serde_json::json!({"a": 1, "b": "x"}));
match v {
Value::Attrs(attrs) => {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::string("x")));
}
_ => panic!("expected Attrs"),
}
}
#[test]
fn value_from_json_nested() {
let v = Value::from(&serde_json::json!({"outer": {"inner": [1, 2]}}));
let json_back = v.to_json();
assert_eq!(json_back, serde_json::json!({"outer": {"inner": [1, 2]}}));
}
#[test]
fn value_from_toml_string() {
let t = toml::Value::String("hi".into());
assert_eq!(Value::from(&t), Value::string("hi"));
}
#[test]
fn value_from_toml_int() {
let t = toml::Value::Integer(42);
assert_eq!(Value::from(&t), Value::Int(42));
}
#[test]
fn value_from_toml_float() {
let t = toml::Value::Float(3.14);
match Value::from(&t) {
Value::Float(f) => assert!((f - 3.14).abs() < f64::EPSILON),
_ => panic!("expected Float"),
}
}
#[test]
fn value_from_toml_bool() {
let t = toml::Value::Boolean(true);
assert_eq!(Value::from(&t), Value::Bool(true));
}
#[test]
fn value_from_toml_array() {
let t = toml::Value::Array(vec![
toml::Value::Integer(1),
toml::Value::Integer(2),
]);
assert_eq!(
Value::from(&t),
Value::list(vec![Value::Int(1), Value::Int(2)]),
);
}
#[test]
fn value_from_toml_table() {
let mut tbl = toml::map::Map::new();
tbl.insert("k".into(), toml::Value::Integer(7));
let t = toml::Value::Table(tbl);
match Value::from(&t) {
Value::Attrs(attrs) => {
assert_eq!(attrs.get("k"), Some(&Value::Int(7)));
}
_ => panic!("expected Attrs"),
}
}
#[test]
fn value_from_toml_datetime_becomes_string() {
let dt: toml::value::Datetime = "2024-01-01T00:00:00Z".parse().unwrap();
let t = toml::Value::Datetime(dt);
match Value::from(&t) {
Value::String(_) => {}
other => panic!("expected String, got {other:?}"),
}
}
#[test]
fn coerce_to_path_from_path() {
let v = Value::Path(Box::new("/foo".into()));
assert_eq!(v.coerce_to_path("ctx").unwrap(), "/foo");
}
#[test]
fn coerce_to_path_from_string() {
let v = Value::string("/bar");
assert_eq!(v.coerce_to_path("ctx").unwrap(), "/bar");
}
#[test]
fn out_path_needs_realize_matches_output_context() {
let mut ctx = StringContext::new();
ctx.add_output("/nix/store/aaa-thing.drv", "out");
assert_eq!(
super::out_path_needs_realize("/nix/store/bbb-thing", &ctx),
Some("/nix/store/aaa-thing.drv".to_string()),
);
}
#[test]
fn out_path_needs_realize_ignores_plain_context() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/ccc-plain");
assert_eq!(super::out_path_needs_realize("/nix/store/ccc-plain", &ctx), None);
}
#[test]
fn out_path_needs_realize_ignores_non_store_path() {
let mut ctx = StringContext::new();
ctx.add_output("/nix/store/ddd.drv", "out");
assert_eq!(super::out_path_needs_realize("/etc/passwd", &ctx), None);
}
#[test]
fn out_path_needs_realize_empty_context_is_none() {
let ctx = StringContext::new();
assert_eq!(super::out_path_needs_realize("/nix/store/eee-lit", &ctx), None);
}
#[test]
fn coerce_to_realized_path_present_output_is_passthrough() {
let dir = std::env::temp_dir().join("sui-ifd-present-test");
std::fs::create_dir_all(&dir).unwrap();
let file = dir.join("out");
std::fs::write(&file, b"present").unwrap();
let present = file.to_string_lossy().to_string();
let mut ctx = StringContext::new();
ctx.add_plain(&present);
let v = Value::String(std::rc::Rc::new(NixString::with_context(
present.as_str(),
ctx,
)));
assert_eq!(v.coerce_to_realized_path("readFile").unwrap(), present);
}
#[test]
fn coerce_to_realized_path_absent_output_invokes_hook() {
use std::sync::{Arc, Mutex};
let seen: Arc<Mutex<Vec<(String, String)>>> = Arc::new(Mutex::new(Vec::new()));
let seen2 = seen.clone();
let _guard = crate::realize::install_realize_hook(Box::new(move |drv, out| {
seen2.lock().unwrap().push((drv.to_string(), out.to_string()));
Ok(())
}));
let out = "/nix/store/zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz-ifd-absent";
assert!(!std::path::Path::new(out).exists(), "test store path must be absent");
let mut ctx = StringContext::new();
ctx.add_output("/nix/store/qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq-ifd-absent.drv", "out");
let v = Value::String(std::rc::Rc::new(NixString::with_context(out, ctx)));
assert_eq!(v.coerce_to_realized_path("readFile").unwrap(), out);
let s = seen.lock().unwrap();
assert_eq!(s.len(), 1, "realize hook should fire once for an absent output");
assert_eq!(s[0].0, "/nix/store/qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqq-ifd-absent.drv");
assert_eq!(s[0].1, out);
}
#[test]
fn coerce_to_path_errors_on_int() {
let v = Value::Int(1);
let e = v.coerce_to_path("readFile").unwrap_err();
match e {
EvalError::TypeError(ref msg) => {
assert!(msg.contains("readFile"));
assert!(msg.contains("path or string"));
assert!(msg.contains("int"));
}
_ => panic!("expected TypeError"),
}
}
#[test]
fn coerce_to_path_errors_on_null() {
let v = Value::Null;
assert!(v.coerce_to_path("ctx").is_err());
}
#[test]
fn coerce_to_path_attrs_with_outpath() {
let mut attrs = NixAttrs::new();
attrs.insert("outPath".to_string(), Value::string("/nix/store/test"));
let val = Value::Attrs(Rc::new(attrs));
assert_eq!(val.coerce_to_path("test").unwrap(), "/nix/store/test");
}
#[test]
fn coerce_to_path_attrs_without_outpath_fails() {
let attrs = NixAttrs::new();
let val = Value::Attrs(Rc::new(attrs));
assert!(val.coerce_to_path("test").is_err());
}
#[test]
fn coerce_to_string_string() {
let v = Value::string("hello");
let (s, _ctx) = v.coerce_to_string().unwrap();
assert_eq!(s, "hello");
}
#[test]
fn coerce_to_string_path() {
let v = Value::Path(Box::new("/foo".into()));
let (s, ctx) = v.coerce_to_string().unwrap();
assert_eq!(s, "/foo");
assert!(!ctx.is_empty()); }
#[test]
fn coerce_to_string_int() {
let v = Value::Int(42);
let (s, _ctx) = v.coerce_to_string().unwrap();
assert_eq!(s, "42");
}
#[test]
fn coerce_to_string_float() {
let v = Value::Float(3.14);
let (s, _ctx) = v.coerce_to_string().unwrap();
assert_eq!(s, "3.140000");
}
#[test]
fn coerce_to_string_bool_true() {
let (s, _ctx) = Value::Bool(true).coerce_to_string().unwrap();
assert_eq!(s, "1");
}
#[test]
fn coerce_to_string_bool_false() {
let (s, _ctx) = Value::Bool(false).coerce_to_string().unwrap();
assert_eq!(s, "");
}
#[test]
fn coerce_to_string_null() {
let (s, _ctx) = Value::Null.coerce_to_string().unwrap();
assert_eq!(s, "");
}
#[test]
fn coerce_to_string_attrs_with_outpath() {
let mut attrs = NixAttrs::new();
attrs.insert("outPath".to_string(), Value::string("/nix/store/abc"));
let val = Value::Attrs(Rc::new(attrs));
let (s, _ctx) = val.coerce_to_string().unwrap();
assert_eq!(s, "/nix/store/abc");
}
#[test]
fn coerce_to_string_attrs_without_outpath_or_tostring_fails() {
let attrs = NixAttrs::new();
let val = Value::Attrs(Rc::new(attrs));
assert!(val.coerce_to_string().is_err());
}
#[test]
fn coerce_to_string_lambda_fails() {
let root = rnix::Root::parse("x: x");
let expr = root.tree().expr().unwrap();
let closure = Closure {
param: match expr {
rnix::ast::Expr::Lambda(ref l) => l.param().unwrap(),
_ => panic!("expected lambda"),
},
body: match expr {
rnix::ast::Expr::Lambda(ref l) => l.body().unwrap(),
_ => panic!("expected lambda"),
},
env: Env::new(),
};
let val = Value::Lambda(Rc::new(closure));
assert!(val.coerce_to_string().is_err());
}
#[test]
fn builtin_fn_debug_includes_name() {
let b = BuiltinFn {
name: "myFunc",
func: Rc::new(|_| Ok(Value::Null)),
};
let s = format!("{b:?}");
assert!(s.contains("myFunc"));
assert!(s.contains("builtin"));
}
#[test]
fn thunk_force_chains_through_inner_thunks() {
let inner_root = rnix::Root::parse("99");
let inner_expr = inner_root.tree().expr().unwrap();
let inner_thunk = Thunk::new_suspended(inner_expr, Env::new());
let outer = Thunk::new_evaluated(Value::Thunk(inner_thunk));
let result = outer.force(&|e, env| crate::eval::eval_expr(e, env));
match result.unwrap() {
Value::Thunk(_) | Value::Int(99) => {}
other => panic!("unexpected: {other:?}"),
}
}
#[test]
fn thunk_inherit_select_debug_format() {
let root = rnix::Root::parse("{ x = 1; }");
let expr = root.tree().expr().unwrap();
let source = Thunk::new_suspended(expr, Env::new());
let thunk = Thunk::new_inherit_select(source, "x");
let s = format!("{thunk:?}");
assert!(s.contains("inherit-select"));
assert!(s.contains("x"));
}
#[test]
fn thunk_blackhole_debug_format() {
let root = rnix::Root::parse("1");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
*unsafe { &mut *thunk.0.repr.get() } = ThunkRepr::Blackhole;
assert_eq!(format!("{thunk:?}"), "<blackhole>");
}
#[test]
fn value_display_thunk_evaluates() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert_eq!(format!("{val}"), "42");
}
#[test]
fn value_to_json_thunk_forces() {
let root = rnix::Root::parse(r#""world""#);
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert_eq!(val.to_json(), serde_json::Value::String("world".into()));
}
#[test]
fn value_type_name_thunk_forces() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert_eq!(val.type_name(), "int");
}
#[test]
fn as_string_errors_on_thunk() {
let root = rnix::Root::parse(r#""x""#);
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
let err = val.as_string().unwrap_err();
match err {
EvalError::TypeError(msg) => assert!(msg.contains("thunk")),
_ => panic!("expected TypeError"),
}
}
#[test]
fn as_nix_string_errors_on_thunk() {
let root = rnix::Root::parse(r#""x""#);
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert!(val.as_nix_string().is_err());
}
#[test]
fn as_attrs_errors_on_thunk() {
let root = rnix::Root::parse("{}");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert!(val.as_attrs().is_err());
}
#[test]
fn as_list_errors_on_thunk() {
let root = rnix::Root::parse("[]");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert!(val.as_list().is_err());
}
#[test]
fn as_nix_string_ok_on_string() {
let v = Value::string("hi");
let ns = v.as_nix_string().unwrap();
assert_eq!(ns.as_str(), "hi");
}
#[test]
fn as_nix_string_errors_on_int() {
let v = Value::Int(1);
match v.as_nix_string() {
Err(EvalError::TypeMismatch { expected, got }) => {
assert_eq!(expected, "string");
assert_eq!(got, "int");
}
_ => panic!("expected TypeMismatch"),
}
}
#[test]
fn oncecell_cache_populated_after_force() {
let root = rnix::Root::parse("42");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
assert!(thunk.0.cache.get().is_none());
let _ = thunk.force(&|e, env| crate::eval::eval_expr(e, env)).unwrap();
assert!(thunk.0.cache.get().is_some());
}
#[test]
fn oncecell_cache_matches_force_result() {
let root = rnix::Root::parse("1 + 2");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let forced = thunk.force(&|e, env| crate::eval::eval_expr(e, env)).unwrap();
let cached = thunk.0.cache.get().unwrap();
assert_eq!((**cached).clone().into_value(), forced);
}
#[test]
fn oncecell_new_evaluated_prepopulates_cache() {
let thunk = Thunk::new_evaluated(Value::Int(77));
let cached = thunk.0.cache.get().expect("cache should be pre-populated");
assert_eq!(**cached, Concrete::Int(77));
}
#[test]
fn oncecell_is_evaluated_uses_cache() {
let thunk = Thunk::new_evaluated(Value::Bool(false));
assert!(thunk.is_evaluated());
assert!(thunk.0.cache.get().is_some());
}
#[test]
fn oncecell_already_evaluated_returns_cached_without_repr() {
let thunk = Thunk::new_evaluated(Value::Int(55));
let result = thunk.force(&|_, _| panic!("evaluator should not be called"));
assert_eq!(result.unwrap(), Value::Int(55));
}
#[test]
fn with_scope_created_with_empty_cache() {
let thunk = Thunk::new_suspended(
rnix::Root::parse("{}").tree().expr().unwrap(),
Env::new(),
);
let env = Env::new().with_scope(Value::Thunk(thunk));
let scope = &env.0.with_scopes[0];
assert!(scope.cached.borrow().is_none());
}
#[test]
fn with_scope_concrete_pre_populates_cache() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(1));
let env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
let scope = &env.0.with_scopes[0];
assert!(scope.cached.borrow().is_some());
}
#[test]
fn with_scope_first_lookup_populates_cache() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(42));
let env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
assert!(env.0.with_scopes[0].cached.borrow().is_some());
let _ = env.lookup("x");
assert!(env.0.with_scopes[0].cached.borrow().is_some());
}
#[test]
fn with_scope_second_lookup_uses_cache() {
let mut attrs = NixAttrs::new();
attrs.insert("x".to_string(), Value::Int(10));
let env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
assert_eq!(env.lookup("x"), Some(Value::Int(10)));
assert!(env.0.with_scopes[0].cached.borrow().is_some());
assert_eq!(env.lookup("x"), Some(Value::Int(10)));
}
#[test]
fn with_scope_child_shares_cache_via_rc() {
let mut attrs = NixAttrs::new();
attrs.insert("shared".to_string(), Value::Int(7));
let parent = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
let child = parent.child();
let _ = parent.lookup("shared");
assert!(child.0.with_scopes[0].cached.borrow().is_some());
}
#[test]
fn with_scope_innermost_checked_first() {
let mut outer = NixAttrs::new();
outer.insert("x".to_string(), Value::Int(1));
outer.insert("y".to_string(), Value::Int(100));
let mut inner = NixAttrs::new();
inner.insert("x".to_string(), Value::Int(2));
let env = Env::new()
.with_scope(Value::Attrs(Rc::new(outer)))
.with_scope(Value::Attrs(Rc::new(inner)));
assert_eq!(env.lookup("x"), Some(Value::Int(2)));
assert_eq!(env.lookup("y"), Some(Value::Int(100)));
}
#[test]
fn fxhashmap_nixattrs_new_creates_empty() {
let a = NixAttrs::new();
assert!(a.is_empty());
assert_eq!(a.len(), 0);
assert!(a.inner().is_empty());
}
#[test]
fn fxhashmap_insert_get_roundtrip_with_symbol_keys() {
let mut a = NixAttrs::new();
a.insert("mykey".to_string(), Value::Int(42));
assert_eq!(a.get("mykey"), Some(&Value::Int(42)));
}
#[test]
fn fxhashmap_contains_key_with_interned_keys() {
let mut a = NixAttrs::new();
a.insert("alpha".to_string(), Value::Int(1));
let sym = intern("alpha");
assert!(a.inner().contains_key(&sym));
let missing_sym = intern("beta");
assert!(!a.inner().contains_key(&missing_sym));
}
#[test]
fn fxhashmap_remove_returns_value() {
let mut a = NixAttrs::new();
a.insert("key".to_string(), Value::Int(99));
let removed = a.remove("key");
assert_eq!(removed, Some(Value::Int(99)));
assert!(a.is_empty());
}
#[test]
fn fxhashmap_keys_returns_sorted_strings() {
let mut a = NixAttrs::new();
a.insert("zulu".to_string(), Value::Int(1));
a.insert("alpha".to_string(), Value::Int(2));
a.insert("mike".to_string(), Value::Int(3));
let keys: Vec<String> = a.keys().collect();
assert_eq!(keys, vec!["alpha", "mike", "zulu"]);
}
#[test]
fn fxhashmap_iter_returns_sorted_string_value_pairs() {
let mut a = NixAttrs::new();
a.insert("b".to_string(), Value::Int(2));
a.insert("a".to_string(), Value::Int(1));
let pairs: Vec<(String, &Value)> = a.iter().collect();
assert_eq!(pairs.len(), 2);
assert_eq!(pairs[0].0, "a");
assert_eq!(*pairs[0].1, Value::Int(1));
assert_eq!(pairs[1].0, "b");
assert_eq!(*pairs[1].1, Value::Int(2));
}
#[test]
fn fxhashmap_update_merges_correctly() {
let mut left = NixAttrs::new();
left.insert("a".to_string(), Value::Int(1));
left.insert("b".to_string(), Value::Int(2));
let mut right = NixAttrs::new();
right.insert("b".to_string(), Value::Int(20));
right.insert("c".to_string(), Value::Int(3));
let merged = left.update(&right);
assert_eq!(merged.get("a"), Some(&Value::Int(1)));
assert_eq!(merged.get("b"), Some(&Value::Int(20))); assert_eq!(merged.get("c"), Some(&Value::Int(3)));
assert_eq!(merged.len(), 3);
}
#[test]
fn fxhashmap_from_iterator_collects_with_interning() {
let pairs = vec![
("x".to_string(), Value::Int(10)),
("y".to_string(), Value::Int(20)),
("z".to_string(), Value::Int(30)),
];
let attrs: NixAttrs = pairs.into_iter().collect();
assert_eq!(attrs.len(), 3);
assert_eq!(attrs.get("x"), Some(&Value::Int(10)));
assert_eq!(attrs.get("y"), Some(&Value::Int(20)));
assert_eq!(attrs.get("z"), Some(&Value::Int(30)));
let sym_x = intern("x");
assert!(attrs.inner().contains_key(&sym_x));
}
#[test]
fn smallvec_context_empty() {
let ctx = StringContext::new();
assert!(ctx.is_empty());
assert_eq!(ctx.len(), 0);
assert_eq!(ctx.elements().len(), 0);
}
#[test]
fn smallvec_context_single_element_inline() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/single");
assert_eq!(ctx.len(), 1);
assert!(!ctx.is_empty());
}
#[test]
fn smallvec_context_two_elements_still_inline() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/one");
ctx.add_output("/nix/store/two.drv", "out");
assert_eq!(ctx.len(), 2);
}
#[test]
fn smallvec_context_three_plus_spills_to_heap() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/a");
ctx.add_plain("/nix/store/b");
ctx.add_drv_deep("/nix/store/c.drv");
assert_eq!(ctx.len(), 3);
assert!(ctx.elements().contains(&ContextElement::Plain(SmolStr::from("/nix/store/a"))));
assert!(ctx.elements().contains(&ContextElement::Plain(SmolStr::from("/nix/store/b"))));
assert!(ctx.elements().contains(&ContextElement::DrvDeep(SmolStr::from("/nix/store/c.drv"))));
}
#[test]
fn smallvec_context_merge_deduplicates() {
let mut ctx1 = StringContext::new();
ctx1.add_plain("/nix/store/dup");
ctx1.add_output("/nix/store/x.drv", "out");
let mut ctx2 = StringContext::new();
ctx2.add_plain("/nix/store/dup"); ctx2.add_plain("/nix/store/unique"); ctx1.merge(&ctx2);
assert_eq!(ctx1.len(), 3); }
#[test]
fn smallvec_context_add_plain_output_drv_deep() {
let mut ctx = StringContext::new();
ctx.add_plain("/nix/store/plain");
assert_eq!(ctx.len(), 1);
assert!(ctx.elements().contains(&ContextElement::Plain(SmolStr::from("/nix/store/plain"))));
ctx.add_output("/nix/store/out.drv", "lib");
assert_eq!(ctx.len(), 2);
assert!(ctx.elements().contains(&ContextElement::Output {
drv: SmolStr::from("/nix/store/out.drv"),
output: SmolStr::from("lib"),
}));
ctx.add_drv_deep("/nix/store/deep.drv");
assert_eq!(ctx.len(), 3);
assert!(ctx.elements().contains(&ContextElement::DrvDeep(SmolStr::from("/nix/store/deep.drv"))));
}
#[test]
fn rc_list_constructor_wraps_in_rc() {
let v = Value::list(vec![Value::Int(1), Value::Int(2)]);
match &v {
Value::List(rc) => {
assert_eq!(rc.len(), 2);
assert_eq!(Rc::strong_count(rc), 1);
}
_ => panic!("expected List"),
}
}
#[test]
fn rc_list_clone_is_refcount_bump() {
let v = Value::list(vec![Value::Int(10)]);
let rc1 = match &v {
Value::List(rc) => rc.clone(),
_ => panic!("expected List"),
};
let v2 = v.clone();
let rc2 = match &v2 {
Value::List(rc) => rc.clone(),
_ => panic!("expected List"),
};
assert!(Rc::ptr_eq(&rc1, &rc2));
assert!(Rc::strong_count(&rc1) >= 2);
}
#[test]
fn rc_list_as_list_returns_slice() {
let v = Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]);
let slice = v.as_list().unwrap();
assert_eq!(slice.len(), 3);
assert_eq!(slice[0], Value::Int(1));
assert_eq!(slice[1], Value::Int(2));
assert_eq!(slice[2], Value::Int(3));
}
#[test]
fn rc_list_from_vec_wraps_in_rc() {
let items = vec![Value::Bool(true), Value::Bool(false)];
let v: Value = items.into();
match &v {
Value::List(rc) => {
assert_eq!(rc.len(), 2);
assert_eq!(Rc::strong_count(rc), 1);
}
_ => panic!("expected List"),
}
}
#[test]
fn intern_same_string_returns_same_symbol() {
let s1 = intern("hello_intern_test");
let s2 = intern("hello_intern_test");
assert_eq!(s1, s2);
}
#[test]
fn intern_different_strings_returns_different_symbols() {
let s1 = intern("unique_str_a_9182");
let s2 = intern("unique_str_b_9182");
assert_ne!(s1, s2);
}
#[test]
fn resolve_roundtrips_correctly() {
let sym = intern("roundtrip_test_str");
let resolved = resolve(sym);
assert_eq!(resolved, "roundtrip_test_str");
}
#[test]
fn intern_cached_same_offset_returns_cached_symbol() {
let sid = next_source_id();
let sym1 = intern_cached("cached_ident_aa", sid, 100);
let sym2 = intern_cached("cached_ident_aa", sid, 100);
assert_eq!(sym1, sym2);
}
#[test]
fn intern_cached_different_offset_same_string_returns_same_symbol() {
let sid = next_source_id();
let sym1 = intern_cached("dedup_test_str_77", sid, 200);
let sym2 = intern_cached("dedup_test_str_77", sid, 300);
assert_eq!(sym1, sym2);
}
#[test]
fn clear_ident_cache_clears() {
let sid = next_source_id();
let _sym = intern_cached("to_be_cleared_99", sid, 500);
clear_ident_cache();
let sym2 = intern_cached("to_be_cleared_99", sid, 500);
let resolved = resolve(sym2);
assert_eq!(resolved, "to_be_cleared_99");
}
#[test]
fn next_source_id_increments_monotonically() {
let id1 = next_source_id();
let id2 = next_source_id();
let id3 = next_source_id();
assert_eq!(id2, id1 + 1);
assert_eq!(id3, id2 + 1);
}
#[test]
fn env_new_creates_empty_bindings() {
let env = Env::new();
assert!(env.0.bindings.is_empty());
assert!(env.0.with_scopes.is_empty());
assert!(env.eval_file().is_none());
}
#[test]
fn env_bind_lookup_roundtrip() {
let mut env = Env::new();
env.bind("foo".to_string(), Value::Int(42));
assert_eq!(env.lookup("foo"), Some(Value::Int(42)));
assert_eq!(env.lookup("bar"), None);
}
#[test]
fn env_child_inherits_parent_bindings_flattened() {
let mut parent = Env::new();
parent.bind("a".to_string(), Value::Int(1));
parent.bind("b".to_string(), Value::Int(2));
let child = parent.child();
assert_eq!(child.lookup("a"), Some(Value::Int(1)));
assert_eq!(child.lookup("b"), Some(Value::Int(2)));
let sym_a = intern("a");
assert!(child.0.bindings.contains_key(&sym_a));
}
#[test]
fn env_child_inherits_with_scopes() {
let mut attrs = NixAttrs::new();
attrs.insert("ws".to_string(), Value::Int(10));
let parent = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
let child = parent.child();
assert_eq!(child.0.with_scopes.len(), parent.0.with_scopes.len());
assert_eq!(child.lookup("ws"), Some(Value::Int(10)));
}
#[test]
fn env_lookup_sym_fast_path_matches_lookup() {
let mut env = Env::new();
env.bind("target".to_string(), Value::Int(88));
let sym = intern("target");
let via_lookup = env.lookup("target");
let via_sym = env.lookup_sym(sym);
assert_eq!(via_lookup, via_sym);
assert_eq!(via_sym, Some(Value::Int(88)));
}
#[test]
fn env_lookup_sym_with_scope_fallback() {
let mut attrs = NixAttrs::new();
attrs.insert("sym_ws".to_string(), Value::Int(33));
let env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
let sym = intern("sym_ws");
assert_eq!(env.lookup_sym(sym), Some(Value::Int(33)));
}
#[test]
fn env_with_scope_ordering_multiple_innermost_wins() {
let mut a1 = NixAttrs::new();
a1.insert("x".to_string(), Value::Int(1));
let mut a2 = NixAttrs::new();
a2.insert("x".to_string(), Value::Int(2));
let mut a3 = NixAttrs::new();
a3.insert("x".to_string(), Value::Int(3));
let env = Env::new()
.with_scope(Value::Attrs(Rc::new(a1)))
.with_scope(Value::Attrs(Rc::new(a2)))
.with_scope(Value::Attrs(Rc::new(a3)));
assert_eq!(env.lookup("x"), Some(Value::Int(3)));
}
#[test]
fn env_lookup_sym_not_found_returns_none() {
let env = Env::new();
let sym = intern("nonexistent_sym_99");
assert_eq!(env.lookup_sym(sym), None);
}
#[test]
fn env_lookup_sym_lexical_wins_over_with_scope() {
let mut attrs = NixAttrs::new();
attrs.insert("priority".to_string(), Value::Int(1));
let mut env = Env::new().with_scope(Value::Attrs(Rc::new(attrs)));
env.bind("priority".to_string(), Value::Int(99));
let sym = intern("priority");
assert_eq!(env.lookup_sym(sym), Some(Value::Int(99)));
}
}