use std::cell::{Cell, RefCell};
use std::collections::{HashSet, HashMap, VecDeque};
use std::path::PathBuf;
use rnix::ast::{self, AstToken, HasEntry, InterpolPart};
use rowan::ast::AstNode;
use crate::builtins;
use crate::value::*;
thread_local! { static EVAL_DEPTH: Cell<usize> = const { Cell::new(0) }; }
thread_local! {
static CURRENT_SOURCE_ID: Cell<u32> = const { Cell::new(0) };
}
thread_local! {
static EVAL_FILE_STACK: RefCell<Vec<Option<PathBuf>>> = const { RefCell::new(Vec::new()) };
static NIX_TRACE_STACK: RefCell<Vec<NixTraceFrame>> = const { RefCell::new(Vec::new()) };
}
#[derive(Debug, Clone)]
pub enum NixTraceFrame {
Eager {
file: Option<String>,
description: String,
},
Lambda {
closure_env: Env,
current_file: Option<PathBuf>,
},
}
fn strip_source_prefix(p: &std::path::Path) -> String {
let s = p.display().to_string();
s.rsplit_once("-source/")
.map_or_else(|| p.display().to_string(), |(_, tail)| tail.to_string())
}
impl NixTraceFrame {
fn file(&self) -> Option<String> {
match self {
NixTraceFrame::Eager { file, .. } => file.clone(),
NixTraceFrame::Lambda { current_file, .. } => {
current_file.as_deref().map(strip_source_prefix)
}
}
}
fn description(&self) -> String {
self.to_string()
}
}
impl std::fmt::Display for NixTraceFrame {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
NixTraceFrame::Eager { description, .. } => f.write_str(description),
NixTraceFrame::Lambda { closure_env, .. } => {
let file = closure_env.eval_file().map(|p| strip_source_prefix(p));
write!(
f,
"while calling function defined in {}",
file.as_deref().unwrap_or("<eval>")
)
}
}
}
}
fn push_nix_trace(desc: impl Into<String>) -> NixTraceGuard {
let frame = NixTraceFrame::Eager {
file: current_eval_file().map(|p| {
p.display().to_string()
.rsplit_once("-source/")
.map_or_else(|| p.display().to_string(), |(_, s)| s.to_string())
}),
description: desc.into(),
};
NIX_TRACE_STACK.with(|s| s.borrow_mut().push(frame));
NixTraceGuard
}
fn push_nix_trace_lambda(closure_env: &Env) -> NixTraceGuard {
let frame = NixTraceFrame::Lambda {
closure_env: closure_env.clone(),
current_file: current_eval_file(),
};
NIX_TRACE_STACK.with(|s| s.borrow_mut().push(frame));
NixTraceGuard
}
struct NixTraceGuard;
impl Drop for NixTraceGuard {
fn drop(&mut self) {
NIX_TRACE_STACK.with(|s| s.borrow_mut().pop());
}
}
pub fn attach_trace(err: EvalError) -> EvalError {
NIX_TRACE_STACK.with(|s| {
let stack = s.borrow();
if stack.is_empty() {
return err;
}
let max_frames = std::env::var("SUI_M26_MAXFRAMES").ok()
.and_then(|s| s.parse::<usize>().ok()).unwrap_or(15);
let mut trace = format!("{err}");
for (i, frame) in stack.iter().rev().take(max_frames).enumerate() {
let file = frame.file();
let loc = file.as_deref().unwrap_or("<eval>");
trace.push_str(&format!("\n {} ({loc})", frame.description()));
if i + 1 >= max_frames && stack.len() > max_frames {
trace.push_str(&format!("\n ... ({} more frames)", stack.len() - max_frames));
}
}
match err {
EvalError::Throw(_) => EvalError::Throw(trace),
EvalError::AssertionFailed(_) => EvalError::AssertionFailed(trace),
_ => EvalError::TypeError(trace),
}
})
}
#[must_use]
pub fn current_eval_dir() -> Option<PathBuf> {
EVAL_FILE_STACK
.with(|s| s.borrow().last().cloned())
.flatten()
.and_then(|p| p.parent().map(PathBuf::from))
}
pub fn push_eval_file(file: PathBuf) -> EvalFileGuard {
push_eval_frame(Some(file))
}
pub fn push_eval_frame(file: Option<PathBuf>) -> EvalFileGuard {
EVAL_FILE_STACK.with(|s| s.borrow_mut().push(file));
EvalFileGuard
}
#[must_use]
pub fn current_eval_file() -> Option<PathBuf> {
EVAL_FILE_STACK.with(|s| s.borrow().last().cloned()).flatten()
}
pub fn eval_file_stack_snapshot() -> Vec<String> {
EVAL_FILE_STACK.with(|s| {
s.borrow().iter().map(|p| {
let Some(p) = p else { return "<no-file>".to_string() };
let s = p.display().to_string();
s.rsplit_once("-source/").map_or(s.clone(), |(_, r)| r.to_string())
}).collect()
})
}
pub(crate) fn eval_file_ctx() -> String {
current_eval_file()
.map(|p| format!(", in '{}'", p.display()))
.unwrap_or_default()
}
pub struct EvalFileGuard;
impl Drop for EvalFileGuard {
fn drop(&mut self) {
EVAL_FILE_STACK.with(|s| {
s.borrow_mut().pop();
});
}
}
pub fn push_source_id(id: u32) -> SourceIdGuard {
let prev = CURRENT_SOURCE_ID.with(|s| {
let old = s.get();
s.set(id);
old
});
SourceIdGuard(prev)
}
pub struct SourceIdGuard(u32);
impl Drop for SourceIdGuard {
fn drop(&mut self) {
CURRENT_SOURCE_ID.with(|s| s.set(self.0));
}
}
pub fn normalize_path(path: &std::path::Path) -> std::path::PathBuf {
crate::path::normalize(path)
}
thread_local! {
static PURE_MODE: Cell<bool> = const { Cell::new(false) };
}
pub fn set_pure_mode(pure: bool) {
PURE_MODE.with(|p| p.set(pure));
}
#[must_use]
pub fn is_pure_mode() -> bool {
PURE_MODE.with(Cell::get)
}
#[cfg(test)]
const MAX_EVAL_DEPTH: Option<usize> = Some(2_048);
#[cfg(not(test))]
const MAX_EVAL_DEPTH: Option<usize> = None;
struct DepthGuard;
const PROMOTION_RUNAWAY_EVAL_DEPTH: usize = 500;
impl DepthGuard {
#[inline(always)]
fn enter() -> Result<Self, EvalError> {
EVAL_DEPTH.with(|d| {
let depth = d.get();
if matches!(MAX_EVAL_DEPTH, Some(max) if depth > max) {
return Err(EvalError::InfiniteRecursion(
"eval depth exceeded".into(),
));
}
if depth > PROMOTION_RUNAWAY_EVAL_DEPTH
&& crate::value::promotion_occurred()
{
return Err(EvalError::InfiniteRecursion(
"overlay-fixpoint promotion runaway (eval depth exceeded)".into(),
));
}
d.set(depth + 1);
Ok(DepthGuard)
})
}
}
impl Drop for DepthGuard {
#[inline(always)]
fn drop(&mut self) {
EVAL_DEPTH.with(|d| d.set(d.get().saturating_sub(1)));
}
}
fn collect_referenced_names(expr: &ast::Expr) -> HashSet<String> {
let mut names = HashSet::new();
for node in expr.syntax().descendants() {
if let Some(ident) = ast::Ident::cast(node) {
names.insert(ident_text(&ident));
}
}
names
}
fn compute_needed_bindings(
body: &ast::Expr,
binding_info: &[(String, Option<ast::Expr>)], ) -> HashSet<String> {
let body_refs = collect_referenced_names(body);
let mut all_names: HashSet<String> = HashSet::with_capacity(binding_info.len());
let mut deps: HashMap<String, HashSet<String>> = HashMap::with_capacity(binding_info.len());
for (name, value_expr) in binding_info {
all_names.insert(name.clone());
if let Some(expr) = value_expr {
deps.insert(name.clone(), collect_referenced_names(expr));
}
}
let mut needed: HashSet<String> = body_refs.intersection(&all_names).cloned().collect();
let mut queue: VecDeque<String> = needed.iter().cloned().collect();
while let Some(name) = queue.pop_front() {
if let Some(name_deps) = deps.get(&name) {
for dep in name_deps {
if all_names.contains(dep) && needed.insert(dep.clone()) {
queue.push_back(dep.clone());
}
}
}
}
needed
}
#[must_use = "evaluation result should be used"]
pub fn eval(input: &str) -> Result<Value, EvalError> {
eval_with_file(input, None)
}
thread_local! {
static EVAL_NESTING: Cell<usize> = const { Cell::new(0) };
}
pub fn eval_with_file(input: &str, file: Option<std::path::PathBuf>) -> Result<Value, EvalError> {
let nesting = EVAL_NESTING.with(|n| {
let v = n.get();
n.set(v + 1);
v
});
if nesting == 0 {
crate::perf::init();
crate::perf::start();
crate::trace::init_trace();
clear_ident_cache();
crate::resolve_env::clear();
}
let parse = rnix::Root::parse(input);
if !parse.errors().is_empty() {
let msgs: Vec<String> = parse.errors().iter().map(|e| e.to_string()).collect();
EVAL_NESTING.with(|n| n.set(n.get().saturating_sub(1)));
return Err(EvalError::ParseError(msgs.join("; ")));
}
let src_id = next_source_id();
if crate::resolve_env::enabled() {
let table = sui_resolve::resolve(&parse.tree());
crate::resolve_env::populate(src_id, &table);
}
crate::pos::register_source(file.as_deref(), input);
let prev_src_id = CURRENT_SOURCE_ID.with(|s| {
let old = s.get();
s.set(src_id);
old
});
let root = parse.tree();
let expr = match root.expr() {
Some(e) => e,
None => {
CURRENT_SOURCE_ID.with(|s| s.set(prev_src_id));
EVAL_NESTING.with(|n| n.set(n.get().saturating_sub(1)));
return Err(EvalError::ParseError("empty expression".to_string()));
}
};
let mut env = Env::new();
env.set_eval_file(file);
env.set_source_id(src_id);
builtins::register(&mut env);
let result = eval_expr(&expr, &env).map_err(|e| attach_trace(e))?;
let final_result = force_value(&result).map_err(|e| attach_trace(e));
CURRENT_SOURCE_ID.with(|s| s.set(prev_src_id));
EVAL_NESTING.with(|n| n.set(n.get().saturating_sub(1)));
if nesting == 0 {
crate::perf::report();
}
final_result
}
#[inline(always)]
pub fn force_concrete(value: &Value) -> Result<Concrete, EvalError> {
value.demand()
}
pub fn force_value(value: &Value) -> Result<Value, EvalError> {
crate::perf::inc(crate::perf::Counter::ForceValue);
if !matches!(value, Value::Thunk(_)) {
return Ok(value.clone());
}
let mut v = value.clone();
let mut depth = 0u32;
loop {
match v {
Value::Thunk(ref thunk) => {
v = force_thunk(thunk)?;
depth += 1;
if depth > 100 {
return Err(EvalError::InfiniteRecursion(
"force_value: thunk chain exceeded depth 100 (cycle or runaway lazy wrap)".into(),
));
}
}
_ => return Ok(v),
}
}
}
pub fn force_value_tracked(value: &Value, site: &str) -> Result<Value, EvalError> {
crate::perf::inc(crate::perf::Counter::ForceValue);
if let Value::Thunk(thunk) = value {
FORCE_SITES.with(|sites| {
*sites.borrow_mut().entry(site.to_string()).or_insert(0) += 1;
});
force_thunk(thunk)
} else {
Ok(value.clone())
}
}
thread_local! {
static FORCE_SITES: std::cell::RefCell<std::collections::HashMap<String, u64>> =
std::cell::RefCell::new(std::collections::HashMap::new());
static APPLY_SITES: std::cell::RefCell<std::collections::HashMap<String, u64>> =
std::cell::RefCell::new(std::collections::HashMap::new());
}
pub fn dump_force_sites() {
FORCE_SITES.with(|sites| {
let sites = sites.borrow();
let mut sorted: Vec<_> = sites.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1));
eprintln!("[force-sites] top thunk force call sites:");
for (site, count) in sorted.iter().take(10) {
eprintln!(" {count:>8} {site}");
}
});
APPLY_SITES.with(|sites| {
let sites = sites.borrow();
let mut sorted: Vec<_> = sites.iter().collect();
sorted.sort_by(|a, b| b.1.cmp(a.1));
eprintln!("[apply-sites] top lambda call sites by source file:");
for (site, count) in sorted.iter().take(15) {
let short = site.rsplit_once("-source/").map_or(site.as_str(), |(_,s)| s);
eprintln!(" {count:>8} {short}");
}
});
}
fn force_thunk(thunk: &Thunk) -> Result<Value, EvalError> {
if let Some(cached) = thunk.peek() {
crate::perf::inc(crate::perf::Counter::ThunkHit);
return Ok(cached.clone().into_value());
}
stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || {
thunk.force(&|expr, env| eval_expr(expr, env))
})
}
fn scope_narrow_level() -> u8 {
static LEVEL: std::sync::OnceLock<u8> = std::sync::OnceLock::new();
*LEVEL.get_or_init(
|| match std::env::var("SUI_SCOPE_NARROW").ok().as_deref() {
Some("0") => 0,
Some("1") => 1,
_ => 2,
},
)
}
#[inline]
fn scope_narrow_enabled() -> bool {
scope_narrow_level() >= 1
}
#[inline]
fn scope_cluster_enabled() -> bool {
scope_narrow_level() >= 2
}
fn referenced_idents(value_expr: &ast::Expr) -> HashSet<SmolStr> {
use rnix::SyntaxKind;
let perf_on = crate::perf::enabled();
let t0 = if perf_on {
Some(std::time::Instant::now())
} else {
None
};
crate::perf::inc(crate::perf::Counter::SelfRecWalkCalls);
let mut nodes_walked: u64 = 0;
let mut set: HashSet<SmolStr> = HashSet::new();
for node in value_expr.syntax().descendants() {
nodes_walked += 1;
if node.kind() == SyntaxKind::NODE_IDENT
&& node
.parent()
.is_none_or(|p| p.kind() != SyntaxKind::NODE_ATTRPATH)
&& let Some(i) = ast::Ident::cast(node)
{
set.insert(SmolStr::from(ident_text(&i).as_str()));
}
}
crate::perf::add(crate::perf::Counter::SelfRecWalkNodes, nodes_walked);
if let Some(t0) = t0 {
crate::trace::add_self_rec_walk_nanos(t0.elapsed().as_nanos());
}
set
}
fn is_self_recursive_binding(value_expr: &ast::Expr, name: &str) -> bool {
referenced_idents(value_expr).contains(name)
}
fn maybe_thunk(
expr: &ast::Expr,
env: &Env,
is_rec: bool,
defined_so_far: Option<&HashSet<String>>,
) -> Value {
match expr {
ast::Expr::Literal(lit) => eval_literal(lit).unwrap_or_else(|_| {
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
}),
ast::Expr::Ident(ident) if !is_rec => {
let sym = {
let src_id = env.source_id();
let offset = u32::from(ident.syntax().text_range().start());
crate::value::intern_cached_with(src_id, offset, || {
crate::value::intern(&ident_text(ident))
})
};
if let Some(kw) = crate::value::with_resolved(sym, |s| match s {
"true" => Some(Value::Bool(true)),
"false" => Some(Value::Bool(false)),
"null" => Some(Value::Null),
_ => None,
}) {
return kw;
}
{
{
if let Some(v) = env.lookup_fast(sym, "") {
return v;
}
if let Some((scope_cache, scope_value)) = env.innermost_with_scope() {
return Value::Thunk(Thunk::new_with_ident(
SmolStr::from(ident_text(ident).as_str()),
scope_cache,
scope_value,
env.clone(),
));
}
crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeIdent);
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
}
}
}
ast::Expr::Ident(ident) if is_rec => {
let name = ident_text(ident);
match name.as_str() {
"true" => Value::Bool(true),
"false" => Value::Bool(false),
"null" => Value::Null,
_ => {
if defined_so_far.map_or(false, |d| d.contains(&name)) {
env.lookup(&name).unwrap_or_else(|| {
crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeIdent);
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
})
} else {
crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeIdent);
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
}
}
}
}
ast::Expr::PathAbs(p) if !parts_have_interpolation(&p.parts()) => {
let text = crate::path::canon_abs(&p.syntax().text().to_string());
Value::Path(Box::new(SmolStr::from(text.as_str())))
}
ast::Expr::PathHome(p) if !parts_have_interpolation(&p.parts()) => {
let text = p.syntax().text().to_string();
Value::Path(Box::new(SmolStr::from(text.as_str())))
}
ast::Expr::Str(st) if !str_has_interpolation(st) => {
eval_str(st, env).unwrap_or_else(|_| {
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
})
}
ast::Expr::Lambda(lam) if !is_rec => {
if let (Some(param), Some(body)) = (lam.param(), lam.body()) {
Value::Lambda(Rc::new(Closure {
param,
body,
env: env.clone(),
}))
} else {
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
}
}
_ => {
crate::perf::inc(crate::perf::Counter::ThunkSiteMaybeOther);
if crate::perf::enabled() {
let kind = match expr {
ast::Expr::Select(_) => "Select",
ast::Expr::Apply(_) => "Apply",
ast::Expr::BinOp(_) => "BinOp",
ast::Expr::IfElse(_) => "IfElse",
ast::Expr::Str(_) => "Str",
ast::Expr::List(_) => "List",
ast::Expr::With(_) => "With",
ast::Expr::Assert(_) => "Assert",
ast::Expr::HasAttr(_) => "HasAttr",
ast::Expr::UnaryOp(_) => "UnaryOp",
ast::Expr::Paren(_) => "Paren",
ast::Expr::LetIn(_) => "LetIn",
ast::Expr::AttrSet(_) => "AttrSet",
ast::Expr::Ident(_) => "Ident(rec)",
ast::Expr::Lambda(_) => "Lambda(rec)",
ast::Expr::LegacyLet(_) => "LegacyLet",
ast::Expr::PathAbs(_)
| ast::Expr::PathHome(_)
| ast::Expr::PathRel(_)
| ast::Expr::PathSearch(_) => "Path(interp)",
_ => "Other",
};
crate::trace::inc_maybe_other_kind(kind);
}
Value::Thunk(Thunk::new_suspended(expr.clone(), env.clone()))
}
}
}
#[inline(always)]
pub fn eval_expr(expr: &ast::Expr, env: &Env) -> Result<Value, EvalError> {
match expr {
ast::Expr::Ident(ident) => {
crate::perf::inc(crate::perf::Counter::EvalExpr);
if crate::perf::enabled() {
crate::perf::inc(crate::perf::Counter::ExprIdent);
}
if crate::resolve_env::enabled() {
let src_id = CURRENT_SOURCE_ID.with(std::cell::Cell::get);
let offset = u32::from(ident.syntax().text_range().start());
if let sui_resolve::Resolution::Lexical { sym } =
crate::resolve_env::resolution_for(src_id, offset)
{
if let Some(v) = env.lookup_lexical_sym(sym) {
return Ok(v);
}
}
}
let sym = {
let src_id = env.source_id();
let offset = u32::from(ident.syntax().text_range().start());
crate::value::intern_cached_with(src_id, offset, || {
crate::value::intern(&ident_text(ident))
})
};
if let Some(kw) = crate::value::with_resolved(sym, |s| match s {
"true" => Some(Value::Bool(true)),
"false" => Some(Value::Bool(false)),
"null" => Some(Value::Null),
_ => None,
}) {
return Ok(kw);
}
return {
{
if let Some(v) = env.lookup_fast(sym, "") {
Ok(v)
} else {
let name = ident_text(ident);
let fresh = crate::value::intern(name.as_str());
if fresh != sym {
if let Some(v) = env.lookup_fast(fresh, name.as_str()) {
return Ok(v);
}
}
if env.with_scope_count() > 0 {
if let Some((scope_cache, scope_value)) = env.innermost_with_scope() {
Ok(Value::Thunk(Thunk::new_with_ident(
SmolStr::from(name.as_str()),
scope_cache,
scope_value,
env.clone(),
)))
} else if crate::value::in_promise_eval() {
Ok(Value::Null)
} else {
Err(EvalError::UndefinedVar(
format!("'{name}'{}", eval_file_ctx()),
))
}
} else {
if let Ok(dbg_var) = std::env::var("SUI_DEBUG_VAR") {
if dbg_var == name || dbg_var == "*" {
eprintln!(
"[sui-debug] UndefinedVar '{name}' in {}\n\
[sui-debug] env bindings ({} total): {:?}\n\
[sui-debug] with_scopes: {}",
eval_file_ctx(),
env.binding_count(),
env.binding_names_preview(20),
env.with_scope_count(),
);
}
}
if crate::value::in_promise_eval() {
return Ok(Value::Null);
}
Err(EvalError::UndefinedVar(
format!("'{name}'{}", eval_file_ctx()),
))
}
}
}
};
}
ast::Expr::Literal(lit) => {
crate::perf::inc(crate::perf::Counter::EvalExpr);
if crate::perf::enabled() {
crate::perf::inc(crate::perf::Counter::ExprLiteral);
}
return eval_literal(lit);
}
ast::Expr::Paren(p) => {
if let Some(inner) = p.expr() {
return eval_expr(&inner, env);
}
}
ast::Expr::Root(r) => {
if let Some(inner) = r.expr() {
return eval_expr(&inner, env);
}
}
ast::Expr::Lambda(lam) => {
crate::perf::inc(crate::perf::Counter::EvalExpr);
if crate::perf::enabled() {
crate::perf::inc(crate::perf::Counter::ExprLambda);
}
if let (Some(param), Some(body)) = (lam.param(), lam.body()) {
return Ok(Value::Lambda(Rc::new(Closure {
param,
body,
env: env.clone(),
})));
}
}
_ => {}
}
stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || {
eval_expr_inner(expr, env)
})
}
fn eval_expr_inner(expr: &ast::Expr, env: &Env) -> Result<Value, EvalError> {
let mut cur_expr = expr.clone();
let mut cur_env = env.clone();
loop {
crate::perf::inc(crate::perf::Counter::EvalExpr);
if crate::perf::enabled() {
use crate::perf::Counter;
let c = match &cur_expr {
ast::Expr::Ident(_) => Counter::ExprIdent,
ast::Expr::Literal(_) => Counter::ExprLiteral,
ast::Expr::Str(_) => Counter::ExprStr,
ast::Expr::List(_) => Counter::ExprList,
ast::Expr::AttrSet(_) => Counter::ExprAttrs,
ast::Expr::Select(_) => Counter::ExprSelect,
ast::Expr::Apply(_) => Counter::ExprApply,
ast::Expr::LetIn(_) => Counter::ExprLetIn,
ast::Expr::IfElse(_) => Counter::ExprIfElse,
ast::Expr::With(_) => Counter::ExprWith,
ast::Expr::Lambda(_) => Counter::ExprLambda,
ast::Expr::BinOp(_) => Counter::ExprBinOp,
ast::Expr::HasAttr(_) => Counter::ExprHasAttr,
ast::Expr::UnaryOp(_) => Counter::ExprUnaryOp,
ast::Expr::Assert(_) => Counter::ExprAssert,
ast::Expr::PathAbs(_) | ast::Expr::PathRel(_)
| ast::Expr::PathHome(_) | ast::Expr::PathSearch(_) => Counter::ExprPath,
_ => Counter::ExprOther,
};
crate::perf::inc(c);
}
let _guard = DepthGuard::enter()?;
let env = &cur_env;
match &cur_expr {
ast::Expr::Literal(lit) => return eval_literal(lit),
ast::Expr::Str(s) => return eval_str(s, env),
ast::Expr::PathAbs(p) => {
let parts = p.parts();
if parts_have_interpolation(&parts) {
return eval_interpol_path_parts(&parts, PathKind::Abs, env);
}
let text = crate::path::canon_abs(&p.syntax().text().to_string());
return Ok(Value::Path(Box::new(SmolStr::from(text.as_str()))));
}
ast::Expr::PathRel(p) => {
let parts = p.parts();
if parts_have_interpolation(&parts) {
return eval_interpol_path_parts(&parts, PathKind::Rel, env);
}
let text = p.syntax().text().to_string();
let resolved = if let Some(dir) = current_eval_dir() {
let joined = dir.join(&text);
let norm = normalize_path(&joined);
crate::path::dematerialize(&norm)
.to_string_lossy()
.into_owned()
} else {
text.clone()
};
return Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))));
}
ast::Expr::PathHome(p) => {
let parts = p.parts();
if parts_have_interpolation(&parts) {
return eval_interpol_path_parts(&parts, PathKind::Home, env);
}
let text = p.syntax().text().to_string();
return Ok(Value::Path(Box::new(SmolStr::from(text.as_str()))));
}
ast::Expr::PathSearch(p) => {
let text = p.syntax().text().to_string();
let inner = text
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(&text);
if let Some(resolved) = crate::builtins::resolve_search_path(inner) {
return Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))));
}
return Err(EvalError::Throw(
format!("search path '{text}' not in NIX_PATH"),
));
}
ast::Expr::Ident(ident) => {
let name = ident_text(ident);
return match name.as_str() {
"true" => Ok(Value::Bool(true)),
"false" => Ok(Value::Bool(false)),
"null" => Ok(Value::Null),
_ => {
env.lookup(&name)
.ok_or_else(|| EvalError::UndefinedVar(
format!("'{name}'{}", eval_file_ctx()),
))
}
};
}
ast::Expr::List(list) => {
let values: Vec<Value> = list.items()
.map(|e| maybe_thunk(&e, env, false, None))
.collect();
return Ok(Value::list(values));
}
ast::Expr::AttrSet(set) => return eval_attrset(set, env),
ast::Expr::Select(sel) => return eval_select(sel, env),
ast::Expr::HasAttr(ha) => return eval_has_attr(ha, env),
ast::Expr::UnaryOp(op) => return eval_unary_op(op, env),
ast::Expr::BinOp(binop) => {
let lhs_expr = binop
.lhs()
.ok_or_else(|| EvalError::ParseError("binop missing lhs".to_string()))?;
let rhs_expr = binop
.rhs()
.ok_or_else(|| EvalError::ParseError("binop missing rhs".to_string()))?;
let kind = binop
.operator()
.ok_or_else(|| EvalError::ParseError("binop missing operator".to_string()))?;
return eval_binop(kind, &lhs_expr, &rhs_expr, env);
}
ast::Expr::Apply(app) => return eval_apply(app, env),
ast::Expr::IfElse(ie) => {
let cond = ie
.condition()
.ok_or_else(|| EvalError::ParseError("if missing condition".to_string()))?;
let body = ie
.body()
.ok_or_else(|| EvalError::ParseError("if missing then body".to_string()))?;
let else_body = ie
.else_body()
.ok_or_else(|| EvalError::ParseError("if missing else body".to_string()))?;
if force_concrete(&eval_expr(&cond, env)?)?.as_bool()? {
cur_expr = body;
} else {
cur_expr = else_body;
}
continue;
}
ast::Expr::Assert(assert) => {
let cond = assert
.condition()
.ok_or_else(|| EvalError::ParseError("assert missing condition".to_string()))?;
let body = assert
.body()
.ok_or_else(|| EvalError::ParseError("assert missing body".to_string()))?;
if !force_concrete(&eval_expr(&cond, env)?)?.as_bool()? {
return Err(EvalError::AssertionFailed(eval_file_ctx()));
}
cur_expr = body;
continue;
}
ast::Expr::With(with) => {
let ns = with
.namespace()
.ok_or_else(|| EvalError::ParseError("with missing namespace".to_string()))?;
let body = with
.body()
.ok_or_else(|| EvalError::ParseError("with missing body".to_string()))?;
let scope_val = maybe_thunk(&ns, env, false, None);
let new_env = env.child().with_scope(scope_val);
cur_expr = body;
cur_env = new_env;
continue;
}
ast::Expr::LetIn(letin) => {
let mut new_env = env.child();
let mut thunks: Vec<(String, Thunk)> = Vec::new();
let mut defined_so_far: HashSet<String> = HashSet::new();
let mut dotted_attrs: NixAttrs = NixAttrs::new();
let mut names_complete = true;
let let_scope_names: HashSet<String> = {
let mut s = HashSet::new();
for entry in letin.entries() {
match entry {
ast::Entry::AttrpathValue(apv) => {
if let Some(attrpath) = apv.attrpath() {
if let Some(first) = attrpath.attrs().next() {
if let ast::Attr::Dynamic(_) = &first {
names_complete = false;
}
if let Ok(name) = eval_attr(&first, env) {
s.insert(name);
} else {
names_complete = false;
}
} else {
names_complete = false;
}
} else {
names_complete = false;
}
}
ast::Entry::Inherit(inherit) => {
for attr in inherit.attrs() {
if let ast::Attr::Dynamic(_) = &attr {
names_complete = false;
}
if let Ok(name) = eval_attr(&attr, env) {
s.insert(name);
} else {
names_complete = false;
}
}
}
}
}
s
};
let narrow = scope_narrow_enabled() && names_complete;
let cluster = narrow && scope_cluster_enabled();
let mut all_bound: Vec<(String, Value)> = Vec::new();
let mut pinned_names: HashSet<String> = HashSet::new();
let mut pinned_refs: Vec<HashSet<SmolStr>> = Vec::new();
let mut has_dotted = false;
for entry in letin.entries() {
match entry {
ast::Entry::AttrpathValue(ref apv) => {
let attrpath = apv.attrpath().ok_or_else(|| {
EvalError::ParseError("binding missing attrpath".to_string())
})?;
let value_expr = apv.value().ok_or_else(|| {
EvalError::ParseError("binding missing value".to_string())
})?;
let mut path_keys: Vec<String> = attrpath
.attrs()
.map(|a| eval_attr(&a, env))
.collect::<Result<_, _>>()?;
if path_keys.len() == 1 {
let key = path_keys.pop().unwrap();
let referenced = referenced_idents(&value_expr);
let in_mutual_cycle = std::iter::once(&key)
.chain(let_scope_names.iter())
.any(|n| referenced.contains(n.as_str()));
let value = if in_mutual_cycle {
Value::Thunk(Thunk::new_suspended_recursive(
value_expr.clone(),
env.clone(),
))
} else {
maybe_thunk(&value_expr, env, true, Some(&defined_so_far))
};
new_env.bind(key.clone(), value.clone());
if cluster {
all_bound.push((key.clone(), value.clone()));
}
if let Value::Thunk(t) = &value {
if in_mutual_cycle || !narrow {
thunks.push((key.clone(), t.clone()));
if cluster {
pinned_names.insert(key.clone());
pinned_refs.push(referenced);
}
crate::value::census::scope_pinned();
} else {
crate::value::census::scope_narrowed();
}
}
defined_so_far.insert(key);
} else if path_keys.len() > 1 {
has_dotted = true;
let key = path_keys[0].clone();
let value = build_nested_attr_thunk(
&path_keys[1..],
&value_expr,
env,
&mut thunks,
);
merge_nested_insert(&mut dotted_attrs, key, value);
}
}
ast::Entry::Inherit(ref inherit) => {
if let Some(from) = inherit.from() {
let source_expr = from.expr().ok_or_else(|| {
EvalError::ParseError(
"inherit from missing expr".to_string(),
)
})?;
let source_refs: Option<HashSet<SmolStr>> = if narrow {
Some(referenced_idents(&source_expr))
} else {
None
};
let source_needs_scope = match &source_refs {
Some(refs) => let_scope_names
.iter()
.any(|n| refs.contains(n.as_str())),
None => true,
};
let source_thunk = Thunk::new_suspended(
source_expr, env.clone(),
);
for attr in inherit.attrs() {
let name = eval_attr(&attr, env)?;
let thunk = Thunk::new_inherit_select(
source_thunk.clone(),
name.clone(),
);
new_env.bind(name.clone(), Value::Thunk(thunk.clone()));
if cluster {
all_bound.push((
name.clone(),
Value::Thunk(thunk.clone()),
));
}
if source_needs_scope {
if cluster {
pinned_names.insert(name.clone());
}
thunks.push((name, thunk));
crate::value::census::scope_pinned();
} else {
crate::value::census::scope_narrowed();
}
}
if cluster
&& source_needs_scope
&& let Some(refs) = source_refs
{
pinned_refs.push(refs);
}
} else {
for attr in inherit.attrs() {
let name = eval_attr(&attr, env)?;
let value = env.lookup(&name).ok_or_else(|| {
EvalError::UndefinedVar(
format!("'{name}'{}", eval_file_ctx()),
)
})?;
if cluster {
all_bound.push((name.clone(), value.clone()));
}
new_env.bind(name, value);
}
}
}
}
}
for (key, value) in dotted_attrs.iter() {
new_env.bind(key.clone(), value.clone());
if cluster {
all_bound.push((key.clone(), value.clone()));
}
}
let fix_env: Option<Env> = if cluster && !has_dotted && !thunks.is_empty() {
let mut pin = pinned_names;
for refs in &pinned_refs {
for n in &let_scope_names {
if refs.contains(n.as_str()) {
pin.insert(n.clone());
}
}
}
if pin.len() < all_bound.len() {
let mut fe = env.child();
for (name, value) in &all_bound {
if pin.contains(name) {
fe.bind(name.clone(), value.clone());
}
}
Some(fe)
} else {
None
}
} else {
None
};
let phase2_env: &Env = fix_env.as_ref().unwrap_or(&new_env);
for (_key, thunk) in &thunks {
thunk.update_env(phase2_env);
}
let body = letin
.body()
.ok_or_else(|| EvalError::ParseError("let missing body".to_string()))?;
cur_expr = body;
cur_env = new_env;
continue;
}
ast::Expr::Lambda(lam) => {
let param = lam
.param()
.ok_or_else(|| EvalError::ParseError("lambda missing param".to_string()))?;
let body = lam
.body()
.ok_or_else(|| EvalError::ParseError("lambda missing body".to_string()))?;
return Ok(Value::Lambda(Rc::new(Closure {
param,
body,
env: env.clone(),
})));
}
ast::Expr::Paren(p) => {
let inner = p
.expr()
.ok_or_else(|| EvalError::ParseError("paren missing expr".to_string()))?;
cur_expr = inner;
continue;
}
ast::Expr::Root(r) => {
let inner = r
.expr()
.ok_or_else(|| EvalError::ParseError("root missing expr".to_string()))?;
cur_expr = inner;
continue;
}
ast::Expr::LegacyLet(ll) => {
let mut new_env = env.child();
eval_entries(ll, &mut new_env)?;
return new_env
.lookup("body")
.ok_or_else(|| EvalError::AttrNotFound(
format!("'body' in legacy let{}", eval_file_ctx()),
));
}
ast::Expr::CurPos(_) => return Err(EvalError::NotImplemented("__curPos".to_string())),
ast::Expr::Error(_) => return Err(EvalError::ParseError("parse error node".to_string())),
} } }
fn eval_literal(lit: &ast::Literal) -> Result<Value, EvalError> {
use ast::LiteralKind;
match lit.kind() {
LiteralKind::Integer(tok) => {
let n = tok
.value()
.map_err(|e| EvalError::ParseError(format!("invalid integer: {e}")))?;
Ok(Value::Int(n))
}
LiteralKind::Float(tok) => {
let f = tok
.value()
.map_err(|e| EvalError::ParseError(format!("invalid float: {e}")))?;
Ok(Value::Float(f))
}
LiteralKind::Uri(tok) => Ok(Value::string(tok.syntax().text().to_string())),
}
}
enum TraverseResult {
Found(Value),
Missing(String),
NotAttrs(Value),
}
fn traverse_attrpath(
base: Value,
attrpath: &rnix::ast::Attrpath,
env: &Env,
) -> Result<TraverseResult, EvalError> {
let attrs: Vec<_> = attrpath.attrs().collect();
let mut value = base;
for (i, attr) in attrs.iter().enumerate() {
let key = eval_attr(attr, env)?;
let forced = force_value(&value)?;
match forced {
Value::Attrs(ref a) => match a.get(&key) {
Some(v) => {
if i < attrs.len() - 1 {
value = force_value(v)?;
} else {
value = v.clone();
}
}
None => return Ok(TraverseResult::Missing(key)),
},
_ => return Ok(TraverseResult::NotAttrs(forced)),
}
}
Ok(TraverseResult::Found(value))
}
fn eval_select(sel: &ast::Select, env: &Env) -> Result<Value, EvalError> {
crate::perf::inc(crate::perf::Counter::Select);
let base_expr = sel.expr().ok_or_else(|| {
EvalError::ParseError("select missing expression".to_string())
})?;
let base_result = eval_expr(&base_expr, env)
.and_then(|v| force_concrete(&v).map(Concrete::into_value));
let base = match base_result {
Ok(v) => v,
Err(EvalError::InfiniteRecursion(_)) if sel.default_expr().is_some() => {
return eval_expr(&sel.default_expr().expect("checked"), env);
}
Err(e) => return Err(e),
};
let base_type = base.type_name();
let attrpath = sel.attrpath().ok_or_else(|| {
EvalError::ParseError("select missing attrpath".to_string())
})?;
let bridge_active = std::env::var_os("SUI_BLACKHOLE_AS_EMPTY_ATTRS").is_some()
|| std::env::var_os("SUI_BLACKHOLE_AS_NULL").is_some();
let traversal = traverse_attrpath(base, &attrpath, env);
match traversal {
Ok(TraverseResult::Found(v)) => Ok(v),
Ok(TraverseResult::Missing(key)) => {
if let Some(def) = sel.default_expr() {
eval_expr(&def, env)
} else if bridge_active {
if std::env::var_os("SUI_M26_SELTRACE").is_some() {
let path: Vec<String> = sel.attrpath().map(|ap|
ap.attrs().map(|a| a.syntax().text().to_string()).collect()
).unwrap_or_default();
eprintln!("[M26 SEL-MISS→null] base_type={base_type} path={path:?} missing-key={key}{}", eval_file_ctx());
}
if let Ok(filt) = std::env::var("SUI_M26_HARDSOFTEN") {
let path: Vec<String> = sel.attrpath().map(|ap|
ap.attrs().map(|a| a.syntax().text().to_string()).collect()
).unwrap_or_default();
if path.iter().any(|p| p.contains(&filt)) {
return Err(EvalError::type_error(format!(
"M26-HARDSOFTEN path={path:?} key={key}"
)));
}
}
Ok(Value::Null)
} else {
Err(EvalError::AttrNotFound(
format!("'{key}'{}", eval_file_ctx()),
))
}
}
Ok(TraverseResult::NotAttrs(forced)) => {
if let Some(def) = sel.default_expr() {
eval_expr(&def, env)
} else if bridge_active {
if let Ok(filt) = std::env::var("SUI_M26_HARDSOFTEN") {
let path: Vec<String> = sel.attrpath().map(|ap|
ap.attrs().map(|a| a.syntax().text().to_string()).collect()
).unwrap_or_default();
if path.iter().any(|p| p.contains(&filt)) {
return Err(EvalError::type_error(format!(
"M26-HARDSOFTEN-NOTATTRS path={path:?} base_type={base_type}"
)));
}
}
return Ok(Value::Null);
} else {
if std::env::var("SUI_DEBUG_SELECT").is_ok() {
let path: Vec<String> = sel.attrpath().map(|ap|
ap.attrs().filter_map(|a| match a {
ast::Attr::Ident(i) => Some(i.to_string()),
ast::Attr::Str(s) => Some(format!("\"{}\"", s.syntax().text())),
ast::Attr::Dynamic(_) => Some("<dyn>".into()),
}).collect()
).unwrap_or_default();
let dbg = format!("{:?}", forced);
let truncated = if dbg.len() > 200 { format!("{}…", &dbg[..200]) } else { dbg };
eprintln!("[SUI_DEBUG_SELECT] base_type={base_type} path={path:?} base={truncated}{}", eval_file_ctx());
}
Err(attach_trace(EvalError::type_error(
format!("cannot select from {base_type}"),
)))
}
}
Err(EvalError::InfiniteRecursion(_)) if sel.default_expr().is_some() => {
eval_expr(&sel.default_expr().expect("checked"), env)
}
Err(e) => Err(e),
}
}
fn eval_has_attr(ha: &ast::HasAttr, env: &Env) -> Result<Value, EvalError> {
let base_expr = ha.expr().ok_or_else(|| {
EvalError::ParseError("hasattr missing expression".to_string())
})?;
let base = force_concrete(&eval_expr(&base_expr, env)?)?.into_value();
let attrpath = ha.attrpath().ok_or_else(|| {
EvalError::ParseError("hasattr missing attrpath".to_string())
})?;
match traverse_attrpath(base, &attrpath, env)? {
TraverseResult::Found(_) => Ok(Value::Bool(true)),
TraverseResult::Missing(_) | TraverseResult::NotAttrs(_) => Ok(Value::Bool(false)),
}
}
fn eval_unary_op(op: &ast::UnaryOp, env: &Env) -> Result<Value, EvalError> {
let inner = op
.expr()
.ok_or_else(|| EvalError::ParseError("unary op missing expr".to_string()))?;
let val = force_value(&eval_expr(&inner, env)?)?;
let kind = op
.operator()
.ok_or_else(|| EvalError::ParseError("unary op missing operator".to_string()))?;
match kind {
ast::UnaryOpKind::Negate => match val {
Value::Int(n) => Ok(Value::Int(-n)),
Value::Float(f) => Ok(Value::Float(-f)),
_ => Err(EvalError::type_error(
format!("cannot negate {}", val.type_name()),
)),
},
ast::UnaryOpKind::Invert => Ok(Value::Bool(!val.as_bool()?)),
}
}
#[inline]
pub(crate) fn builtin_takes_lazy_arg(name: &str) -> bool {
matches!(
name,
"tryEval" | "addErrorContext<partial>" | "seq<partial>" | "deepSeq<partial>" | "foldl'<p1>"
)
}
fn eval_apply(app: &ast::Apply, env: &Env) -> Result<Value, EvalError> {
let func_expr = app
.lambda()
.ok_or_else(|| EvalError::ParseError("apply missing function".to_string()))?;
let arg_expr = app
.argument()
.ok_or_else(|| EvalError::ParseError("apply missing argument".to_string()))?;
let func = force_value(&eval_expr(&func_expr, env)?)?;
let arg = match &func {
Value::Lambda(_) => {
if let Some(v) = eval_pure_constant_arg(&arg_expr) {
v
} else {
crate::perf::inc(crate::perf::Counter::ThunkSiteApplyArg);
Value::Thunk(Thunk::new_suspended(arg_expr.clone(), env.clone()))
}
}
Value::Builtin(b) if builtin_takes_lazy_arg(&b.name) => {
crate::perf::inc(crate::perf::Counter::ThunkSiteApplyArg);
Value::Thunk(Thunk::new_suspended(arg_expr.clone(), env.clone()))
}
_ => eval_expr(&arg_expr, env)?,
};
apply(func, arg)
}
fn eval_pure_constant_arg(arg_expr: &ast::Expr) -> Option<Value> {
match arg_expr {
ast::Expr::Literal(lit) => eval_literal(lit).ok(),
ast::Expr::Str(st) if !str_has_interpolation(st) => {
eval_str(st, &Env::new()).ok()
}
ast::Expr::PathAbs(p) if !parts_have_interpolation(&p.parts()) => {
let text = crate::path::canon_abs(&p.syntax().text().to_string());
Some(Value::Path(Box::new(SmolStr::from(text.as_str()))))
}
ast::Expr::PathHome(p) if !parts_have_interpolation(&p.parts()) => {
let text = p.syntax().text().to_string();
Some(Value::Path(Box::new(SmolStr::from(text.as_str()))))
}
_ => None,
}
}
fn eval_str(s: &ast::Str, env: &Env) -> Result<Value, EvalError> {
let mut result = String::new();
let mut ctx = StringContext::new();
for part in s.normalized_parts() {
match part {
InterpolPart::Literal(text) => result.push_str(&text),
InterpolPart::Interpolation(interpol) => {
let expr = interpol.expr().ok_or_else(|| {
EvalError::ParseError("interpolation missing expr".to_string())
})?;
let val = force_value(&eval_expr(&expr, env)?)?;
let (s, c) = val.coerce_to_string_copy_to_store()?;
result.push_str(&s);
ctx.merge(&c);
}
}
}
Ok(Value::String(Rc::new(NixString::with_context(result, ctx))))
}
fn parts_have_interpolation(parts: &[InterpolPart<rnix::ast::PathContent>]) -> bool {
parts
.iter()
.any(|p| matches!(p, InterpolPart::Interpolation(_)))
}
fn str_has_interpolation(s: &ast::Str) -> bool {
s.normalized_parts()
.iter()
.any(|p| matches!(p, InterpolPart::Interpolation(_)))
}
fn eval_interpol_path_parts(
parts: &[InterpolPart<rnix::ast::PathContent>],
kind: PathKind,
env: &Env,
) -> Result<Value, EvalError> {
let mut text = String::new();
for part in parts {
match part {
InterpolPart::Literal(content) => text.push_str(content.text()),
InterpolPart::Interpolation(interpol) => {
let expr = interpol.expr().ok_or_else(|| {
EvalError::ParseError("path interpolation missing expr".to_string())
})?;
let val = force_value(&eval_expr(&expr, env)?)?;
let (s, _ctx) = val.coerce_to_string()?;
text.push_str(&s);
}
}
}
let resolved = match kind {
PathKind::Rel => {
if let Some(dir) = current_eval_dir() {
let norm = normalize_path(&dir.join(&text));
crate::path::dematerialize(&norm).to_string_lossy().into_owned()
} else {
text
}
}
PathKind::Abs => crate::path::canon_abs(&text),
PathKind::Home => normalize_path(std::path::Path::new(&text))
.to_string_lossy()
.into_owned(),
};
Ok(Value::Path(Box::new(SmolStr::from(resolved.as_str()))))
}
#[derive(Clone, Copy)]
enum PathKind {
Abs,
Rel,
Home,
}
fn eval_attr(attr: &ast::Attr, env: &Env) -> Result<String, EvalError> {
eval_attr_maybe_null(attr, env)?
.ok_or_else(|| EvalError::TypeError("null dynamic attribute name".into()))
}
fn eval_attr_maybe_null(attr: &ast::Attr, env: &Env) -> Result<Option<String>, EvalError> {
match attr {
ast::Attr::Ident(ident) => Ok(Some(ident_text(ident))),
ast::Attr::Dynamic(dyn_) => {
let expr = dyn_
.expr()
.ok_or_else(|| EvalError::ParseError("dynamic attr missing expr".to_string()))?;
let val = force_value(&eval_expr(&expr, env)?)?;
if val == Value::Null {
return Ok(None);
}
Ok(Some(val.as_string()?.to_string()))
}
ast::Attr::Str(s) => {
let val = eval_str(s, env)?;
Ok(Some(val.as_string()?.to_string()))
}
}
}
fn ident_text(ident: &ast::Ident) -> String {
match ident.ident_token() {
Some(tok) => tok.text().to_string(),
None => ident.syntax().text().to_string(),
}
}
fn static_attr_offset(attr: &ast::Attr) -> Option<u32> {
let node = match attr {
ast::Attr::Ident(i) => i.syntax(),
ast::Attr::Str(s) => s.syntax(),
ast::Attr::Dynamic(_) => return None,
};
Some(u32::from(node.text_range().start()))
}
fn attach_attrset_positions(set: &ast::AttrSet, attrs: &mut NixAttrs, env: &Env) {
let mut table = crate::pos::AttrPositions::new(current_eval_file());
for entry in set.entries() {
if let ast::Entry::AttrpathValue(apv) = entry {
let Some(attrpath) = apv.attrpath() else { continue };
let path_attrs: Vec<ast::Attr> = attrpath.attrs().collect();
let Some(head) = path_attrs.first() else { continue };
let Some(offset) = static_attr_offset(head) else { continue };
if let Ok(Some(name)) = eval_attr_maybe_null(&path_attrs[0], env) {
table.insert(intern(&name), offset);
}
} else if let ast::Entry::Inherit(inh) = entry {
for attr in inh.attrs() {
let Some(offset) = static_attr_offset(&attr) else { continue };
if let Ok(Some(name)) = eval_attr_maybe_null(&attr, env) {
table.insert(intern(&name), offset);
}
}
}
}
if !table.is_empty() {
attrs.set_positions(std::rc::Rc::new(table));
}
}
fn eval_attrset(set: &ast::AttrSet, env: &Env) -> Result<Value, EvalError> {
crate::perf::inc(crate::perf::Counter::Attrset);
let mut attrs = NixAttrs::new();
let is_rec = set.rec_token().is_some();
if is_rec {
let mut rec_env = env.child();
let mut thunks: Vec<(String, Thunk)> = Vec::new();
let mut defined_so_far: HashSet<String> = HashSet::new();
let mut dotted_attrs: NixAttrs = NixAttrs::new();
let mut names_complete = scope_narrow_enabled();
let rec_scope_names: HashSet<String> = if names_complete {
let mut s = HashSet::new();
for entry in set.entries() {
match entry {
ast::Entry::AttrpathValue(apv) => {
match apv.attrpath().and_then(|p| p.attrs().next()) {
Some(ast::Attr::Ident(i)) => {
s.insert(ident_text(&i));
}
_ => names_complete = false,
}
}
ast::Entry::Inherit(inh) => {
for attr in inh.attrs() {
match attr {
ast::Attr::Ident(i) => {
s.insert(ident_text(&i));
}
_ => names_complete = false,
}
}
}
}
}
s
} else {
HashSet::new()
};
let narrow = names_complete;
for entry in set.entries() {
match entry {
ast::Entry::AttrpathValue(apv) => {
let attrpath = apv.attrpath().ok_or_else(|| {
EvalError::ParseError("binding missing attrpath".to_string())
})?;
let value_expr = apv.value().ok_or_else(|| {
EvalError::ParseError("binding missing value".to_string())
})?;
let mut path_keys: Vec<String> = attrpath
.attrs()
.filter_map(|a| eval_attr_maybe_null(&a, env).transpose())
.collect::<Result<_, _>>()?;
if path_keys.is_empty() { continue; }
if path_keys.len() == 1 {
let key = path_keys.pop().unwrap();
let referenced = referenced_idents(&value_expr);
let is_recursive_binding = referenced.contains(key.as_str())
|| defined_so_far
.iter()
.any(|n| referenced.contains(n.as_str()));
let value = if is_recursive_binding {
Value::Thunk(Thunk::new_suspended_recursive(
value_expr.clone(),
env.clone(),
))
} else {
maybe_thunk(&value_expr, env, true, Some(&defined_so_far))
};
let needs_scope = !narrow
|| is_recursive_binding
|| rec_scope_names
.iter()
.any(|n| referenced.contains(n.as_str()));
rec_env.bind(key.clone(), value.clone());
attrs.insert(key.clone(), value.clone());
if let Value::Thunk(t) = &value {
if needs_scope {
thunks.push((key.clone(), t.clone()));
crate::value::census::scope_pinned();
} else {
crate::value::census::scope_narrowed();
}
}
defined_so_far.insert(key);
} else {
let key = path_keys[0].clone();
let value =
build_nested_attr_thunk(&path_keys[1..], &value_expr, env, &mut thunks);
merge_nested_insert(&mut dotted_attrs, key, value);
}
}
ast::Entry::Inherit(inherit) => {
eval_inherit(&inherit, env, &mut attrs, Some(&mut rec_env), Some(&mut thunks))?;
}
}
}
for (key, value) in dotted_attrs.iter() {
attrs.insert(key.clone(), value.clone());
rec_env.bind(key.clone(), value.clone());
}
for (_key, thunk) in &thunks {
thunk.update_env(&rec_env);
}
} else {
for entry in set.entries() {
match entry {
ast::Entry::AttrpathValue(apv) => {
let attrpath = apv.attrpath().ok_or_else(|| {
EvalError::ParseError("binding missing attrpath".to_string())
})?;
let value_expr = apv.value().ok_or_else(|| {
EvalError::ParseError("binding missing value".to_string())
})?;
let path_attrs: Vec<ast::Attr> = attrpath.attrs().collect();
let tail_is_dynamic =
path_attrs.len() > 1 && attrs_have_dynamic(&path_attrs[1..]);
let head_key = match eval_attr_maybe_null(&path_attrs[0], env)? {
Some(k) => k,
None => continue,
};
if tail_is_dynamic && attrs.get(&head_key).is_none() {
let value =
build_deferred_tail_attr(&path_attrs[1..], &value_expr, env);
attrs.insert(head_key, value);
continue;
}
if tail_is_dynamic {
if let Some(existing) = attrs.get(&head_key).cloned() {
let merged = merge_deferred_dynamic_tail(
existing,
&path_attrs[1..],
&value_expr,
env,
)?;
attrs.insert(head_key, merged);
continue;
}
}
let mut path_keys: Vec<String> = {
let mut v = Vec::with_capacity(path_attrs.len());
v.push(head_key);
let mut skip = false;
for a in &path_attrs[1..] {
match eval_attr_maybe_null(a, env)? {
Some(k) => v.push(k),
None => { skip = true; break; }
}
}
if skip { v.clear(); }
v
};
if path_keys.is_empty() { continue; }
if path_keys.len() == 1 {
let key = path_keys.pop().unwrap();
let value = maybe_thunk(&value_expr, env, false, None);
if matches!(attrs.get(&key), Some(Value::Thunk(_))) {
let existing = attrs.get(&key).cloned().unwrap();
let forced_existing = force_value(&existing)?;
attrs.insert(key.clone(), forced_existing);
}
if matches!(attrs.get(&key), Some(Value::Attrs(_))) {
let forced = force_value(&value)?;
merge_nested_insert(&mut attrs, key, forced);
} else {
attrs.insert(key, value);
}
} else {
let key = path_keys[0].clone();
let value = build_nested_attr(&path_keys[1..], &value_expr, env)?;
if matches!(attrs.get(&key), Some(Value::Thunk(_))) {
let existing = attrs.get(&key).cloned().unwrap();
let forced = force_value(&existing)?;
attrs.insert(key.clone(), forced);
}
merge_nested_insert(&mut attrs, key, value);
}
}
ast::Entry::Inherit(inherit) => {
eval_inherit(&inherit, env, &mut attrs, None, None)?;
}
}
}
}
attach_attrset_positions(set, &mut attrs, env);
Ok(Value::Attrs(Rc::new(attrs)))
}
fn eval_inherit(
inherit: &ast::Inherit,
env: &Env,
attrs: &mut NixAttrs,
bind_env: Option<&mut Env>,
mut thunks: Option<&mut Vec<(String, Thunk)>>,
) -> Result<(), EvalError> {
if let Some(from) = inherit.from() {
let source_expr = from
.expr()
.ok_or_else(|| EvalError::ParseError("inherit from missing expr".to_string()))?;
let source_thunk = Thunk::new_suspended(source_expr, env.clone());
let mut be = bind_env;
for attr in inherit.attrs() {
let name = eval_attr(&attr, env)?;
let thunk = Thunk::new_inherit_select(source_thunk.clone(), name.clone());
let value = Value::Thunk(thunk.clone());
attrs.insert(name.clone(), value.clone());
if let Some(ref mut e) = be {
e.bind(name.clone(), value);
}
if let Some(ref mut t) = thunks {
t.push((name, thunk));
}
}
} else {
let mut be = bind_env;
for attr in inherit.attrs() {
let name = eval_attr(&attr, env)?;
let sym = crate::value::intern(&name);
let value = if let Some(v) = env.lookup_fast(sym, &name) {
v
} else if let Some((scope_cache, scope_value)) =
env.innermost_with_scope()
{
Value::Thunk(Thunk::new_with_ident(
SmolStr::from(name.as_str()),
scope_cache,
scope_value,
env.clone(),
))
} else {
return Err(EvalError::UndefinedVar(format!(
"'{name}'{}",
eval_file_ctx()
)));
};
attrs.insert(name.clone(), value.clone());
if let Some(ref mut e) = be {
e.bind(name, value);
}
}
}
Ok(())
}
fn build_nested_attr(
path: &[String],
expr: &ast::Expr,
env: &Env,
) -> Result<Value, EvalError> {
if path.is_empty() {
return Ok(maybe_thunk(expr, env, false, None));
}
let key = path[0].clone();
let inner = build_nested_attr(&path[1..], expr, env)?;
let mut attrs = NixAttrs::new();
attrs.insert(key, inner);
Ok(Value::Attrs(Rc::new(attrs)))
}
fn attr_is_dynamic(attr: &ast::Attr) -> bool {
match attr {
ast::Attr::Dynamic(_) => true,
ast::Attr::Str(s) => s
.normalized_parts()
.iter()
.any(|p| matches!(p, InterpolPart::Interpolation(_))),
ast::Attr::Ident(_) => false,
}
}
fn attrs_have_dynamic(attrs: &[ast::Attr]) -> bool {
attrs.iter().any(attr_is_dynamic)
}
fn build_deferred_tail_attr(
tail: &[ast::Attr],
value_expr: &ast::Expr,
env: &Env,
) -> Value {
let tail: Vec<ast::Attr> = tail.to_vec();
let value_expr = value_expr.clone();
let env = env.clone();
Value::Thunk(Thunk::new_native(move || {
build_tail_attrs_now(&tail, &value_expr, &env)
}))
}
fn build_tail_attrs_now(
tail: &[ast::Attr],
value_expr: &ast::Expr,
env: &Env,
) -> Result<Value, EvalError> {
if tail.is_empty() {
return Ok(maybe_thunk(value_expr, env, false, None));
}
if std::env::var_os("SUI_M26_TAILTRACE").is_some() {
let t: String = tail[0].syntax().text().to_string().chars().take(40).collect();
eprintln!("[M26 TAIL-RESOLVE] forcing dynamic tail key `{t}`");
if attrs_have_dynamic(&tail[..1]) {
crate::trace::dump_force_stack_ids();
}
}
let key = match eval_attr_maybe_null(&tail[0], env)? {
Some(k) => k,
None => return Ok(Value::Attrs(Rc::new(NixAttrs::new()))),
};
let inner = if tail.len() == 1 {
maybe_thunk(value_expr, env, false, None)
} else {
build_deferred_tail_attr(&tail[1..], value_expr, env)
};
let mut attrs = NixAttrs::new();
attrs.insert(key, inner);
Ok(Value::Attrs(Rc::new(attrs)))
}
fn merge_deferred_dynamic_tail(
existing: Value,
tail: &[ast::Attr],
value_expr: &ast::Expr,
env: &Env,
) -> Result<Value, EvalError> {
debug_assert!(!tail.is_empty());
if attr_is_dynamic(&tail[0]) {
let deferred = build_deferred_tail_attr(tail, value_expr, env);
return Ok(lazy_overlay_merge(existing, deferred));
}
let key = match eval_attr_maybe_null(&tail[0], env)? {
Some(k) => k,
None => return Ok(existing),
};
let existing_forced = force_value(&existing)?;
let mut base = match existing_forced {
Value::Attrs(a) => (*a).clone(),
_ => {
let deferred = build_deferred_tail_attr(tail, value_expr, env);
return Ok(deferred);
}
};
let child_existing = base.get(&key).cloned();
let new_child = match child_existing {
Some(child) if tail.len() > 1 => {
merge_deferred_dynamic_tail(child, &tail[1..], value_expr, env)?
}
Some(child) => {
let leaf = maybe_thunk(value_expr, env, false, None);
lazy_overlay_merge(child, leaf)
}
None if tail.len() > 1 => {
build_deferred_tail_attr(&tail[1..], value_expr, env)
}
None => maybe_thunk(value_expr, env, false, None),
};
base.insert(key, new_child);
Ok(Value::Attrs(Rc::new(base)))
}
fn lazy_overlay_merge(left: Value, right: Value) -> Value {
match (&left, &right) {
(Value::Attrs(la), Value::Attrs(_)) => {
crate::perf::inc(crate::perf::Counter::SlashDeferredTailClone);
let mut merged = (**la).clone();
if let Value::Attrs(ra) = &right {
for (k, v) in ra.iter_unsorted() {
merge_nested_insert(&mut merged, k.clone(), v.clone());
}
}
Value::Attrs(Rc::new(merged))
}
_ => {
Value::Thunk(Thunk::new_native(move || {
let lf = force_value(&left)?;
let rf = force_value(&right)?;
let la = lf.as_attrs()?;
let ra = rf.as_attrs()?;
crate::perf::inc(crate::perf::Counter::SlashDeferredTailClone);
let mut merged = (*la).clone();
for (k, v) in ra.iter_unsorted() {
merge_nested_insert(&mut merged, k.clone(), v.clone());
}
Ok(Value::Attrs(Rc::new(merged)))
}))
}
}
}
fn build_nested_attr_thunk(
path: &[String],
expr: &ast::Expr,
env: &Env,
thunks: &mut Vec<(String, Thunk)>,
) -> Value {
if path.is_empty() {
let thunk = Thunk::new_suspended(expr.clone(), env.clone());
let val = Value::Thunk(thunk.clone());
thunks.push((String::new(), thunk));
return val;
}
let key = path[0].clone();
let inner = build_nested_attr_thunk(&path[1..], expr, env, thunks);
let mut attrs = NixAttrs::new();
attrs.insert(key, inner);
Value::Attrs(Rc::new(attrs))
}
fn merge_nested_insert(target: &mut NixAttrs, key: String, value: Value) {
let existing = match target.get(&key) {
Some(e) => e.clone(),
None => {
target.insert(key, value);
return;
}
};
let value = match value {
Value::Thunk(_) => match force_value(&value) {
Ok(v @ Value::Attrs(_)) => v,
_ => value,
},
other => other,
};
if !matches!(value, Value::Attrs(_)) {
target.insert(key, value);
return;
}
let existing_concrete = match &existing {
Value::Attrs(_) => existing.clone(),
Value::Thunk(_) => match force_value(&existing) {
Ok(v @ Value::Attrs(_)) => v,
_ => {
target.insert(key, value);
return;
}
},
_ => {
target.insert(key, value);
return;
}
};
let mut existing_attrs = match existing_concrete {
Value::Attrs(a) => (*a).clone(),
_ => unreachable!(),
};
let new_attrs = match value {
Value::Attrs(ref a) => a,
_ => unreachable!(),
};
for (k, v) in new_attrs.iter_unsorted() {
merge_nested_insert(&mut existing_attrs, k.clone(), v.clone());
}
target.insert(key, Value::Attrs(Rc::new(existing_attrs)));
}
fn eval_entries<N: HasEntry + AstNode>(node: &N, env: &mut Env) -> Result<(), EvalError> {
for entry in node.entries() {
match entry {
ast::Entry::AttrpathValue(apv) => {
let attrpath = apv.attrpath().ok_or_else(|| {
EvalError::ParseError("binding missing attrpath".to_string())
})?;
let value_expr = apv.value().ok_or_else(|| {
EvalError::ParseError("binding missing value".to_string())
})?;
let mut path_keys: Vec<String> = attrpath
.attrs()
.map(|a| eval_attr(&a, env))
.collect::<Result<_, _>>()?;
if path_keys.len() == 1 {
let key = path_keys.pop().unwrap();
let value = eval_expr(&value_expr, env)?;
env.bind(key, value);
}
}
ast::Entry::Inherit(inherit) => {
if let Some(from) = inherit.from() {
let source_expr = from.expr().ok_or_else(|| {
EvalError::ParseError("inherit from missing expr".to_string())
})?;
let source = force_value(&eval_expr(&source_expr, env)?)?;
let source_attrs = source.as_attrs()?;
for attr in inherit.attrs() {
let name = eval_attr(&attr, env)?;
let value = source_attrs
.get(&name)
.cloned()
.ok_or_else(|| EvalError::AttrNotFound(
format!("'{name}' in inherit{}", eval_file_ctx()),
))?;
env.bind(name, value);
}
} else {
for attr in inherit.attrs() {
let name = eval_attr(&attr, env)?;
let value = env
.lookup(&name)
.ok_or_else(|| EvalError::UndefinedVar(
format!("'{name}'{}", eval_file_ctx()),
))?;
env.bind(name, value);
}
}
}
}
}
Ok(())
}
fn eval_binop(
op: ast::BinOpKind,
lhs: &ast::Expr,
rhs: &ast::Expr,
env: &Env,
) -> Result<Value, EvalError> {
match op {
ast::BinOpKind::And => {
let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
if !l {
return Ok(Value::Bool(false));
}
return eval_expr(rhs, env);
}
ast::BinOpKind::Or => {
let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
if l {
return Ok(Value::Bool(true));
}
return eval_expr(rhs, env);
}
ast::BinOpKind::Implication => {
let l = force_value(&eval_expr(lhs, env)?)?.as_bool()?;
if !l {
return Ok(Value::Bool(true));
}
return eval_expr(rhs, env);
}
_ => {}
}
let lc = force_concrete(&eval_expr(lhs, env)?)?;
let rc = force_concrete(&eval_expr(rhs, env)?)?;
let l = lc.into_value();
let r = rc.into_value();
match op {
ast::BinOpKind::Add => match (&l, &r) {
(Value::Int(a), Value::Int(b)) => a
.checked_add(*b)
.map(Value::Int)
.ok_or_else(|| int_overflow("adding", *a, '+', *b)),
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
(Value::Int(a), Value::Float(b)) => Ok(Value::Float(*a as f64 + b)),
(Value::Float(a), Value::Int(b)) => Ok(Value::Float(a + *b as f64)),
(Value::String(a), Value::String(b)) => {
let mut ctx = a.context.clone();
ctx.merge(&b.context);
let mut s = String::with_capacity(a.chars.len() + b.chars.len());
s.push_str(&a.chars);
s.push_str(&b.chars);
Ok(Value::String(Rc::new(NixString::with_context(s, ctx))))
}
(Value::Path(a), Value::String(b)) => Ok(Value::Path(Box::new(SmolStr::from(format!("{a}{}", b.chars).as_str())))),
(Value::Path(a), Value::Path(b)) => Ok(Value::Path(Box::new(SmolStr::from(format!("{a}/{b}").as_str())))),
(Value::Attrs(_), _) | (_, Value::Attrs(_)) => {
let (ls, lctx) = l.coerce_to_string()?;
let (rs, rctx) = r.coerce_to_string()?;
let mut ctx = lctx;
ctx.merge(&rctx);
Ok(Value::String(Rc::new(NixString::with_context(
format!("{ls}{rs}"),
ctx,
))))
}
_ => Err(EvalError::op_type("add", l.type_name(), r.type_name())),
},
ast::BinOpKind::Sub => num_op(
&l,
&r,
|a, b| a.checked_sub(b),
|a, b| a - b,
|a, b| int_overflow("subtracting", a, '-', b),
),
ast::BinOpKind::Mul => num_op(
&l,
&r,
|a, b| a.checked_mul(b),
|a, b| a * b,
|a, b| int_overflow("multiplying", a, '*', b),
),
ast::BinOpKind::Div => {
let rhs_is_zero = match &r {
Value::Int(0) => true,
Value::Float(f) => *f == 0.0,
_ => false,
};
if rhs_is_zero {
return Err(EvalError::DivisionByZero);
}
num_op(
&l,
&r,
|a, b| a.checked_div(b),
|a, b| a / b,
|a, b| int_overflow("dividing", a, '/', b),
)
}
ast::BinOpKind::Equal => Ok(Value::Bool(l == r)),
ast::BinOpKind::NotEqual => Ok(Value::Bool(l != r)),
ast::BinOpKind::Less => compare(&l, &r, |o| o == std::cmp::Ordering::Less),
ast::BinOpKind::LessOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Greater),
ast::BinOpKind::More => compare(&l, &r, |o| o == std::cmp::Ordering::Greater),
ast::BinOpKind::MoreOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Less),
ast::BinOpKind::Update => {
let la = l.to_attrs()?;
let ra = r.to_attrs()?;
Ok(Value::Attrs(Rc::new(la.overlay(ra))))
}
ast::BinOpKind::Concat => {
crate::value::concat_lists(l, r.as_list()?)
}
ast::BinOpKind::And | ast::BinOpKind::Or | ast::BinOpKind::Implication => {
unreachable!("handled above")
}
ast::BinOpKind::PipeRight | ast::BinOpKind::PipeLeft => {
Err(EvalError::NotImplemented("pipe operators".to_string()))
}
}
}
#[inline]
fn int_overflow(verb: &str, a: i64, sym: char, b: i64) -> EvalError {
EvalError::Abort(format!("integer overflow in {verb} {a} {sym} {b}"))
}
fn num_op(
l: &Value,
r: &Value,
int_op: impl Fn(i64, i64) -> Option<i64>,
float_op: impl Fn(f64, f64) -> f64,
overflow: impl Fn(i64, i64) -> EvalError,
) -> Result<Value, EvalError> {
match (l, r) {
(Value::Int(a), Value::Int(b)) => {
int_op(*a, *b).map(Value::Int).ok_or_else(|| overflow(*a, *b))
}
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(float_op(*a, *b))),
(Value::Int(a), Value::Float(b)) => Ok(Value::Float(float_op(*a as f64, *b))),
(Value::Float(a), Value::Int(b)) => Ok(Value::Float(float_op(*a, *b as f64))),
_ => Err(EvalError::op_type("perform arithmetic on", l.type_name(), r.type_name())),
}
}
fn compare(
l: &Value,
r: &Value,
pred: impl Fn(std::cmp::Ordering) -> bool,
) -> Result<Value, EvalError> {
let ord = match (l, r) {
(Value::Int(a), Value::Int(b)) => a.cmp(b),
(Value::Float(a), Value::Float(b)) => {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
}
(Value::Int(a), Value::Float(b)) => (*a as f64)
.partial_cmp(b)
.unwrap_or(std::cmp::Ordering::Equal),
(Value::Float(a), Value::Int(b)) => a
.partial_cmp(&(*b as f64))
.unwrap_or(std::cmp::Ordering::Equal),
(Value::String(a), Value::String(b)) => a.chars.cmp(&b.chars),
_ => {
return Err(EvalError::op_type("compare", l.type_name(), r.type_name()));
}
};
Ok(Value::Bool(pred(ord)))
}
pub fn apply_and_force(func: Value, arg: Value) -> Result<Value, EvalError> {
force_value(&apply(func, arg)?)
}
pub fn apply(func: Value, arg: Value) -> Result<Value, EvalError> {
stacker::maybe_grow(64 * 1024, 2 * 1024 * 1024, || apply_inner(func, arg))
}
fn apply_inner(func: Value, arg: Value) -> Result<Value, EvalError> {
crate::perf::inc(crate::perf::Counter::Apply);
let func = force_concrete(&func)?.into_value();
match func {
Value::Lambda(closure) => {
if crate::perf::enabled() {
APPLY_SITES.with(|sites| {
let file = closure.env.eval_file()
.map(|p| p.display().to_string())
.unwrap_or_else(|| "<eval>".into());
let param_name = match &closure.param {
rnix::ast::Param::IdentParam(ip) => ip.ident().map(|i| ident_text(&i)).unwrap_or_default(),
rnix::ast::Param::Pattern(pat) => {
let mut names: Vec<String> = pat.pat_entries()
.filter_map(|e| e.ident().map(|i| ident_text(&i)))
.take(3)
.collect();
if pat.pat_entries().count() > 3 { names.push("...".to_string()); }
format!("{{{}}}", names.join(","))
}
};
let key = format!("{}:{}", file.rsplit_once("-source/").map_or(file.as_str(), |(_,s)| s), param_name);
*sites.borrow_mut().entry(key).or_insert(0u64) += 1;
});
}
let mut call_env = closure.env.child();
let _file_guard = push_eval_frame(closure.env.eval_file().cloned());
let _trace = push_nix_trace_lambda(&closure.env);
match &closure.param {
rnix::ast::Param::IdentParam(_) => {
bind_param(&closure.param, &arg, &mut call_env)?;
}
rnix::ast::Param::Pattern(_) => {
let forced_arg = force_concrete(&arg)?.into_value();
bind_param(&closure.param, &forced_arg, &mut call_env)?;
}
}
eval_expr(&closure.body, &call_env)
}
Value::Builtin(b) => {
let _trace = push_nix_trace(format!("while calling the '{}' builtin", b.name));
if builtin_takes_lazy_arg(&b.name) {
(b.func)(&[arg])
} else {
let forced_arg = force_value(&arg)?;
(b.func)(&[forced_arg])
}
}
Value::Attrs(ref attrs) => {
if let Some(functor) = attrs.get("__functor") {
let functor = force_value(functor)?;
let partial = apply(functor, func.clone())?;
apply(partial, arg)
} else if crate::value::in_promise_eval() {
Ok(Value::Null)
} else {
Err(EvalError::type_error(
format!("cannot call {} (missing __functor){}", func.type_name(), eval_file_ctx()),
))
}
}
_ if crate::value::in_promise_eval() => {
Ok(Value::Null)
}
_ => Err(EvalError::type_error(
format!("cannot call {}{}", func.type_name(), eval_file_ctx()),
)),
}
}
static SUI_BATCH_BIND: std::sync::LazyLock<bool> =
std::sync::LazyLock::new(|| std::env::var_os("SUI_BATCH_BIND").is_some());
fn bind_param(param: &ast::Param, arg: &Value, env: &mut Env) -> Result<(), EvalError> {
match param {
ast::Param::IdentParam(ip) => {
let ident = ip
.ident()
.ok_or_else(|| EvalError::ParseError("ident param missing ident".to_string()))?;
let name = ident_text(&ident);
env.bind(name, arg.clone());
}
ast::Param::Pattern(pat) => {
let attrs = arg.as_attrs()?;
if let Some(pat_bind) = pat.pat_bind()
&& let Some(ident) = pat_bind.ident()
{
let name = ident_text(&ident);
env.bind(name, arg.clone());
}
let has_ellipsis = pat.ellipsis_token().is_some();
let entries: Vec<ast::PatEntry> = pat.pat_entries().collect();
let mut default_thunks: Vec<Thunk> = Vec::new();
let use_batch = *SUI_BATCH_BIND;
let mut pairs: Vec<(String, Value)> =
if use_batch { Vec::with_capacity(entries.len()) } else { Vec::new() };
let narrow = scope_narrow_enabled();
let default_names: HashSet<String> = if narrow {
entries
.iter()
.filter(|e| e.default().is_some())
.filter_map(ast::PatEntry::ident)
.map(|i| ident_text(&i))
.filter(|n| attrs.get(n).is_none())
.collect()
} else {
HashSet::new()
};
if narrow {
let mut deferred: Vec<(String, ast::Expr)> =
Vec::with_capacity(default_names.len());
for entry in &entries {
let ident = entry.ident().ok_or_else(|| {
EvalError::ParseError("pat entry missing ident".to_string())
})?;
let name = ident_text(&ident);
if let Some(v) = attrs.get(&name) {
env.bind(name, v.clone());
} else if let Some(default_expr) = entry.default() {
deferred.push((
name,
ast::Expr::cast(default_expr.syntax().clone()).unwrap(),
));
} else {
return Err(EvalError::type_error(
format!("missing argument '{name}'{}", eval_file_ctx()),
));
}
}
for (name, default_expr) in deferred {
let thunk =
Thunk::new_suspended(default_expr.clone(), env.clone());
let referenced = referenced_idents(&default_expr);
if default_names.iter().any(|n| referenced.contains(n.as_str())) {
default_thunks.push(thunk.clone());
crate::value::census::scope_pinned();
} else {
crate::value::census::scope_narrowed();
}
env.bind(name, Value::Thunk(thunk));
}
} else {
for entry in &entries {
let ident = entry.ident().ok_or_else(|| {
EvalError::ParseError("pat entry missing ident".to_string())
})?;
let name = ident_text(&ident);
let value = if let Some(v) = attrs.get(&name) {
v.clone()
} else if let Some(default_expr) = entry.default() {
let thunk = Thunk::new_suspended(
ast::Expr::cast(default_expr.syntax().clone()).unwrap(),
env.clone(),
);
default_thunks.push(thunk.clone());
Value::Thunk(thunk)
} else {
return Err(EvalError::type_error(
format!("missing argument '{name}'{}", eval_file_ctx()),
));
};
if use_batch {
pairs.push((name, value));
} else {
env.bind(name, value);
}
}
if use_batch {
env.bind_many(pairs);
}
}
for thunk in &default_thunks {
thunk.update_env(env);
}
if !has_ellipsis {
let entry_names: std::collections::HashSet<String> = entries
.iter()
.filter_map(|e| e.ident().map(|i| ident_text(&i)))
.collect();
for key in attrs.keys() {
if !entry_names.contains(key.as_str()) {
return Err(EvalError::type_error(
format!("unexpected argument '{key}'{}", eval_file_ctx()),
));
}
}
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
fn ev(input: &str) -> Value {
eval(input).unwrap()
}
#[test]
fn is_self_recursive_binding_ignores_attribute_names() {
fn expr(s: &str) -> ast::Expr {
rnix::Root::parse(s).tree().expr().expect("parse")
}
assert!(!is_self_recursive_binding(&expr("lhs.placeholder"), "placeholder"));
assert!(!is_self_recursive_binding(&expr("{ placeholder = 1; }"), "placeholder"));
assert!(!is_self_recursive_binding(
&expr("if lhs.placeholder == rhs.placeholder then lhs.placeholder else null"),
"placeholder",
));
assert!(is_self_recursive_binding(&expr("placeholder + 1"), "placeholder"));
assert!(is_self_recursive_binding(
&expr("if placeholder then 1 else 2"),
"placeholder"
));
}
#[test]
fn maybe_thunk_eager_constant_str_is_byte_identical() {
fn expr(s: &str) -> ast::Expr {
rnix::Root::parse(s).tree().expr().expect("parse")
}
let env = Env::new();
let v = maybe_thunk(&expr(r#""abc""#), &env, false, None);
assert!(matches!(v, Value::String(_)), "constant str should be eager, got {v:?}");
assert_eq!(force_value(&v).unwrap(), Value::string("abc"));
let vi = maybe_thunk(&expr(r#""a${b}c""#), &env, false, None);
assert!(matches!(vi, Value::Thunk(_)), "interpolated str must stay thunked");
}
#[test]
fn eval_pure_constant_arg_classification() {
fn expr(s: &str) -> ast::Expr {
rnix::Root::parse(s).tree().expr().expect("parse")
}
assert!(eval_pure_constant_arg(&expr("42")).is_some());
assert!(eval_pure_constant_arg(&expr("3.14")).is_some());
assert!(eval_pure_constant_arg(&expr(r#""const""#)).is_some());
assert!(eval_pure_constant_arg(&expr("/abs/path")).is_some());
assert!(eval_pure_constant_arg(&expr(r#""a${b}c""#)).is_none(), "interpolated str");
assert!(eval_pure_constant_arg(&expr("true")).is_none(), "bool is an ident");
assert!(eval_pure_constant_arg(&expr("x")).is_none(), "ident (with-scope force)");
assert!(eval_pure_constant_arg(&expr("a.b")).is_none(), "select (fixpoint)");
assert!(eval_pure_constant_arg(&expr("f x")).is_none(), "apply (may throw)");
assert!(eval_pure_constant_arg(&expr("1 + 1")).is_none(), "binop (may throw)");
assert!(eval_pure_constant_arg(&expr("throw \"x\"")).is_none(), "throw stays lazy");
}
#[test]
fn ignored_throwing_arg_stays_lazy() {
assert_eq!(ev(r#"(x: 7) (throw "boom")"#), Value::Int(7));
assert_eq!(ev(r#"(x: 7) "const""#), Value::Int(7));
assert_eq!(ev(r#"(x: x) "used""#), Value::string("used"));
}
#[test]
fn eval_int() { assert_eq!(ev("42"), Value::Int(42)); }
#[test]
fn eval_float() { assert_eq!(ev("3.14"), Value::Float(3.14)); }
#[test]
fn eval_string() { assert_eq!(ev(r#""hello""#), Value::string("hello")); }
#[test]
fn eval_bool() { assert_eq!(ev("true"), Value::Bool(true)); }
#[test]
fn eval_null() { assert_eq!(ev("null"), Value::Null); }
#[test]
fn eval_arithmetic() {
assert_eq!(ev("1 + 2"), Value::Int(3));
assert_eq!(ev("10 - 3"), Value::Int(7));
assert_eq!(ev("2 * 3"), Value::Int(6));
assert_eq!(ev("10 / 3"), Value::Int(3));
}
#[test]
fn eval_precedence() {
assert_eq!(ev("1 + 2 * 3"), Value::Int(7));
assert_eq!(ev("(1 + 2) * 3"), Value::Int(9));
}
#[test]
fn eval_comparison() {
assert_eq!(ev("1 == 1"), Value::Bool(true));
assert_eq!(ev("1 == 2"), Value::Bool(false));
assert_eq!(ev("1 < 2"), Value::Bool(true));
assert_eq!(ev("2 <= 2"), Value::Bool(true));
}
#[test]
fn eval_logic() {
assert_eq!(ev("true && false"), Value::Bool(false));
assert_eq!(ev("true || false"), Value::Bool(true));
assert_eq!(ev("!true"), Value::Bool(false));
}
#[test]
fn eval_string_concat() {
assert_eq!(ev(r#""hello" + " " + "world""#), Value::string("hello world"));
}
#[test]
fn eval_if() {
assert_eq!(ev("if true then 1 else 2"), Value::Int(1));
assert_eq!(ev("if false then 1 else 2"), Value::Int(2));
}
#[test]
fn eval_let() {
assert_eq!(ev("let x = 1; in x"), Value::Int(1));
assert_eq!(ev("let x = 1; y = 2; in x + y"), Value::Int(3));
}
#[test]
fn eval_let_dotted_simple() {
assert_eq!(ev("let a.b = 1; a.c = 2; in a.b + a.c"), Value::Int(3));
}
#[test]
fn eval_let_dotted_deep() {
assert_eq!(ev("let a.b.c = 1; in a.b.c"), Value::Int(1));
}
#[test]
fn eval_let_dotted_mixed() {
assert_eq!(
ev("let a.x = 1; b = 2; a.y = 3; in a.x + a.y + b"),
Value::Int(6),
);
}
#[test]
fn eval_let_dotted_produces_attrset() {
let v = ev("let a.b = 1; a.c = 2; in a");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("b"), Some(&Value::Int(1)));
assert_eq!(attrs.get("c"), Some(&Value::Int(2)));
} else {
panic!("expected Attrs, got {v:?}");
}
}
#[test]
fn dynamic_inner_attr_key_is_lazy_on_sibling_read() {
assert_eq!(
ev(r#"let s = { a.${throw "KEYFORCED"} = 7; other = 9; }; in s.other"#),
Value::Int(9),
);
}
#[test]
fn dynamic_inner_attr_key_resolves_on_head_demand() {
let v = ev(r#"let u = "bob"; s = { homes.${u} = 7; }; in s.homes"#);
if let Value::Attrs(attrs) = force_value(&v).unwrap() {
assert_eq!(attrs.get("bob"), Some(&Value::Int(7)));
} else {
panic!("expected Attrs");
}
}
#[test]
fn dynamic_inner_attr_key_merges_with_static_sibling() {
let v = ev(r#"let u = "x"; s = { a.${u} = 1; a.b = 2; }; in s.a"#);
if let Value::Attrs(attrs) = force_value(&v).unwrap() {
assert_eq!(attrs.get("x"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected Attrs");
}
}
#[test]
fn dynamic_inner_attr_key_null_skips_binding() {
let v = ev(
r#"let c = true; s = { a.${if c then null else "n"} = 5; b = 1; }; in s.b"#,
);
assert_eq!(v, Value::Int(1));
}
#[test]
fn interpolated_string_attr_key_is_lazy_on_sibling_read() {
assert_eq!(
ev(r#"let s = { a."p/${throw "KEYFORCED"}" = 7; other = 9; }; in s.other"#),
Value::Int(9),
);
}
#[test]
fn interpolated_string_attr_key_resolves_on_head_demand() {
let v = ev(r#"let u = "bob"; s = { homes."u/${u}" = 7; }; in s.homes"#);
if let Value::Attrs(attrs) = force_value(&v).unwrap() {
assert_eq!(attrs.get("u/bob"), Some(&Value::Int(7)));
} else {
panic!("expected Attrs");
}
}
#[test]
fn purely_literal_string_attr_key_stays_eager_static() {
let v = ev(r#"let s = { a."foo bar" = 1; a.b = 2; }; in s.a"#);
if let Value::Attrs(attrs) = force_value(&v).unwrap() {
assert_eq!(attrs.get("foo bar"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected Attrs");
}
}
#[test]
fn dynamic_tail_key_under_colliding_head_is_lazy() {
let v = ev(
r#"let s = { sd.services.x = 1; sd.tmpfiles.${throw "KEYFORCED"}.d = 2; }; in s.sd.services.x"#,
);
assert_eq!(v, Value::Int(1));
}
#[test]
fn dynamic_tail_key_under_colliding_head_resolves_and_merges() {
let v = ev(
r#"let k = "z"; s = { sd.services.x = 1; sd.tmpfiles.${k}.d = 2; }; in s.sd"#,
);
let sd = force_value(&v).unwrap();
if let Value::Attrs(sd_attrs) = &sd {
let services = force_value(sd_attrs.get("services").unwrap()).unwrap();
if let Value::Attrs(a) = &services {
assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
} else { panic!("expected services attrs"); }
let tmpfiles = force_value(sd_attrs.get("tmpfiles").unwrap()).unwrap();
if let Value::Attrs(a) = &tmpfiles {
let z = force_value(a.get("z").unwrap()).unwrap();
if let Value::Attrs(zd) = &z {
assert_eq!(force_value(zd.get("d").unwrap()).unwrap(), Value::Int(2));
} else { panic!("expected z attrs"); }
} else { panic!("expected tmpfiles attrs"); }
} else {
panic!("expected sd attrs");
}
}
#[test]
fn with_namespace_is_lazy_on_body_whnf() {
let v = ev(r#"builtins.attrNames (with (throw "WITH-FORCED"); { a = 1; b = 2; })"#);
if let Value::List(items) = force_value(&v).unwrap() {
let names: Vec<String> = items
.iter()
.map(|i| match force_value(i).unwrap() {
Value::String(s) => s.as_str().to_string(),
other => panic!("expected string, got {}", other.type_name()),
})
.collect();
assert_eq!(names, vec!["a".to_string(), "b".to_string()]);
} else {
panic!("expected list");
}
}
#[test]
fn with_namespace_forces_only_on_fallthrough() {
assert_eq!(ev(r#"with { x = 42; }; x"#), Value::Int(42));
assert_eq!(ev(r#"let x = 7; in with (throw "NS"); x"#), Value::Int(7));
}
#[test]
fn dotted_fullset_leaf_deep_merges_with_deeper_sibling() {
let v = ev(r#"{ o.a = { x = 1; }; o.a.y = 2; }.o.a"#);
if let Value::Attrs(a) = force_value(&v).unwrap() {
assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(a.get("y").unwrap()).unwrap(), Value::Int(2));
} else {
panic!("expected attrs");
}
}
#[test]
fn dotted_fullset_leaf_deep_merge_reverse_order() {
let v = ev(r#"{ o.a.y = 2; o.a = { x = 1; }; }.o.a"#);
if let Value::Attrs(a) = force_value(&v).unwrap() {
assert_eq!(force_value(a.get("x").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(a.get("y").unwrap()).unwrap(), Value::Int(2));
} else {
panic!("expected attrs");
}
}
#[test]
fn dotted_fullset_leaf_merge_preserves_leaf_laziness() {
assert_eq!(ev(r#"{ o.a = { x = throw "X-NEVER"; }; o.a.y = 2; }.o.a.y"#), Value::Int(2));
}
#[test]
fn eval_nested_let() {
assert_eq!(ev("let a = 1; b = let c = 2; in c; in a + b"), Value::Int(3));
}
#[test]
fn eval_lambda() {
assert_eq!(ev("(x: x + 1) 41"), Value::Int(42));
}
#[test]
fn eval_lambda_multi_arg() {
assert_eq!(ev("(x: y: x + y) 1 2"), Value::Int(3));
}
#[test]
fn eval_list() {
let v = ev("[1 2 3]");
assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]));
}
#[test]
fn eval_list_concat() {
let v = ev("[1 2] ++ [3 4]");
assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]));
}
#[test]
fn eval_attrset() {
let v = ev("{ a = 1; b = 2; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected attrset");
}
}
#[test]
fn eval_select() {
assert_eq!(ev("{ a = 42; }.a"), Value::Int(42));
}
#[test]
fn eval_select_or() {
assert_eq!(ev("{ a = 42; }.b or 0"), Value::Int(0));
}
#[test]
fn eval_has_attr() {
assert_eq!(ev("{ a = 1; } ? a"), Value::Bool(true));
assert_eq!(ev("{ a = 1; } ? b"), Value::Bool(false));
}
#[test]
fn eval_update() {
let v = ev("{ a = 1; b = 2; } // { b = 3; c = 4; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(3)));
assert_eq!(attrs.get("c"), Some(&Value::Int(4)));
} else {
panic!("expected attrset");
}
}
#[test]
fn eval_with() {
assert_eq!(ev("with { x = 42; }; x"), Value::Int(42));
}
#[test]
fn eval_assert() {
assert_eq!(ev("assert true; 42"), Value::Int(42));
assert!(eval("assert false; 42").is_err());
}
#[test]
fn eval_formals() {
assert_eq!(ev("({ a, b }: a + b) { a = 1; b = 2; }"), Value::Int(3));
}
#[test]
fn eval_formals_default() {
assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 1; }"), Value::Int(11));
}
#[test]
fn eval_formals_ellipsis() {
assert_eq!(ev("({ a, ... }: a) { a = 1; b = 2; }"), Value::Int(1));
}
#[test]
fn eval_named_formals() {
assert_eq!(ev("(args @ { a }: args.a) { a = 42; }"), Value::Int(42));
}
#[test]
fn eval_rec_attrset() {
assert_eq!(ev("(rec { a = 1; b = a + 1; }).b"), Value::Int(2));
}
#[test]
fn eval_negation() {
assert_eq!(ev("-42"), Value::Int(-42));
}
#[test]
fn eval_float_arithmetic() {
assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
assert_eq!(ev("1 + 1.5"), Value::Float(2.5));
}
#[test]
fn eval_division_by_zero() {
assert!(eval("1 / 0").is_err());
}
#[test]
fn eval_builtins_available() {
assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
}
#[test]
fn eval_builtins_length() {
assert_eq!(ev("builtins.length [1 2 3]"), Value::Int(3));
}
#[test]
fn eval_builtins_head_tail() {
assert_eq!(ev("builtins.head [1 2 3]"), Value::Int(1));
assert_eq!(ev("builtins.length (builtins.tail [1 2 3])"), Value::Int(2));
}
#[test]
fn eval_builtins_add() {
assert_eq!(ev("builtins.add 1 2"), Value::Int(3));
}
#[test]
fn eval_builtins_to_string() {
assert_eq!(ev("builtins.toString 42"), Value::string("42"));
}
#[test]
fn eval_implication() {
assert_eq!(ev("false -> true"), Value::Bool(true));
assert_eq!(ev("true -> false"), Value::Bool(false));
assert_eq!(ev("true -> true"), Value::Bool(true));
}
#[test]
fn eval_error_undefined_variable() {
let result = eval("nonexistent");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("undefined variable"));
}
#[test]
fn eval_error_type_mismatch_arithmetic() {
let result = eval(r#"1 + "hello""#);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("cannot add") || msg.contains("type"));
}
#[test]
fn eval_error_unexpected_argument() {
let result = eval("({ a }: a) { a = 1; b = 2; }");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("unexpected argument"));
}
#[test]
fn eval_error_missing_required_argument() {
let result = eval("({ a, b }: a + b) { a = 1; }");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("missing argument"));
}
#[test]
fn eval_builtins_attr_names_sorted() {
let v = ev("builtins.attrNames { z = 1; a = 2; m = 3; }");
assert_eq!(
v,
Value::list(vec![
Value::string("a"),
Value::string("m"),
Value::string("z"),
]),
);
}
#[test]
fn eval_builtins_attr_values() {
let v = ev("builtins.attrValues { a = 1; b = 2; }");
assert_eq!(v, Value::list(vec![Value::Int(1), Value::Int(2)]));
}
#[test]
fn eval_builtins_is_null() {
assert_eq!(ev("builtins.isNull null"), Value::Bool(true));
assert_eq!(ev("builtins.isNull 1"), Value::Bool(false));
}
#[test]
fn eval_builtins_is_int() {
assert_eq!(ev("builtins.isInt 42"), Value::Bool(true));
assert_eq!(ev("builtins.isInt 3.14"), Value::Bool(false));
}
#[test]
fn eval_builtins_is_bool() {
assert_eq!(ev("builtins.isBool true"), Value::Bool(true));
assert_eq!(ev("builtins.isBool 0"), Value::Bool(false));
}
#[test]
fn eval_builtins_is_string() {
assert_eq!(ev(r#"builtins.isString "hi""#), Value::Bool(true));
assert_eq!(ev("builtins.isString 1"), Value::Bool(false));
}
#[test]
fn eval_builtins_is_list() {
assert_eq!(ev("builtins.isList [1 2]"), Value::Bool(true));
assert_eq!(ev("builtins.isList {}"), Value::Bool(false));
}
#[test]
fn eval_builtins_is_attrs() {
assert_eq!(ev("builtins.isAttrs {}"), Value::Bool(true));
assert_eq!(ev("builtins.isAttrs []"), Value::Bool(false));
}
#[test]
fn eval_builtins_string_length() {
assert_eq!(ev(r#"builtins.stringLength "hello""#), Value::Int(5));
assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
}
#[test]
fn eval_builtins_to_json_roundtrip() {
assert_eq!(
ev(r#"builtins.fromJSON (builtins.toJSON 42)"#),
Value::Int(42),
);
assert_eq!(
ev(r#"builtins.fromJSON (builtins.toJSON [1 2 3])"#),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn eval_builtins_from_json() {
assert_eq!(
ev(r#"builtins.fromJSON "{\"a\": 1}""#),
{
let mut attrs = NixAttrs::new();
attrs.insert("a".to_string(), Value::Int(1));
Value::Attrs(Rc::new(attrs))
},
);
assert_eq!(ev(r#"builtins.fromJSON "null""#), Value::Null);
assert_eq!(ev(r#"builtins.fromJSON "true""#), Value::Bool(true));
}
#[test]
fn eval_nested_function_application() {
assert_eq!(ev("(x: y: x + y) 1 2"), Value::Int(3));
assert_eq!(ev("((x: y: x + y) 1) 2"), Value::Int(3));
}
#[test]
fn eval_recursive_let() {
assert_eq!(ev("let a = 1; b = a + 1; in b"), Value::Int(2));
assert_eq!(ev("let a = 1; b = a + 1; c = b + 1; in c"), Value::Int(3));
}
#[test]
fn eval_string_comparison() {
assert_eq!(ev(r#""a" < "b""#), Value::Bool(true));
assert_eq!(ev(r#""b" < "a""#), Value::Bool(false));
assert_eq!(ev(r#""abc" == "abc""#), Value::Bool(true));
assert_eq!(ev(r#""abc" != "def""#), Value::Bool(true));
}
#[test]
fn eval_list_in_attrset() {
let v = ev("{ x = [1 2 3]; }.x");
assert_eq!(
v,
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn eval_nested_attrset_select() {
assert_eq!(ev("{ a = { b = 42; }; }.a.b"), Value::Int(42));
}
#[test]
fn eval_let_shadows_outer() {
assert_eq!(
ev("let x = 1; in let x = 2; in x"),
Value::Int(2),
);
}
#[test]
fn eval_with_provides_scope() {
assert_eq!(
ev("with { x = 42; y = 10; }; x + y"),
Value::Int(52),
);
}
#[test]
fn eval_list_equality() {
assert_eq!(ev("[1 2] == [1 2]"), Value::Bool(true));
assert_eq!(ev("[1 2] == [1 3]"), Value::Bool(false));
}
#[test]
fn eval_attrset_equality() {
assert_eq!(ev("{ a = 1; } == { a = 1; }"), Value::Bool(true));
assert_eq!(ev("{ a = 1; } == { a = 2; }"), Value::Bool(false));
}
#[test]
fn literal_int_large_zero_negative() {
assert_eq!(ev("9223372036854775807"), Value::Int(i64::MAX));
assert_eq!(ev("0"), Value::Int(0));
assert_eq!(ev("-1"), Value::Int(-1));
assert_eq!(ev("-999999"), Value::Int(-999999));
}
#[test]
fn literal_float_small_large() {
assert_eq!(ev("0.001"), Value::Float(0.001));
assert_eq!(ev("999999.999"), Value::Float(999999.999));
assert_eq!(ev("1.0e3"), Value::Float(1000.0));
assert_eq!(ev("1.5e2"), Value::Float(150.0));
}
#[test]
fn literal_string_empty_and_escapes() {
assert_eq!(ev(r#""""#), Value::string(""));
assert_eq!(ev(r#""hello\nworld""#), Value::string("hello\nworld"));
assert_eq!(ev(r#""tab\there""#), Value::string("tab\there"));
}
#[test]
fn literal_multiline_string() {
assert_eq!(
ev("''hello''"),
Value::string("hello"),
);
assert_eq!(
ev("''\n line1\n line2\n''"),
Value::string("line1\nline2\n"),
);
}
#[test]
fn literal_paths() {
assert_eq!(ev("./foo"), Value::Path(Box::new(SmolStr::from("./foo"))));
assert_eq!(ev("/nix/store/abc"), Value::Path(Box::new(SmolStr::from("/nix/store/abc"))));
assert_eq!(ev("~/myfile"), Value::Path(Box::new(SmolStr::from("~/myfile"))));
}
#[test]
fn interp_path_abs_splices_and_types_path() {
let v = ev(r#"let x = "foo"; in /a/${x}/b"#);
assert_eq!(v, Value::Path(Box::new(SmolStr::from("/a/foo/b"))));
}
#[test]
fn interp_path_abs_multi_and_slash_in_value() {
assert_eq!(
ev(r#"let a = "x"; b = "y/z"; in /p/${a}/${b}.nix"#),
Value::Path(Box::new(SmolStr::from("/p/x/y/z.nix"))),
);
}
#[test]
fn interp_path_abs_normalizes_double_slash_seam() {
assert_eq!(
ev(r#"/bar/${/tmp/foo}"#),
Value::Path(Box::new(SmolStr::from("/bar/tmp/foo"))),
);
}
#[test]
fn interp_path_rel_resolves_against_eval_dir() {
let _g = push_eval_file(std::path::PathBuf::from("/tmp/example/default.nix"));
assert_eq!(
ev(r#"let x = "foo"; in ./${x}.nix"#),
Value::Path(Box::new(SmolStr::from("/tmp/example/foo.nix"))),
);
}
#[test]
fn interp_path_rel_no_eval_dir_keeps_relative_text() {
assert_eq!(
ev(r#"let x = "foo"; in ./${x}.nix"#),
Value::Path(Box::new(SmolStr::from("./foo.nix"))),
);
}
#[test]
fn interp_path_home_splices_leading_tilde_preserved() {
assert_eq!(
ev(r#"let x = "foo"; in ~/${x}/bar"#),
Value::Path(Box::new(SmolStr::from("~/foo/bar"))),
);
}
#[test]
fn interp_path_non_interpolated_still_raw() {
assert_eq!(ev("/a/b/c"), Value::Path(Box::new(SmolStr::from("/a/b/c"))));
assert_eq!(ev("~/plain"), Value::Path(Box::new(SmolStr::from("~/plain"))));
}
#[test]
fn literal_null_true_false_standalone() {
assert_eq!(ev("null"), Value::Null);
assert_eq!(ev("true"), Value::Bool(true));
assert_eq!(ev("false"), Value::Bool(false));
}
#[test]
fn op_arithmetic_int() {
assert_eq!(ev("100 + 200"), Value::Int(300));
assert_eq!(ev("50 - 30"), Value::Int(20));
assert_eq!(ev("7 * 8"), Value::Int(56));
assert_eq!(ev("17 / 3"), Value::Int(5)); }
#[test]
fn op_arithmetic_float() {
assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
assert_eq!(ev("5.0 - 1.5"), Value::Float(3.5));
assert_eq!(ev("2.0 * 3.0"), Value::Float(6.0));
assert_eq!(ev("7.0 / 2.0"), Value::Float(3.5));
}
#[test]
fn op_arithmetic_mixed_int_float() {
assert_eq!(ev("1 + 2.5"), Value::Float(3.5));
assert_eq!(ev("2.5 + 1"), Value::Float(3.5));
assert_eq!(ev("2 * 1.5"), Value::Float(3.0));
assert_eq!(ev("5.5 - 2"), Value::Float(3.5));
}
#[test]
fn op_string_concat() {
assert_eq!(ev(r#""foo" + "bar""#), Value::string("foobar"));
assert_eq!(ev(r#""" + "x""#), Value::string("x"));
assert_eq!(ev(r#""a" + "" + "b""#), Value::string("ab"));
}
#[test]
fn op_path_concat() {
assert_eq!(ev(r#"./foo + "/bar""#), Value::Path(Box::new(SmolStr::from("./foo/bar"))));
assert_eq!(ev("./a + ./b"), Value::Path(Box::new(SmolStr::from("./a/./b"))));
}
#[test]
fn op_comparison_ints() {
assert_eq!(ev("1 < 2"), Value::Bool(true));
assert_eq!(ev("2 < 1"), Value::Bool(false));
assert_eq!(ev("2 > 1"), Value::Bool(true));
assert_eq!(ev("1 > 2"), Value::Bool(false));
assert_eq!(ev("2 <= 2"), Value::Bool(true));
assert_eq!(ev("3 <= 2"), Value::Bool(false));
assert_eq!(ev("2 >= 2"), Value::Bool(true));
assert_eq!(ev("1 >= 2"), Value::Bool(false));
}
#[test]
fn op_comparison_floats() {
assert_eq!(ev("1.5 < 2.5"), Value::Bool(true));
assert_eq!(ev("2.5 > 1.5"), Value::Bool(true));
assert_eq!(ev("1.5 <= 1.5"), Value::Bool(true));
assert_eq!(ev("1.5 >= 1.5"), Value::Bool(true));
}
#[test]
fn op_comparison_strings() {
assert_eq!(ev(r#""apple" < "banana""#), Value::Bool(true));
assert_eq!(ev(r#""banana" > "apple""#), Value::Bool(true));
assert_eq!(ev(r#""abc" == "abc""#), Value::Bool(true));
assert_eq!(ev(r#""abc" != "xyz""#), Value::Bool(true));
assert_eq!(ev(r#""abc" <= "abd""#), Value::Bool(true));
assert_eq!(ev(r#""abc" >= "abb""#), Value::Bool(true));
}
#[test]
fn op_equality_various_types() {
assert_eq!(ev("null == null"), Value::Bool(true));
assert_eq!(ev("true == true"), Value::Bool(true));
assert_eq!(ev("false == false"), Value::Bool(true));
assert_eq!(ev("true == false"), Value::Bool(false));
assert_eq!(ev("1 == 1"), Value::Bool(true));
assert_eq!(ev("1 != 2"), Value::Bool(true));
assert_eq!(ev(r#"1 == "1""#), Value::Bool(false));
assert_eq!(ev("null == false"), Value::Bool(false));
}
#[test]
fn op_logic_short_circuit() {
assert_eq!(ev("false && (1 / 0 == 0)"), Value::Bool(false));
assert_eq!(ev("true || (1 / 0 == 0)"), Value::Bool(true));
}
#[test]
fn op_logic_full() {
assert_eq!(ev("true && true"), Value::Bool(true));
assert_eq!(ev("true && false"), Value::Bool(false));
assert_eq!(ev("false && true"), Value::Bool(false));
assert_eq!(ev("false && false"), Value::Bool(false));
assert_eq!(ev("true || true"), Value::Bool(true));
assert_eq!(ev("true || false"), Value::Bool(true));
assert_eq!(ev("false || true"), Value::Bool(true));
assert_eq!(ev("false || false"), Value::Bool(false));
assert_eq!(ev("!true"), Value::Bool(false));
assert_eq!(ev("!false"), Value::Bool(true));
}
#[test]
fn op_implication_truth_table() {
assert_eq!(ev("false -> false"), Value::Bool(true));
assert_eq!(ev("false -> true"), Value::Bool(true));
assert_eq!(ev("true -> true"), Value::Bool(true));
assert_eq!(ev("true -> false"), Value::Bool(false));
}
#[test]
fn op_implication_short_circuit() {
assert_eq!(ev("false -> (1 / 0 == 0)"), Value::Bool(true));
}
#[test]
fn op_update_merge() {
let v = ev("{ a = 1; } // { b = 2; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected attrs");
}
}
#[test]
fn op_update_right_wins() {
assert_eq!(ev("({ a = 1; } // { a = 2; }).a"), Value::Int(2));
}
#[test]
fn op_list_concat() {
assert_eq!(
ev("[1 2] ++ [3 4]"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3), Value::Int(4)]),
);
assert_eq!(ev("[] ++ [1]"), Value::list(vec![Value::Int(1)]));
assert_eq!(ev("[1] ++ []"), Value::list(vec![Value::Int(1)]));
}
#[test]
fn op_has_attr_present_and_absent() {
assert_eq!(ev("{ x = 1; y = 2; } ? x"), Value::Bool(true));
assert_eq!(ev("{ x = 1; } ? z"), Value::Bool(false));
assert_eq!(ev("{} ? anything"), Value::Bool(false));
}
#[test]
fn op_unary_negate() {
assert_eq!(ev("-42"), Value::Int(-42));
assert_eq!(ev("-3.14"), Value::Float(-3.14));
assert_eq!(ev("- -5"), Value::Int(5));
}
#[test]
fn control_if_true_branch() {
assert_eq!(ev("if true then 42 else 0"), Value::Int(42));
}
#[test]
fn control_if_false_branch() {
assert_eq!(ev("if false then 42 else 0"), Value::Int(0));
}
#[test]
fn control_if_nested() {
assert_eq!(
ev("if true then (if false then 1 else 2) else 3"),
Value::Int(2),
);
assert_eq!(
ev("if false then 1 else (if true then 2 else 3)"),
Value::Int(2),
);
}
#[test]
fn control_assert_passing() {
assert_eq!(ev("assert 1 == 1; 42"), Value::Int(42));
assert_eq!(ev("assert true; true"), Value::Bool(true));
}
#[test]
fn control_assert_failing() {
assert!(eval("assert false; 42").is_err());
assert!(eval("assert 1 == 2; 42").is_err());
}
#[test]
fn control_with_basic_scope() {
assert_eq!(ev("with { a = 1; b = 2; }; a + b"), Value::Int(3));
}
#[test]
fn control_with_lexical_precedence() {
assert_eq!(
ev("let x = 10; in with { x = 99; }; x"),
Value::Int(10),
);
}
#[test]
fn control_with_nested() {
assert_eq!(
ev("with { a = 1; }; with { b = 2; }; a + b"),
Value::Int(3),
);
}
#[test]
fn control_with_lazy_fix_self() {
let result = eval(
"let fix = f: let x = f x; in x; in fix (self: with self; { a = 1; b = a + 1; })"
);
assert!(result.is_ok(), "fix with self should work: {:?}", result);
if let Ok(Value::Attrs(attrs)) = result {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected Attrs, got {:?}", result);
}
}
#[test]
fn control_with_lazy_fix_self_lib_pattern() {
let result = eval(r#"
let fix = f: let x = f x; in x;
in (fix (self: with self; {
lib = { version = "1.0"; };
hello = "hello ${lib.version}";
})).hello
"#);
assert!(result.is_ok(), "nixpkgs-style lib pattern: {:?}", result);
assert_eq!(
result.unwrap(),
Value::String(Rc::new(NixString::plain("hello 1.0"))),
);
}
#[test]
fn control_with_non_attrset_errors() {
let result = eval("with 42; 1");
assert_eq!(result.unwrap(), Value::Int(1));
}
#[test]
fn control_with_non_attrset_lookup_falls_through() {
let result = eval("let x = 1; in with 42; x");
assert_eq!(result.unwrap(), Value::Int(1));
}
#[test]
fn control_let_simple_and_multiple() {
assert_eq!(ev("let x = 5; in x"), Value::Int(5));
assert_eq!(ev("let x = 1; y = 2; z = 3; in x + y + z"), Value::Int(6));
}
#[test]
fn control_let_shadow_outer() {
assert_eq!(
ev("let x = 1; in let x = 2; in x"),
Value::Int(2),
);
}
#[test]
fn control_let_recursive_reference() {
assert_eq!(ev("let a = 1; b = a + 1; in b"), Value::Int(2));
assert_eq!(ev("let a = 1; b = a + 1; c = b + 1; in c"), Value::Int(3));
}
#[test]
fn control_nested_let_expression() {
assert_eq!(
ev("let a = let b = 1; in b; in a"),
Value::Int(1),
);
assert_eq!(
ev("let a = let b = 10; in b + 5; in a * 2"),
Value::Int(30),
);
}
#[test]
fn func_identity_lambda() {
assert_eq!(ev("(x: x) 42"), Value::Int(42));
assert_eq!(ev(r#"(x: x) "hello""#), Value::string("hello"));
}
#[test]
fn func_curried_two_args() {
assert_eq!(ev("(x: y: x + y) 3 4"), Value::Int(7));
}
#[test]
fn func_curried_three_args() {
assert_eq!(ev("(a: b: c: a + b + c) 1 2 3"), Value::Int(6));
}
#[test]
fn func_formals_basic() {
assert_eq!(ev("({ a, b }: a + b) { a = 3; b = 7; }"), Value::Int(10));
}
#[test]
fn func_formals_with_defaults() {
assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 5; }"), Value::Int(15));
assert_eq!(ev("({ a, b ? 10 }: a + b) { a = 5; b = 20; }"), Value::Int(25));
}
#[test]
fn func_formals_with_ellipsis() {
assert_eq!(ev("({ a, ... }: a) { a = 1; b = 2; c = 3; }"), Value::Int(1));
}
#[test]
fn func_named_formals_at_before() {
assert_eq!(
ev("(args @ { a, b }: args.a + args.b) { a = 3; b = 4; }"),
Value::Int(7),
);
}
#[test]
fn func_named_formals_at_after() {
assert_eq!(
ev("({ a, b } @ args: args.a + args.b) { a = 10; b = 20; }"),
Value::Int(30),
);
}
#[test]
fn func_nested_application() {
assert_eq!(ev("((x: y: x * y) 3) 4"), Value::Int(12));
}
#[test]
fn func_higher_order_map() {
assert_eq!(
ev("builtins.map (x: x * 2) [1 2 3]"),
Value::list(vec![Value::Int(2), Value::Int(4), Value::Int(6)]),
);
}
#[test]
fn func_higher_order_filter() {
assert_eq!(
ev("builtins.filter (x: x > 2) [1 2 3 4 5]"),
Value::list(vec![Value::Int(3), Value::Int(4), Value::Int(5)]),
);
}
#[test]
fn func_higher_order_foldl() {
assert_eq!(
ev("builtins.foldl' (acc: x: acc + x) 0 [1 2 3 4]"),
Value::Int(10),
);
}
#[test]
fn func_as_attrset_value() {
assert_eq!(
ev("let s = { f = x: x + 1; }; in s.f 5"),
Value::Int(6),
);
}
#[test]
fn func_immediate_application() {
assert_eq!(ev("(x: x * x) 7"), Value::Int(49));
}
#[test]
fn func_in_let_binding() {
assert_eq!(
ev("let double = x: x * 2; in double 21"),
Value::Int(42),
);
}
#[test]
fn attrs_empty_set() {
let v = ev("{}");
if let Value::Attrs(attrs) = v {
assert!(attrs.is_empty());
} else {
panic!("expected attrs");
}
}
#[test]
fn attrs_simple() {
assert_eq!(ev("{ a = 1; }.a"), Value::Int(1));
}
#[test]
fn attrs_nested_access() {
assert_eq!(ev("{ a = { b = { c = 42; }; }; }.a.b.c"), Value::Int(42));
}
#[test]
fn attrs_recursive_set() {
assert_eq!(ev("(rec { a = 1; b = a + 1; c = b + 1; }).c"), Value::Int(3));
}
#[test]
fn attrs_update_disjoint() {
let v = ev("{ a = 1; } // { b = 2; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.len(), 2);
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected attrs");
}
}
#[test]
fn attrs_update_override() {
assert_eq!(ev("({ a = 1; } // { a = 2; }).a"), Value::Int(2));
}
#[test]
fn attrs_has_attr_operator() {
assert_eq!(ev("{ a = 1; } ? a"), Value::Bool(true));
assert_eq!(ev("{ a = 1; } ? b"), Value::Bool(false));
}
#[test]
fn attrs_select_with_default() {
assert_eq!(ev("{ a = 1; }.a or 99"), Value::Int(1));
assert_eq!(ev("{}.missing or 99"), Value::Int(99));
assert_eq!(ev("{ a = 1; }.b or 42"), Value::Int(42));
}
#[test]
fn attrs_nested_attr_path_in_binding() {
assert_eq!(ev("{ a.b = 1; }.a.b"), Value::Int(1));
}
#[test]
fn attrs_inherit_from_scope() {
assert_eq!(ev("let x = 1; y = 2; in { inherit x y; }.x"), Value::Int(1));
assert_eq!(ev("let x = 1; y = 2; in { inherit x y; }.y"), Value::Int(2));
}
#[test]
fn attrs_inherit_from_expr() {
assert_eq!(
ev("{ inherit ({ a = 42; b = 10; }) a; }.a"),
Value::Int(42),
);
}
#[test]
fn attrs_dynamic_attr_name() {
assert_eq!(
ev(r#"let name = "x"; in { ${name} = 42; }.x"#),
Value::Int(42),
);
}
#[test]
fn attrs_attr_names_sorted() {
assert_eq!(
ev("builtins.attrNames { z = 1; m = 2; a = 3; }"),
Value::list(vec![
Value::string("a"),
Value::string("m"),
Value::string("z"),
]),
);
}
#[test]
fn attrs_attr_values_follow_key_order() {
assert_eq!(
ev("builtins.attrValues { c = 3; a = 1; b = 2; }"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn attrs_update_is_shallow() {
assert_eq!(
ev("({ a = { x = 1; }; } // { a = { y = 2; }; }).a ? x"),
Value::Bool(false),
);
assert_eq!(
ev("({ a = { x = 1; }; } // { a = { y = 2; }; }).a.y"),
Value::Int(2),
);
}
#[test]
fn list_empty() {
assert_eq!(ev("[]"), Value::list(vec![]));
}
#[test]
fn list_single_element() {
assert_eq!(ev("[1]"), Value::list(vec![Value::Int(1)]));
}
#[test]
fn list_mixed_types() {
assert_eq!(
ev(r#"[1 "two" true null]"#),
Value::list(vec![
Value::Int(1),
Value::string("two"),
Value::Bool(true),
Value::Null,
]),
);
}
#[test]
fn list_nested() {
assert_eq!(
ev("[[1 2] [3 4]]"),
Value::list(vec![
Value::list(vec![Value::Int(1), Value::Int(2)]),
Value::list(vec![Value::Int(3), Value::Int(4)]),
]),
);
}
#[test]
fn list_concat_operator() {
assert_eq!(
ev("[1] ++ [2] ++ [3]"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn list_builtins_length() {
assert_eq!(ev("builtins.length [1 2 3]"), Value::Int(3));
assert_eq!(ev("builtins.length []"), Value::Int(0));
}
#[test]
fn list_builtins_elem_at() {
assert_eq!(ev("builtins.elemAt [10 20 30] 0"), Value::Int(10));
assert_eq!(ev("builtins.elemAt [10 20 30] 1"), Value::Int(20));
assert_eq!(ev("builtins.elemAt [10 20 30] 2"), Value::Int(30));
}
#[test]
fn list_equality() {
assert_eq!(ev("[1 2 3] == [1 2 3]"), Value::Bool(true));
assert_eq!(ev("[1 2] == [1 2 3]"), Value::Bool(false));
assert_eq!(ev("[] == []"), Value::Bool(true));
}
#[test]
fn interp_simple_variable() {
assert_eq!(
ev(r#"let name = "world"; in "hello ${name}""#),
Value::string("hello world"),
);
}
#[test]
fn interp_nested_expression() {
assert_eq!(
ev(r#""result: ${builtins.toString (1 + 2)}""#),
Value::string("result: 3"),
);
}
#[test]
fn interp_int_coercion() {
assert_eq!(
ev(r#"let x = 42; in "count: ${builtins.toString x}""#),
Value::string("count: 42"),
);
}
#[test]
fn interp_multiple() {
assert_eq!(
ev(r#"let a = "foo"; b = "bar"; in "${a} and ${b}""#),
Value::string("foo and bar"),
);
}
#[test]
fn interp_in_let() {
assert_eq!(
ev(r#"let x = "world"; in "hello ${x}""#),
Value::string("hello world"),
);
}
#[test]
fn interp_empty_result() {
assert_eq!(
ev(r#"let x = ""; in "a${x}b""#),
Value::string("ab"),
);
}
#[test]
fn interp_path_in_string_context() {
assert!(eval(r#""path: ${./foo-nonexistent-xyz}""#).is_err());
}
#[test]
fn interp_adjacent_interpolations() {
assert_eq!(
ev(r#"let a = "x"; b = "y"; in "${a}${b}""#),
Value::string("xy"),
);
}
#[test]
fn builtins_map_filter_foldl() {
assert_eq!(
ev("builtins.map (x: x + 10) [1 2 3]"),
Value::list(vec![Value::Int(11), Value::Int(12), Value::Int(13)]),
);
assert_eq!(
ev("builtins.filter (x: x > 1) [1 2 3]"),
Value::list(vec![Value::Int(2), Value::Int(3)]),
);
assert_eq!(
ev("builtins.foldl' (a: b: a * b) 1 [2 3 4]"),
Value::Int(24),
);
}
#[test]
fn builtins_map_attrs() {
assert_eq!(
ev("(builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }).a"),
Value::Int(2),
);
assert_eq!(
ev("(builtins.mapAttrs (name: value: value * 2) { a = 1; b = 2; }).b"),
Value::Int(4),
);
}
#[test]
fn builtins_list_to_attrs() {
assert_eq!(
ev(r#"(builtins.listToAttrs [{ name = "x"; value = 1; } { name = "y"; value = 2; }]).x"#),
Value::Int(1),
);
}
#[test]
fn builtins_list_to_attrs_duplicate_key_first_wins() {
assert_eq!(
ev(r#"(builtins.listToAttrs [{ name = "k"; value = 1; } { name = "k"; value = 2; }]).k"#),
Value::Int(1),
);
}
#[test]
fn builtins_concat_map() {
assert_eq!(
ev("builtins.concatMap (x: [x (x * 2)]) [1 2 3]"),
Value::list(vec![
Value::Int(1), Value::Int(2),
Value::Int(2), Value::Int(4),
Value::Int(3), Value::Int(6),
]),
);
}
#[test]
fn builtins_concat_lists() {
assert_eq!(
ev("builtins.concatLists [[1 2] [3] [4 5]]"),
Value::list(vec![
Value::Int(1), Value::Int(2), Value::Int(3),
Value::Int(4), Value::Int(5),
]),
);
}
#[test]
fn builtins_concat_strings_sep() {
assert_eq!(
ev(r#"builtins.concatStringsSep ", " ["a" "b" "c"]"#),
Value::string("a, b, c"),
);
assert_eq!(
ev(r#"builtins.concatStringsSep "" ["x" "y"]"#),
Value::string("xy"),
);
}
#[test]
fn builtins_replace_strings() {
assert_eq!(
ev(r#"builtins.replaceStrings ["o"] ["0"] "foobar""#),
Value::string("f00bar"),
);
assert_eq!(
ev(r#"builtins.replaceStrings ["hello"] ["goodbye"] "hello world""#),
Value::string("goodbye world"),
);
}
#[test]
fn builtins_has_prefix_has_suffix_are_not_builtins() {
assert_eq!(ev(r#"builtins ? hasPrefix"#), Value::Bool(false));
assert_eq!(ev(r#"builtins ? hasSuffix"#), Value::Bool(false));
assert!(
eval(r#"builtins.hasPrefix "he" "hello""#).is_err(),
"builtins.hasPrefix must fail the way real nix fails it"
);
assert!(
eval(r#"builtins.hasSuffix "lo" "hello""#).is_err(),
"builtins.hasSuffix must fail the way real nix fails it"
);
}
#[test]
fn builtins_all_any() {
assert_eq!(ev("builtins.all (x: x > 0) [1 2 3]"), Value::Bool(true));
assert_eq!(ev("builtins.all (x: x > 1) [1 2 3]"), Value::Bool(false));
assert_eq!(ev("builtins.any (x: x > 2) [1 2 3]"), Value::Bool(true));
assert_eq!(ev("builtins.any (x: x > 5) [1 2 3]"), Value::Bool(false));
}
#[test]
fn builtins_sort() {
assert_eq!(
ev("builtins.sort (a: b: a < b) [3 1 2]"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
}
#[test]
fn builtins_remove_attrs() {
let v = ev(r#"builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b" "c"]"#);
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.len(), 1);
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert!(attrs.get("b").is_none());
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_intersect_attrs() {
let v = ev("builtins.intersectAttrs { a = 1; b = 2; } { b = 20; c = 30; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.len(), 1);
assert_eq!(attrs.get("b"), Some(&Value::Int(20)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_type_of_all_types() {
assert_eq!(ev("builtins.typeOf null"), Value::string("null"));
assert_eq!(ev("builtins.typeOf true"), Value::string("bool"));
assert_eq!(ev("builtins.typeOf 42"), Value::string("int"));
assert_eq!(ev("builtins.typeOf 3.14"), Value::string("float"));
assert_eq!(ev(r#"builtins.typeOf "hi""#), Value::string("string"));
assert_eq!(ev("builtins.typeOf [1]"), Value::string("list"));
assert_eq!(ev("builtins.typeOf {}"), Value::string("set"));
assert_eq!(ev("builtins.typeOf (x: x)"), Value::string("lambda"));
}
#[test]
fn builtins_is_type_checks() {
assert_eq!(ev("builtins.isNull null"), Value::Bool(true));
assert_eq!(ev("builtins.isNull 0"), Value::Bool(false));
assert_eq!(ev("builtins.isInt 42"), Value::Bool(true));
assert_eq!(ev("builtins.isInt 3.14"), Value::Bool(false));
assert_eq!(ev("builtins.isBool true"), Value::Bool(true));
assert_eq!(ev("builtins.isBool 1"), Value::Bool(false));
assert_eq!(ev(r#"builtins.isString "x""#), Value::Bool(true));
assert_eq!(ev("builtins.isString 1"), Value::Bool(false));
assert_eq!(ev("builtins.isList []"), Value::Bool(true));
assert_eq!(ev("builtins.isList {}"), Value::Bool(false));
assert_eq!(ev("builtins.isAttrs {}"), Value::Bool(true));
assert_eq!(ev("builtins.isAttrs []"), Value::Bool(false));
assert_eq!(ev("builtins.isFunction (x: x)"), Value::Bool(true));
assert_eq!(ev("builtins.isFunction 1"), Value::Bool(false));
assert_eq!(ev("builtins.isFloat 3.14"), Value::Bool(true));
assert_eq!(ev("builtins.isFloat 1"), Value::Bool(false));
}
#[test]
fn builtins_to_json_from_json_roundtrip() {
assert_eq!(ev("builtins.fromJSON (builtins.toJSON 42)"), Value::Int(42));
assert_eq!(
ev(r#"builtins.fromJSON (builtins.toJSON "hello")"#),
Value::string("hello"),
);
assert_eq!(
ev("builtins.fromJSON (builtins.toJSON [1 2 3])"),
Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)]),
);
assert_eq!(ev("builtins.fromJSON (builtins.toJSON null)"), Value::Null);
assert_eq!(ev("builtins.fromJSON (builtins.toJSON true)"), Value::Bool(true));
}
#[test]
fn builtins_to_string_various() {
assert_eq!(ev("builtins.toString 42"), Value::string("42"));
assert_eq!(ev("builtins.toString true"), Value::string("1"));
assert_eq!(ev("builtins.toString false"), Value::string(""));
assert_eq!(ev("builtins.toString null"), Value::string(""));
assert_eq!(ev(r#"builtins.toString "hello""#), Value::string("hello"));
}
#[test]
fn builtins_function_args() {
let v = ev("builtins.functionArgs ({ a, b ? 1 }: a)");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Bool(false))); assert_eq!(attrs.get("b"), Some(&Value::Bool(true))); } else {
panic!("expected attrs");
}
}
#[test]
fn builtins_gen_list() {
assert_eq!(
ev("builtins.genList (x: x * x) 5"),
Value::list(vec![
Value::Int(0), Value::Int(1), Value::Int(4),
Value::Int(9), Value::Int(16),
]),
);
assert_eq!(ev("builtins.genList (x: x) 0"), Value::list(vec![]));
}
#[test]
fn builtins_elem() {
assert_eq!(ev("builtins.elem 2 [1 2 3]"), Value::Bool(true));
assert_eq!(ev("builtins.elem 5 [1 2 3]"), Value::Bool(false));
assert_eq!(ev("builtins.elem 1 []"), Value::Bool(false));
}
#[test]
fn builtins_head_tail() {
assert_eq!(ev("builtins.head [10 20 30]"), Value::Int(10));
assert_eq!(
ev("builtins.tail [10 20 30]"),
Value::list(vec![Value::Int(20), Value::Int(30)]),
);
}
#[test]
fn builtins_string_length() {
assert_eq!(ev(r#"builtins.stringLength "hello""#), Value::Int(5));
assert_eq!(ev(r#"builtins.stringLength """#), Value::Int(0));
assert_eq!(ev(r#"builtins.stringLength "abc def""#), Value::Int(7));
}
#[test]
fn builtins_ceil_floor() {
assert_eq!(ev("builtins.ceil 2.3"), Value::Int(3));
assert_eq!(ev("builtins.ceil 2.0"), Value::Int(2));
assert_eq!(ev("builtins.floor 2.9"), Value::Int(2));
assert_eq!(ev("builtins.floor 2.0"), Value::Int(2));
assert_eq!(ev("builtins.ceil 5"), Value::Int(5));
assert_eq!(ev("builtins.floor 5"), Value::Int(5));
}
#[test]
fn builtins_try_eval() {
let v = ev("builtins.tryEval 42");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("success"), Some(&Value::Bool(true)));
assert_eq!(attrs.get("value"), Some(&Value::Int(42)));
} else {
panic!("expected attrs");
}
}
#[test]
fn builtins_throw() {
let result = eval(r#"builtins.throw "oops""#);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("oops"));
}
#[test]
fn builtins_seq_deep_seq() {
assert_eq!(ev("builtins.seq 1 42"), Value::Int(42));
assert_eq!(ev("builtins.deepSeq [1 2 3] 99"), Value::Int(99));
}
#[test]
fn builtins_current_system() {
let v = ev("builtins.currentSystem");
if let Value::String(ns) = v {
let s = &ns.chars;
assert!(
s == "aarch64-darwin"
|| s == "x86_64-darwin"
|| s == "aarch64-linux"
|| s == "x86_64-linux",
"unexpected system: {s}",
);
} else {
panic!("expected string");
}
}
#[test]
fn pattern_mkif_like() {
assert_eq!(
ev("(if true then { x = 1; } else {}).x"),
Value::Int(1),
);
let v = ev("if false then { x = 1; } else {}");
if let Value::Attrs(attrs) = v {
assert!(attrs.is_empty());
} else {
panic!("expected attrs");
}
}
#[test]
fn pattern_optional_attrs() {
assert_eq!(
ev("let optionalAttrs = cond: attrs: if cond then attrs else {}; in (optionalAttrs true { a = 1; }).a"),
Value::Int(1),
);
let v = ev("let optionalAttrs = cond: attrs: if cond then attrs else {}; in optionalAttrs false { a = 1; }");
if let Value::Attrs(attrs) = v {
assert!(attrs.is_empty());
} else {
panic!("expected attrs");
}
}
#[test]
fn pattern_filter_attrs_via_remove() {
assert_eq!(
ev(r#"(builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b"]).a"#),
Value::Int(1),
);
assert_eq!(
ev(r#"(builtins.removeAttrs { a = 1; b = 2; c = 3; } ["b"]) ? b"#),
Value::Bool(false),
);
}
#[test]
fn pattern_override() {
let v = ev(r#"
let
defaults = { debug = false; port = 8080; host = "localhost"; };
overrides = { debug = true; port = 9090; };
in defaults // overrides
"#);
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("debug"), Some(&Value::Bool(true)));
assert_eq!(attrs.get("port"), Some(&Value::Int(9090)));
assert_eq!(attrs.get("host"), Some(&Value::string("localhost")));
} else {
panic!("expected attrs");
}
}
#[test]
fn pattern_functor() {
assert_eq!(
ev("let s = { __functor = self: x: self.value + x; value = 10; }; in s 5"),
Value::Int(15),
);
}
#[test]
fn pattern_platform_check() {
let v = ev(r#"if builtins.currentSystem == "aarch64-darwin" then "arm" else "other""#);
if let Value::String(_) = v {
} else {
panic!("expected string");
}
}
#[test]
fn pattern_recursive_overlay_lambda_structure() {
let v = ev("let overlay = self: super: { pkg = 42; }; in overlay {} {}");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("pkg"), Some(&Value::Int(42)));
} else {
panic!("expected attrs");
}
}
#[test]
fn pattern_call_package_simplified() {
assert_eq!(
ev("let callPkg = f: f { lib = { id = x: x; }; }; lib = { id = x: x; }; in callPkg ({ lib }: lib.id 42)"),
Value::Int(42),
);
}
#[test]
fn pattern_derivation_like_attrset() {
let v = ev(r#"{ type = "derivation"; name = "hello"; system = builtins.currentSystem; builder = "/bin/sh"; }"#);
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("type"), Some(&Value::string("derivation")));
assert_eq!(attrs.get("name"), Some(&Value::string("hello")));
assert_eq!(attrs.get("builder"), Some(&Value::string("/bin/sh")));
let system = force_value(attrs.get("system").unwrap()).unwrap();
assert!(matches!(system, Value::String(_)), "expected string, got {system:?}");
} else {
panic!("expected attrs");
}
}
#[test]
fn pattern_module_system_simplified() {
assert_eq!(
ev(r#"
let
eval = m: m { config = {}; lib = { mkDefault = x: x; }; };
in eval ({ config, lib }: { result = lib.mkDefault 42; })
"#),
{
let mut attrs = NixAttrs::new();
attrs.insert("result".to_string(), Value::Int(42));
Value::Attrs(Rc::new(attrs))
},
);
}
#[test]
fn error_undefined_variable() {
let result = eval("nonexistent_var");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("undefined variable") || msg.contains("nonexistent_var"));
}
#[test]
fn error_type_mismatch_arithmetic() {
let result = eval(r#"1 + "hello""#);
assert!(result.is_err());
}
#[test]
fn error_missing_attribute() {
let result = eval("{}.nonexistent");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("nonexistent") || msg.contains("not found"));
}
#[test]
fn error_division_by_zero() {
assert!(eval("1 / 0").is_err());
assert!(eval("100 / 0").is_err());
}
#[test]
fn error_missing_required_function_arg() {
let result = eval("({ a, b }: a + b) { a = 1; }");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("missing argument"));
}
#[test]
fn error_unexpected_function_arg() {
let result = eval("({ a }: a) { a = 1; b = 2; }");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("unexpected argument"));
}
#[test]
fn error_assertion_failure() {
assert!(eval("assert false; 1").is_err());
assert!(eval("assert 1 == 2; 1").is_err());
}
#[test]
fn error_infinite_recursion() {
let result = eval("let x = x; in x");
assert!(result.is_err());
}
#[test]
fn error_infinite_recursion_via_lambda() {
let result = eval("let f = x: f x; in f 1");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("infinite recursion") || msg.contains("eval depth") || msg.contains("undefined"),
);
}
#[test]
fn integration_let_with_function_returning_attrset() {
assert_eq!(
ev("let mkPkg = name: { inherit name; version = 1; }; in (mkPkg \"hello\").name"),
Value::string("hello"),
);
}
#[test]
fn integration_chained_updates() {
assert_eq!(
ev("({ a = 1; } // { b = 2; } // { c = 3; }).c"),
Value::Int(3),
);
}
#[test]
fn integration_map_over_attrnames() {
assert_eq!(
ev(r#"
let
set = { a = 1; b = 2; };
names = builtins.attrNames set;
in builtins.length names
"#),
Value::Int(2),
);
}
#[test]
fn integration_compose_functions() {
assert_eq!(
ev("let compose = f: g: x: f (g x); double = x: x * 2; inc = x: x + 1; in compose double inc 5"),
Value::Int(12), );
}
#[test]
fn integration_recursive_list_building() {
assert_eq!(
ev("builtins.map (x: x * x) (builtins.genList (x: x + 1) 4)"),
Value::list(vec![Value::Int(1), Value::Int(4), Value::Int(9), Value::Int(16)]),
);
}
#[test]
fn integration_attrset_from_list() {
let v = ev(r#"
builtins.listToAttrs (builtins.map (x: { name = x; value = true; }) ["a" "b" "c"])
"#);
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Bool(true)));
assert_eq!(attrs.get("b"), Some(&Value::Bool(true)));
assert_eq!(attrs.get("c"), Some(&Value::Bool(true)));
} else {
panic!("expected attrs");
}
}
#[test]
fn integration_nested_with_and_let() {
assert_eq!(
ev("let x = 10; in with { y = 20; }; x + y"),
Value::Int(30),
);
}
#[test]
fn integration_complex_pattern_match() {
assert_eq!(
ev("(args @ { a, b ? 5, ... }: a + b + (if args ? c then args.c else 0)) { a = 1; c = 10; }"),
Value::Int(16), );
}
#[test]
fn integration_substring() {
assert_eq!(
ev(r#"builtins.substring 0 5 "hello world""#),
Value::string("hello"),
);
assert_eq!(
ev(r#"builtins.substring 6 5 "hello world""#),
Value::string("world"),
);
}
#[test]
fn integration_has_attr_on_nested() {
assert_eq!(ev("{ a = { b = 1; }; } ? a"), Value::Bool(true));
assert_eq!(
ev("({ a = { b = 1; }; }.a) ? b"),
Value::Bool(true),
);
}
#[test]
fn integration_cat_attrs() {
assert_eq!(
ev(r#"builtins.catAttrs "x" [{ x = 1; } { y = 2; } { x = 3; }]"#),
Value::list(vec![Value::Int(1), Value::Int(3)]),
);
}
#[test]
fn integration_get_attr_builtin() {
assert_eq!(
ev(r#"builtins.getAttr "a" { a = 42; b = 10; }"#),
Value::Int(42),
);
}
#[test]
fn integration_has_attr_builtin() {
assert_eq!(
ev(r#"builtins.hasAttr "a" { a = 1; }"#),
Value::Bool(true),
);
assert_eq!(
ev(r#"builtins.hasAttr "z" { a = 1; }"#),
Value::Bool(false),
);
}
#[test]
fn integration_is_path() {
assert_eq!(ev("builtins.isPath ./foo"), Value::Bool(true));
assert_eq!(ev("builtins.isPath 42"), Value::Bool(false));
}
#[test]
fn integration_builtins_trace() {
assert_eq!(ev(r#"builtins.trace "debug msg" 42"#), Value::Int(42));
}
#[test]
fn integration_builtins_split() {
assert_eq!(
ev(r#"builtins.split "/" "a/b/c""#),
Value::list(vec![
Value::string("a"),
Value::list(vec![]),
Value::string("b"),
Value::list(vec![]),
Value::string("c"),
]),
);
assert_eq!(
ev(r#"builtins.split "(/)" "a/b/c""#),
Value::list(vec![
Value::string("a"),
Value::list(vec![Value::string("/")]),
Value::string("b"),
Value::list(vec![Value::string("/")]),
Value::string("c"),
]),
);
}
#[test]
fn integration_builtins_split_no_capture_groups() {
assert_eq!(
ev(r#"builtins.split "-" "aarch64-darwin""#),
Value::list(vec![
Value::string("aarch64"),
Value::list(vec![]),
Value::string("darwin"),
]),
);
}
#[test]
fn integration_builtins_split_system_string_filter() {
assert_eq!(
ev(r#"builtins.filter builtins.isString (builtins.split "-" "aarch64-darwin")"#),
Value::list(vec![
Value::string("aarch64"),
Value::string("darwin"),
]),
);
}
#[test]
fn integration_deeply_nested_let() {
assert_eq!(
ev("let a = let b = let c = 10; in c * 2; in b + 1; in a"),
Value::Int(21),
);
}
#[test]
fn integration_if_in_attrset_value() {
assert_eq!(
ev("{ x = if true then 1 else 2; }.x"),
Value::Int(1),
);
}
#[test]
fn integration_lambda_in_list() {
assert_eq!(
ev("let fs = [(x: x + 1) (x: x * 2)]; in (builtins.elemAt fs 0) 5"),
Value::Int(6),
);
assert_eq!(
ev("let fs = [(x: x + 1) (x: x * 2)]; in (builtins.elemAt fs 1) 5"),
Value::Int(10),
);
}
#[test]
fn integration_nixpkgs_lib_id() {
assert_eq!(
ev("let lib = { id = x: x; const = a: b: a; }; in lib.id 42"),
Value::Int(42),
);
assert_eq!(
ev("let lib = { id = x: x; const = a: b: a; }; in lib.const 1 2"),
Value::Int(1),
);
}
#[test]
fn integration_multiple_inherit() {
assert_eq!(
ev("let a = 1; b = 2; c = 3; in { inherit a b c; }.b"),
Value::Int(2),
);
}
#[test]
fn integration_rec_set_with_builtins() {
assert_eq!(
ev(r#"(rec { a = "hello"; b = builtins.stringLength a; }).b"#),
Value::Int(5),
);
}
#[test]
fn functor_simple_callable_attrset() {
assert_eq!(
ev("let s = { __functor = self: x: x + 1; }; in s 41"),
Value::Int(42),
);
}
#[test]
fn functor_with_self_reference() {
assert_eq!(
ev("let s = { __functor = self: x: self.base + x; base = 100; }; in s 23"),
Value::Int(123),
);
}
#[test]
fn functor_updated_attrset() {
assert_eq!(
ev(r#"
let
mk = { __functor = self: x: self.n + x; n = 0; };
s = mk // { n = 50; };
in s 7
"#),
Value::Int(57),
);
}
#[test]
fn functor_error_on_non_callable_attrset() {
let result = eval("let s = { a = 1; }; in s 5");
assert!(result.is_err());
}
#[test]
fn to_string_protocol_in_interpolation() {
assert_eq!(
ev(r#"let s = { __toString = self: "world"; }; in "hello ${s}""#),
Value::string("hello world"),
);
}
#[test]
fn to_string_protocol_accesses_self() {
assert_eq!(
ev(r#"let s = { __toString = self: self.val; val = "abc"; }; in "${s}""#),
Value::string("abc"),
);
}
#[test]
fn to_string_protocol_via_builtin_to_string() {
assert_eq!(
ev(r#"builtins.toString { __toString = self: "via-builtin"; }"#),
Value::string("via-builtin"),
);
}
#[test]
fn to_string_protocol_attrset_without_toString_fails() {
let result = eval(r#""${{}}"#);
assert!(result.is_err());
}
#[test]
fn eval_builtins_concat_strings_is_not_a_builtin() {
assert_eq!(ev(r#"builtins ? concatStrings"#), Value::Bool(false));
assert!(
eval(r#"builtins.concatStrings ["a" "b" "c"]"#).is_err(),
"builtins.concatStrings must fail the way real nix fails it"
);
assert_eq!(
ev(r#"builtins.concatStringsSep "" ["a" "b" "c"]"#),
Value::string("abc"),
);
assert_eq!(
ev(r#"builtins.concatStringsSep "" []"#),
Value::string(""),
);
}
#[test]
fn eval_builtins_partition() {
let v = ev("builtins.partition (x: x > 3) [1 2 3 4 5]");
if let Value::Attrs(a) = v {
assert_eq!(a.get("right"), Some(&Value::list(vec![Value::Int(4), Value::Int(5)])));
assert_eq!(a.get("wrong"), Some(&Value::list(vec![Value::Int(1), Value::Int(2), Value::Int(3)])));
} else {
panic!("expected attrs");
}
}
#[test]
fn eval_builtins_group_by() {
let v = ev(r#"builtins.groupBy (x: if x > 0 then "pos" else "neg") [1 (0 - 2) 3 (0 - 4)]"#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("pos"), Some(&Value::list(vec![Value::Int(1), Value::Int(3)])));
assert_eq!(a.get("neg"), Some(&Value::list(vec![Value::Int(-2), Value::Int(-4)])));
} else {
panic!("expected attrs");
}
}
#[test]
fn eval_builtins_zip_attrs_with() {
let v = ev("builtins.zipAttrsWith (n: vs: builtins.head vs) [{ a = 1; } { a = 2; b = 3; }]");
if let Value::Attrs(a) = v {
assert_eq!(a.get("a"), Some(&Value::Int(1)));
assert_eq!(a.get("b"), Some(&Value::Int(3)));
} else {
panic!("expected attrs");
}
}
#[test]
fn eval_builtins_compare_versions() {
assert_eq!(ev(r#"builtins.compareVersions "2.0" "1.0""#), Value::Int(1));
assert_eq!(ev(r#"builtins.compareVersions "1.0" "2.0""#), Value::Int(-1));
assert_eq!(ev(r#"builtins.compareVersions "1.0" "1.0""#), Value::Int(0));
}
#[test]
fn eval_builtins_parse_drv_name() {
let v = ev(r#"builtins.parseDrvName "nix-2.3.4""#);
if let Value::Attrs(a) = v {
assert_eq!(a.get("name"), Some(&Value::string("nix")));
assert_eq!(a.get("version"), Some(&Value::string("2.3.4")));
} else {
panic!("expected attrs");
}
}
#[test]
fn eval_builtins_base_name_of() {
assert_eq!(
ev(r#"builtins.baseNameOf "/foo/bar/baz""#),
Value::string("baz"),
);
}
#[test]
fn eval_builtins_dir_of() {
assert_eq!(
ev(r#"builtins.dirOf "/foo/bar/baz""#),
Value::string("/foo/bar"),
);
}
#[test]
fn eval_builtins_add_error_context() {
assert_eq!(
ev(r#"builtins.addErrorContext "some context" 42"#),
Value::Int(42),
);
}
#[test]
fn eval_builtins_abort() {
let result = eval(r#"builtins.abort "fatal error""#);
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("fatal error"));
}
#[test]
fn indented_string_simple() {
assert_eq!(ev("''hello''"), Value::string("hello"));
}
#[test]
fn indented_string_multiline_strips_indent() {
assert_eq!(
ev("''\n line1\n line2\n''"),
Value::string("line1\nline2\n"),
);
}
#[test]
fn indented_string_with_interpolation() {
let code = "let x = \"world\"; in ''hello ${x}''";
assert_eq!(
ev(code),
Value::string("hello world"),
);
}
#[test]
fn indented_string_deeper_indent_preserved() {
assert_eq!(
ev("''\n a\n b\n''"),
Value::string("a\n b\n"),
);
}
#[test]
fn dynamic_attr_name_in_set() {
assert_eq!(
ev(r#"let key = "mykey"; in { ${key} = 42; }.mykey"#),
Value::Int(42),
);
}
#[test]
fn dynamic_attr_name_with_expression() {
assert_eq!(
ev(r#"let prefix = "foo"; in { ${"${prefix}bar"} = 1; }.foobar"#),
Value::Int(1),
);
}
#[test]
fn eval_builtins_match() {
assert_eq!(
ev(r#"builtins.match "([0-9]+)" "42""#),
Value::list(vec![Value::string("42")]),
);
}
#[test]
fn eval_builtins_hash_string() {
let v = ev(r#"builtins.hashString "sha256" "hello""#);
if let Value::String(ns) = v {
assert_eq!(ns.chars.len(), 64);
} else {
panic!("expected string");
}
}
#[test]
fn eval_builtins_import() {
let dir = std::env::temp_dir();
let path = dir.join("sui_eval_test_import_eval.nix");
std::fs::write(&path, "42").unwrap();
let expr = format!(r#"import "{}""#, path.display());
let v = eval(&expr).unwrap();
assert_eq!(v, Value::Int(42));
std::fs::remove_file(&path).ok();
}
#[test]
fn eval_builtins_derivation() {
let v = eval(r#"builtins.derivation { name = "test"; system = "x86_64-linux"; builder = "/bin/sh"; }"#).unwrap();
if let Value::Attrs(a) = v {
assert_eq!(a.get("type"), Some(&Value::string("derivation")));
} else {
panic!("expected attrs");
}
}
#[test]
fn eval_mutual_recursive_let() {
let v = eval("let a = { x = b; }; b = { y = a; }; in a.x.y");
assert!(v.is_ok(), "mutual recursive let should not error: {v:?}");
let val = v.unwrap();
assert!(
matches!(val, Value::Attrs(_)),
"a.x.y should be an attrset, got: {val:?}",
);
}
#[test]
fn eval_mutual_recursive_let_simple() {
let v = eval("let a = b; b = 42; in a");
assert!(v.is_ok());
assert_eq!(v.unwrap(), Value::Int(42));
}
#[test]
fn eval_builtins_read_dir() {
let dir = std::env::temp_dir().join("sui_eval_test_readdir_eval");
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
std::fs::write(dir.join("a.txt"), "").unwrap();
let expr = format!(r#"builtins.readDir "{}""#, dir.display());
let v = eval(&expr).unwrap();
if let Value::Attrs(a) = v {
assert_eq!(a.get("a.txt"), Some(&Value::string("regular")));
} else {
panic!("expected attrs");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn thunk_basic_let() {
assert_eq!(ev("let x = 1; in x"), Value::Int(1));
}
#[test]
fn thunk_forward_ref() {
assert_eq!(ev("let a = b; b = 1; in a"), Value::Int(1));
}
#[test]
fn thunk_mutual_rec_attrset_in_let() {
assert_eq!(ev("let a = { x = b; }; b = { y = 1; }; in a.x.y"), Value::Int(1));
}
#[test]
fn thunk_rec_attrset() {
assert_eq!(ev("(rec { a = b; b = 1; }).a"), Value::Int(1));
}
#[test]
fn thunk_rec_attrset_chain() {
assert_eq!(ev("(rec { a = 1; b = a + 1; c = b + 1; }).c"), Value::Int(3));
}
#[test]
fn thunk_fixpoint() {
assert_eq!(
ev("let fix = f: let x = f x; in x; in (fix (self: { a = 1; b = self.a + 1; })).b"),
Value::Int(2),
);
}
#[test]
fn thunk_blackhole_self_reference() {
let result = eval("let x = x; in x");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(
msg.contains("infinite recursion") || msg.contains("blackhole"),
"expected blackhole error, got: {msg}",
);
}
#[test]
fn thunk_mutual_blackhole() {
let result = eval("let a = b; b = a; in a");
assert!(result.is_err());
}
#[test]
fn thunk_let_body_forces_correctly() {
assert_eq!(ev("let a = 10; b = 20; in a + b"), Value::Int(30));
}
#[test]
fn thunk_only_forced_when_needed() {
assert_eq!(ev("let bad = 1 / 0; good = 42; in good"), Value::Int(42));
}
#[test]
fn thunk_forward_ref_in_function_body() {
assert_eq!(
ev("let f = x: x + b; b = 10; in f 5"),
Value::Int(15),
);
}
#[test]
fn thunk_rec_set_self_ref_through_self() {
assert_eq!(
ev(r#"(rec { a = "hello"; b = builtins.stringLength a; }).b"#),
Value::Int(5),
);
}
#[test]
fn thunk_nested_let_forward_ref() {
assert_eq!(
ev("let a = b + 1; b = 2; in a"),
Value::Int(3),
);
}
#[test]
fn thunk_deep_chain() {
assert_eq!(
ev("let a = 1; b = a; c = b; d = c; e = d; in e"),
Value::Int(1),
);
}
#[test]
fn thunk_rec_set_fixpoint() {
assert_eq!(
ev("let fix = f: let x = f x; in x; in (fix (self: { a = 1; b = self.a + 1; c = self.b + 1; })).c"),
Value::Int(3),
);
}
#[test]
fn thunk_let_with_inherit() {
assert_eq!(
ev("let a = 1; in let inherit a; b = a + 1; in b"),
Value::Int(2),
);
}
#[test]
fn thunk_attrset_value_lazy() {
assert_eq!(
ev("let x = 42; in { a = x; }.a"),
Value::Int(42),
);
}
#[test]
fn thunk_unused_error_not_forced() {
assert_eq!(
ev(r#"let bad = builtins.throw "boom"; ok = 1; in ok"#),
Value::Int(1),
);
}
#[test]
fn thunk_rec_set_mutual_reference() {
let v = ev("rec { a = { val = b.val + 1; }; b = { val = 10; }; }");
if let Value::Attrs(attrs) = v {
let a = attrs.get("a").unwrap();
let a_forced = force_value(a).unwrap();
if let Value::Attrs(a_attrs) = a_forced {
assert_eq!(a_attrs.get("val"), Some(&Value::Int(11)));
} else {
panic!("expected attrs for a");
}
} else {
panic!("expected attrs");
}
}
#[test]
fn let_rec_self_reference_simple() {
assert_eq!(
ev("let x = 1; y = x + 1; in y"),
Value::Int(2),
);
}
#[test]
fn let_rec_self_reference_chain() {
assert_eq!(
ev("let a = 1; b = a + 1; c = b + 1; in c"),
Value::Int(3),
);
}
#[test]
fn let_rec_self_reference_with_function() {
assert_eq!(
ev("let f = x: x + 1; y = f 10; in y"),
Value::Int(11),
);
}
#[test]
fn let_rec_mutual_recursion_via_if() {
assert_eq!(
ev("let isEven = n: if n == 0 then true else isOdd (n - 1); isOdd = n: if n == 0 then false else isEven (n - 1); in isEven 4"),
Value::Bool(true),
);
}
#[test]
fn let_rec_forward_ref_in_list() {
assert_eq!(
ev("let xs = [a b]; a = 1; b = 2; in builtins.length xs"),
Value::Int(2),
);
}
#[test]
fn with_shadowing_let_wins_over_with() {
assert_eq!(
ev("let x = 1; in with { x = 2; }; x"),
Value::Int(1),
);
}
#[test]
fn with_shadowing_inner_with_wins() {
assert_eq!(
ev("with { x = 1; }; with { x = 2; }; x"),
Value::Int(2),
);
}
#[test]
fn with_shadowing_outer_provides_missing() {
assert_eq!(
ev("with { x = 1; y = 10; }; with { x = 2; }; x + y"),
Value::Int(12),
);
}
#[test]
fn with_shadowing_lambda_arg_wins() {
assert_eq!(
ev("(x: with { x = 99; }; x) 42"),
Value::Int(42),
);
}
#[test]
fn with_shadowing_nested_let_wins_over_with() {
assert_eq!(
ev("with { x = 1; }; let x = 2; in x"),
Value::Int(2),
);
}
#[test]
fn with_scope_dynamic_attrs() {
assert_eq!(
ev(r#"with { x = 1; y = 2; z = 3; }; x + y + z"#),
Value::Int(6),
);
}
#[test]
fn with_scope_over_lazy_thunk_chain_resolves() {
assert_eq!(
ev(r#"let outer = if true then (if true then { unix = 42; } else {}) else {};
# force a two-deep lazy wrap of the with-head
head = (x: x) ((y: y) outer);
in with head; unix"#),
Value::Int(42),
);
}
#[test]
fn with_scope_head_from_deep_select_resolves() {
assert_eq!(
ev(r#"let a = { b = { c = { key = 7; }; }; }; in with a.b.c; key"#),
Value::Int(7),
);
}
#[test]
fn attrset_deep_merge_simple() {
let v = ev("{ a.b = 1; a.c = 2; }");
if let Value::Attrs(attrs) = v {
let a = force_value(attrs.get("a").unwrap()).unwrap();
if let Value::Attrs(inner) = a {
assert_eq!(force_value(inner.get("b").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(inner.get("c").unwrap()).unwrap(), Value::Int(2));
} else {
panic!("expected nested attrs");
}
} else {
panic!("expected attrs");
}
}
#[test]
fn attrset_deep_merge_three_levels() {
let v = ev("{ a.b.c = 1; a.b.d = 2; a.e = 3; }");
if let Value::Attrs(attrs) = v {
let a = force_value(attrs.get("a").unwrap()).unwrap();
if let Value::Attrs(a_inner) = a {
let e = force_value(a_inner.get("e").unwrap()).unwrap();
assert_eq!(e, Value::Int(3));
let b = force_value(a_inner.get("b").unwrap()).unwrap();
if let Value::Attrs(b_inner) = b {
assert_eq!(force_value(b_inner.get("c").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(b_inner.get("d").unwrap()).unwrap(), Value::Int(2));
} else {
panic!("expected nested attrs for b");
}
} else {
panic!("expected nested attrs for a");
}
} else {
panic!("expected attrs");
}
}
#[test]
fn attrset_deep_merge_preserves_siblings() {
assert_eq!(
ev("{ a.x = 1; b = 2; a.y = 3; }.b"),
Value::Int(2),
);
}
#[test]
fn attrset_deep_merge_in_let() {
let v = ev("let s = { a.b = 1; a.c = 2; }; in s.a.b + s.a.c");
assert_eq!(v, Value::Int(3));
}
#[test]
fn attrset_deep_merge_fullset_then_dotted() {
let v = ev("let s = { a = { x = 1; }; a.y = 2; }; in s.a.x + s.a.y");
assert_eq!(v, Value::Int(3));
let both = ev("let s = { a = { x = 1; }; a.y = 2; }; in [ s.a.x s.a.y ]");
if let Value::List(items) = both {
assert_eq!(force_value(&items[0]).unwrap(), Value::Int(1));
assert_eq!(force_value(&items[1]).unwrap(), Value::Int(2));
} else {
panic!("expected list");
}
}
#[test]
fn inherit_from_basic() {
assert_eq!(
ev("let s = { x = 1; y = 2; }; in let inherit (s) x y; in x + y"),
Value::Int(3),
);
}
#[test]
fn inherit_from_with_shadowing() {
assert_eq!(
ev("let x = 10; in let inherit ({ x = 20; }) x; in x"),
Value::Int(20),
);
}
#[test]
fn inherit_from_in_attrset() {
let v = ev(r#"let s = { a = 1; b = 2; }; in { inherit (s) a b; c = 3; }"#);
if let Value::Attrs(attrs) = v {
assert_eq!(force_value(attrs.get("a").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(attrs.get("b").unwrap()).unwrap(), Value::Int(2));
assert_eq!(force_value(attrs.get("c").unwrap()).unwrap(), Value::Int(3));
} else {
panic!("expected attrs");
}
}
#[test]
fn inherit_from_rec_set() {
assert_eq!(
ev("rec { inherit ({ x = 42; }) x; y = x; }.y"),
Value::Int(42),
);
}
#[test]
fn inherit_plain_from_scope() {
assert_eq!(
ev("let x = 1; in { inherit x; }.x"),
Value::Int(1),
);
}
#[test]
fn inherit_plain_from_with_scope_lazy() {
assert_eq!(
ev("let fix = f: let x = f x; in x;
self = fix (self: with self; {
a = use { inherit cp; };
use = { cp }: cp 5;
cp = x: x + 100;
});
in self.a"),
Value::Int(105),
);
assert_eq!(
ev("with { y = 7; }; { inherit y; }.y"),
Value::Int(7),
);
}
#[test]
fn inherit_multiple_from_expr() {
assert_eq!(
ev("let s = { a = 10; b = 20; c = 30; }; in let inherit (s) a b c; in a + b + c"),
Value::Int(60),
);
}
#[test]
fn interp_nested_attrset_access() {
assert_eq!(
ev(r#"let x = { a = "hello"; }; in "${x.a} world""#),
Value::string("hello world"),
);
}
#[test]
fn interp_with_let_expression() {
assert_eq!(
ev(r#""${let x = "inner"; in x}""#),
Value::string("inner"),
);
}
#[test]
fn interp_float_coercion() {
assert_eq!(
ev(r#""${toString 3.14}""#),
Value::string("3.140000"),
);
}
#[test]
fn compare_mixed_int_float() {
assert_eq!(ev("1 < 1.5"), Value::Bool(true));
assert_eq!(ev("1.5 > 1"), Value::Bool(true));
assert_eq!(ev("2.0 == 2"), Value::Bool(true));
}
#[test]
fn compare_string_lexicographic() {
assert_eq!(ev(r#""abc" < "abd""#), Value::Bool(true));
assert_eq!(ev(r#""abc" < "abc""#), Value::Bool(false));
assert_eq!(ev(r#""abc" <= "abc""#), Value::Bool(true));
}
#[test]
fn update_empty_sets() {
let v = ev("{} // {}");
if let Value::Attrs(a) = v { assert!(a.is_empty()); } else { panic!(); }
}
#[test]
fn update_right_overrides_completely() {
assert_eq!(
ev("{ a = 1; b = 2; } // { a = 10; c = 30; }"),
ev("{ a = 10; b = 2; c = 30; }"),
);
}
#[test]
fn update_chained() {
assert_eq!(
ev("{ a = 1; } // { b = 2; } // { c = 3; }"),
ev("{ a = 1; b = 2; c = 3; }"),
);
}
#[test]
fn force_value_concrete_unchanged() {
let v = Value::Int(42);
assert_eq!(force_value(&v).unwrap(), Value::Int(42));
}
#[test]
fn force_value_null() {
assert_eq!(force_value(&Value::Null).unwrap(), Value::Null);
}
#[test]
fn eval_with_file_none() {
let result = eval_with_file("1 + 2", None).unwrap();
assert_eq!(result, Value::Int(3));
}
#[test]
fn error_type_mismatch_in_comparison() {
let result = eval(r#"1 < "a""#);
assert!(result.is_err());
}
#[test]
fn error_select_from_non_set() {
let result = eval("42.x");
assert!(result.is_err());
}
#[test]
fn error_call_non_function() {
let result = eval("42 1");
assert!(result.is_err());
}
#[test]
fn error_negate_string() {
let result = eval(r#"-"hello""#);
assert!(result.is_err());
}
#[test]
fn multiline_string_empty() {
assert_eq!(ev("''''"), Value::string(""));
}
#[test]
fn multiline_string_with_trailing_newline() {
let v = ev("''\n hello\n''");
assert_eq!(v, Value::string("hello\n"));
}
#[test]
fn list_concat_empty_left() {
assert_eq!(ev("[] ++ [1 2]"), Value::list(vec![Value::Int(1), Value::Int(2)]));
}
#[test]
fn list_concat_empty_right() {
assert_eq!(ev("[1 2] ++ []"), Value::list(vec![Value::Int(1), Value::Int(2)]));
}
#[test]
fn list_concat_both_empty() {
assert_eq!(ev("[] ++ []"), Value::list(vec![]));
}
#[test]
fn formals_at_pattern_accessible() {
assert_eq!(
ev("({ x, ... } @ args: builtins.length (builtins.attrNames args)) { x = 1; y = 2; z = 3; }"),
Value::Int(3),
);
}
#[test]
fn formals_default_uses_other_arg() {
assert_eq!(
ev("({ x, y ? x + 1 }: y) { x = 10; }"),
Value::Int(11),
);
}
#[test]
fn formals_default_lazy_assert_false() {
assert_eq!(
ev("({ cpu, vendor ? assert false; null, kernel } @ args: if args ? vendor then vendor else \"inferred\") { cpu = \"x86_64\"; kernel = \"linux\"; }"),
Value::String(Rc::new(NixString::plain("inferred"))),
);
}
#[test]
fn formals_default_lazy_only_forced_when_accessed() {
assert_eq!(
ev("({ a, b ? 42 }: b) { a = 1; }"),
Value::Int(42),
);
}
#[test]
fn formals_ellipsis_ignores_extra() {
assert_eq!(
ev("({ x, ... }: x) { x = 1; y = 2; z = 3; }"),
Value::Int(1),
);
}
#[test]
fn pure_mode_roundtrip() {
let was_pure = is_pure_mode();
set_pure_mode(true);
assert!(is_pure_mode());
set_pure_mode(false);
assert!(!is_pure_mode());
set_pure_mode(was_pure);
}
#[test]
fn path_concat_with_string() {
assert_eq!(
ev(r#"/foo + "bar""#),
Value::Path(Box::new(SmolStr::from("/foobar"))),
);
}
#[test]
fn path_concat_with_path() {
assert_eq!(
ev("/foo + /bar"),
Value::Path(Box::new(SmolStr::from("/foo//bar"))),
);
}
#[test]
fn current_eval_dir_empty_when_no_file_pushed() {
let snapshot = current_eval_dir();
let _ = snapshot;
}
#[test]
fn push_eval_file_sets_current_dir() {
let p = std::path::PathBuf::from("/tmp/example/file.nix");
{
let _g = push_eval_file(p.clone());
assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/tmp/example")));
}
}
#[test]
fn push_eval_file_nested_stack() {
let outer = std::path::PathBuf::from("/a/x.nix");
let inner = std::path::PathBuf::from("/b/y.nix");
{
let _g_outer = push_eval_file(outer.clone());
assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/a")));
{
let _g_inner = push_eval_file(inner.clone());
assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/b")));
}
assert_eq!(current_eval_dir(), Some(std::path::PathBuf::from("/a")));
}
}
#[test]
fn fileless_frame_masks_parent_file() {
let outer = std::path::PathBuf::from("/a/x.nix");
let _g_outer = push_eval_file(outer.clone());
assert_eq!(current_eval_file(), Some(outer.clone()));
{
let _g_none = push_eval_frame(None);
assert_eq!(current_eval_file(), None);
assert_eq!(current_eval_dir(), None);
assert_eq!(eval_file_stack_snapshot().last().map(String::as_str), Some("<no-file>"));
}
assert_eq!(current_eval_file(), Some(outer));
}
#[test]
fn error_undefined_var_includes_file_context() {
let p = std::path::PathBuf::from("/nix/store/abc-default.nix");
let _g = push_eval_file(p);
let result = eval("nonexistent_xyz");
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("undefined variable"), "msg: {msg}");
assert!(msg.contains("nonexistent_xyz"), "msg: {msg}");
assert!(msg.contains("abc-default.nix"), "msg: {msg}");
}
#[test]
fn error_attr_not_found_includes_file_context() {
let p = std::path::PathBuf::from("/nix/store/xyz-module.nix");
let _g = push_eval_file(p);
let result = eval("{}.missing_key");
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("not found") || msg.contains("missing_key"), "msg: {msg}");
assert!(msg.contains("xyz-module.nix"), "msg: {msg}");
}
#[test]
fn error_assertion_failed_includes_file_context() {
let p = std::path::PathBuf::from("/nix/store/test-assert.nix");
let _g = push_eval_file(p);
let result = eval("assert false; 1");
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("assertion failed"), "msg: {msg}");
assert!(msg.contains("test-assert.nix"), "msg: {msg}");
}
#[test]
fn inherit_bindings_carry_positions() {
let dir = tempfile::tempdir().unwrap();
let body = "{ inherit ({ x = 1; }) x; }\n";
let f = dir.path().join("inh.nix");
std::fs::write(&f, body).unwrap();
let v = eval(&format!("builtins.unsafeGetAttrPos \"x\" (import {})", f.display())).unwrap();
let attrs = match v {
Value::Attrs(a) => a,
Value::Null => panic!("null — the inherit binding carried no position"),
o => panic!("expected attrs, got {o:?}"),
};
let off = body.rfind("x; }").unwrap();
let bol = body[..off].rfind('\n').map_or(0, |i| i + 1);
assert_eq!(*attrs.get("line").unwrap(), Value::Int(1));
assert_eq!(*attrs.get("column").unwrap(), Value::Int((off - bol) as i64 + 1));
}
#[test]
fn every_binding_form_carries_a_position() {
let dir = tempfile::tempdir().unwrap();
let body = concat!(
"let src = { i = 1; j = 2; }; in {\n",
" plain = 1;\n",
" \"quoted\" = 2;\n",
" inherit (src) i;\n",
" inherit src;\n",
" nested.deep = 3;\n",
"}\n",
);
let f = dir.path().join("forms.nix");
std::fs::write(&f, body).unwrap();
let keys = ["plain", "quoted", "i", "src", "nested"];
let probe = keys
.iter()
.map(|k| format!(
"(let q = builtins.unsafeGetAttrPos \"{k}\" t; \
in if q == null then \"{k}=NULL\" \
else \"{k}=${{toString q.line}}:${{toString q.column}}\")"
))
.collect::<Vec<_>>()
.join(" + \" \" + ");
let got = eval(&format!("let t = import {}; in {probe}", f.display()))
.unwrap()
.as_string()
.unwrap()
.to_string();
assert!(!got.contains("NULL"), "a binding form lost its position: {got}");
let rows: Vec<&str> = got.split(' ').collect();
assert_eq!(rows.len(), keys.len(), "corpus shrank — gate would be vacuous: {got}");
for (k, row) in keys.iter().zip(&rows) {
let needle = match *k {
"quoted" => "\"quoted\"".to_string(),
"i" => "i;".to_string(),
"src" => "src;".to_string(),
"nested" => "nested.".to_string(),
other => format!("{other} ="),
};
let off = body.find(&needle).unwrap();
let bol = body[..off].rfind('\n').map_or(0, |i| i + 1);
let line = 1 + body[..off].matches('\n').count();
let col = off - bol + 1;
assert_eq!(*row, format!("{k}={line}:{col}"), "wrong position for `{k}` in:\n{body}");
}
}
#[test]
fn error_missing_argument_includes_file_context() {
let p = std::path::PathBuf::from("/nix/store/func.nix");
let result = eval_with_file("({ a, b }: a) { a = 1; }", Some(p));
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("missing argument"), "msg: {msg}");
assert!(msg.contains("func.nix"), "msg: {msg}");
}
#[test]
fn error_cannot_call_includes_file_context() {
let p = std::path::PathBuf::from("/nix/store/call.nix");
let _g = push_eval_file(p);
let result = eval("42 99");
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("cannot call"), "msg: {msg}");
assert!(msg.contains("call.nix"), "msg: {msg}");
}
#[test]
fn error_without_file_has_no_in_prefix() {
let result = eval("nonexistent_xyz");
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("undefined variable"), "msg: {msg}");
assert!(!msg.contains(", in"), "msg should not contain file context: {msg}");
}
#[test]
fn pure_mode_set_get_independence() {
let was = is_pure_mode();
set_pure_mode(true);
assert!(is_pure_mode());
set_pure_mode(false);
assert!(!is_pure_mode());
set_pure_mode(was);
}
#[test]
fn eval_with_file_some_path_arithmetic() {
let p = std::path::PathBuf::from("/tmp/imaginary.nix");
let result = eval_with_file("1 + 2", Some(p)).unwrap();
assert_eq!(result, Value::Int(3));
}
#[test]
fn unsafe_get_attr_pos_reports_file_and_offset_column() {
let dir = tempfile::tempdir().unwrap();
let file_body = "{ a = 1;\n b = 2; }\n";
let f = dir.path().join("lit.nix");
std::fs::write(&f, file_body).unwrap();
let src = format!("builtins.unsafeGetAttrPos \"b\" (import {})", f.display());
let v = eval(&src).unwrap();
let attrs = match v { Value::Attrs(a) => a, other => panic!("expected attrs, got {other:?}") };
assert_eq!(
attrs.get("file").unwrap().as_string().unwrap(),
f.to_string_lossy(),
);
let off = file_body.find("b = 2").unwrap();
let bol = file_body[..off].rfind('\n').map_or(0, |i| i + 1);
let expected_line = 1 + file_body[..off].matches('\n').count() as i64;
let expected_col = (off - bol) as i64 + 1;
assert_eq!(expected_line, 2, "fixture must put `b` on line 2");
assert_eq!(*attrs.get("line").unwrap(), Value::Int(expected_line));
let col = match attrs.get("column").unwrap() { Value::Int(n) => *n, o => panic!("{o:?}") };
assert_eq!(col, expected_col, "column must be the 1-based BYTE column");
}
#[test]
fn unsafe_get_attr_pos_null_for_string_origin() {
let v = eval("builtins.unsafeGetAttrPos \"a\" { a = 1; }").unwrap();
assert_eq!(v, Value::Null);
}
#[test]
fn unsafe_get_attr_pos_null_for_missing_key() {
let dir = tempfile::tempdir().unwrap();
let f = dir.path().join("lit.nix");
std::fs::write(&f, "{ a = 1; }\n").unwrap();
let src = format!("builtins.unsafeGetAttrPos \"zzz\" (import {})", f.display());
let v = eval(&src).unwrap();
assert_eq!(v, Value::Null);
}
#[test]
fn interp_int_into_string() {
assert_eq!(ev(r#""val=${toString 42}""#), Value::string("val=42"));
}
#[test]
fn interp_bool_true_becomes_one() {
let v = ev(r#"let x = true; in "${builtins.toString x}""#);
assert_eq!(v, Value::string("1"));
}
#[test]
fn interp_null_becomes_empty() {
let v = ev(r#"let x = null; in "${builtins.toString x}""#);
assert_eq!(v, Value::string(""));
}
#[test]
fn interp_attrset_without_to_string_errors() {
let result = eval(r#"let s = { x = 1; }; in "${s}""#);
assert!(result.is_err());
}
#[test]
fn interp_attrset_with_to_string_protocol() {
let v = ev(r#""${{ __toString = self: "ok"; }}""#);
assert_eq!(v, Value::string("ok"));
}
#[test]
fn eval_path_absolute_literal() {
let v = ev("/tmp/foo");
match v {
Value::Path(p) => assert!(p.contains("/tmp/foo")),
_ => panic!("expected Path"),
}
}
#[test]
fn eval_path_home_literal() {
let v = ev("~/foo.nix");
match v {
Value::Path(p) => assert!(p.contains("~/foo.nix") || p.ends_with("foo.nix")),
_ => panic!("expected Path"),
}
}
#[test]
fn path_search_unmatched_errors() {
let saved = std::env::var("NIX_PATH").ok();
unsafe {
std::env::remove_var("NIX_PATH");
}
let result = eval("<this_should_not_resolve>");
if let Some(v) = saved {
unsafe {
std::env::set_var("NIX_PATH", v);
}
}
assert!(result.is_err());
}
#[test]
fn unary_negate_int() {
assert_eq!(ev("-7"), Value::Int(-7));
}
#[test]
fn unary_negate_float() {
assert_eq!(ev("-2.5"), Value::Float(-2.5));
}
#[test]
fn unary_invert_true() {
assert_eq!(ev("!true"), Value::Bool(false));
}
#[test]
fn unary_invert_false() {
assert_eq!(ev("!false"), Value::Bool(true));
}
#[test]
fn unary_negate_bool_errors() {
let result = eval("-true");
assert!(result.is_err());
}
#[test]
fn unary_invert_int_errors() {
let result = eval("!42");
assert!(result.is_err());
}
#[test]
fn binop_add_attrs_errors() {
let result = eval("{a=1;} + {b=2;}");
assert!(result.is_err());
}
#[test]
fn binop_sub_string_errors() {
let result = eval(r#""a" - "b""#);
assert!(result.is_err());
}
#[test]
fn binop_mul_string_errors() {
let result = eval(r#""a" * "b""#);
assert!(result.is_err());
}
#[test]
fn binop_div_string_errors() {
let result = eval(r#""a" / "b""#);
assert!(result.is_err());
}
#[test]
fn binop_compare_attrs_errors() {
let result = eval("{a=1;} < {b=2;}");
assert!(result.is_err());
}
#[test]
fn binop_div_float_by_zero_int() {
let result = eval("1.0 / 0");
let _ = result;
}
#[test]
fn binop_int_div_zero_is_division_by_zero() {
let result = eval("5 / 0");
match result {
Err(EvalError::DivisionByZero) => {}
other => panic!("expected DivisionByZero, got {other:?}"),
}
}
#[test]
fn if_else_only_chosen_branch_evaluated_then() {
assert_eq!(ev("if true then 42 else 1 / 0"), Value::Int(42));
}
#[test]
fn if_else_only_chosen_branch_evaluated_else() {
assert_eq!(ev("if false then 1 / 0 else 99"), Value::Int(99));
}
#[test]
fn if_condition_must_be_bool() {
let result = eval("if 1 then 1 else 2");
assert!(result.is_err());
}
#[test]
fn if_condition_lazy_does_not_force_unused() {
assert_eq!(
ev("let bad = 1 / 0; in if true then 42 else bad"),
Value::Int(42),
);
}
#[test]
fn and_short_circuits_on_false() {
assert_eq!(ev("false && (1 / 0 == 0)"), Value::Bool(false));
}
#[test]
fn or_short_circuits_on_true() {
assert_eq!(ev("true || (1 / 0 == 0)"), Value::Bool(true));
}
#[test]
fn implication_short_circuits_on_false_lhs() {
assert_eq!(ev("false -> (1 / 0 == 0)"), Value::Bool(true));
}
#[test]
fn lambda_fix_combinator_returns_attrset() {
let v = ev(
"let fix = f: let x = f x; in x; in
(fix (self: { val = 1; double = self.val * 2; })).double",
);
assert_eq!(v, Value::Int(2));
}
#[test]
fn rec_attrset_self_reference() {
let v = ev("(rec { a = b; b = 1; }).a");
assert_eq!(v, Value::Int(1));
}
#[test]
fn rec_attrset_inherit_from_uses_outer_scope() {
let v = ev(
"let src = { a = 10; }; in
rec {
inherit (src) a;
b = a + 1;
}",
);
if let Value::Attrs(attrs) = v {
let b = attrs.get("b").unwrap();
let b_forced = force_value(b).unwrap();
assert_eq!(b_forced, Value::Int(11));
} else {
panic!("expected attrs");
}
}
#[test]
fn nonrec_attrset_no_self_reference() {
let result = eval("({ a = 1; b = a + 1; }).b");
assert!(result.is_err());
}
#[test]
fn dotted_binding_three_segments_then_sibling() {
let v = ev("{ a.b.c = 1; a.b.d = 2; a.e = 3; }");
if let Value::Attrs(attrs) = v {
let a = attrs.get("a").unwrap();
let a_forced = force_value(a).unwrap();
if let Value::Attrs(a_attrs) = a_forced {
let b = a_attrs.get("b").unwrap();
let b_forced = force_value(b).unwrap();
if let Value::Attrs(b_attrs) = b_forced {
assert_eq!(force_value(b_attrs.get("c").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(b_attrs.get("d").unwrap()).unwrap(), Value::Int(2));
} else {
panic!("expected b to be attrs");
}
assert_eq!(force_value(a_attrs.get("e").unwrap()).unwrap(), Value::Int(3));
} else {
panic!("expected a to be attrs");
}
} else {
panic!("expected outer attrs");
}
}
#[test]
fn rec_dotted_bindings_visible_to_siblings() {
let v = ev("rec { types.openSB = 1; types.openCpu = 2; foo = types.openSB; }.foo");
assert_eq!(v, Value::Int(1));
}
#[test]
fn rec_dotted_leaf_uses_rec_scope() {
let v = ev("rec { types.a = f 1; f = x: x + 1; }.types.a");
assert_eq!(v, Value::Int(2));
}
#[test]
fn rec_dotted_multiple_keys_merge() {
let v = ev("rec { types.a = 1; types.b = 2; x = types; }.x");
if let Value::Attrs(attrs) = v {
assert_eq!(force_value(attrs.get("a").unwrap()).unwrap(), Value::Int(1));
assert_eq!(force_value(attrs.get("b").unwrap()).unwrap(), Value::Int(2));
} else {
panic!("expected attrs");
}
}
#[test]
fn rec_nixpkgs_parse_pattern() {
let v = ev(r#"
let
mkOptionType = x: x;
mergeOneOption = "merge";
attrValues = builtins.attrValues;
setType = name: value: { __type = name; } // value;
mapAttrs = builtins.mapAttrs;
enum = xs: mkOptionType { name = "enum"; check = x: builtins.elem x xs; };
setTypes = type: mapAttrs (name: value: setType type.name ({ inherit name; } // value));
in
rec {
types.openSB = mkOptionType { name = "sb"; merge = mergeOneOption; };
types.significantByte = enum (attrValues significantBytes);
significantBytes = setTypes types.openSB { bigEndian = {}; littleEndian = {}; };
types.openCpuType = mkOptionType { name = "cpu-type"; };
types.cpuType = enum (attrValues cpuTypes);
cpuTypes = setTypes types.openCpuType { arm = { bits = 32; }; };
}.types.openCpuType
"#);
if let Value::Attrs(attrs) = v {
assert_eq!(
force_value(attrs.get("name").unwrap()).unwrap(),
Value::string("cpu-type")
);
} else {
panic!("expected attrs");
}
}
#[test]
fn let_dotted_leaf_uses_let_scope() {
let v = ev("let a.x = f 1; f = x: x + 1; in a.x");
assert_eq!(v, Value::Int(2));
}
#[test]
fn let_inherit_from_plus_dotted_overrides() {
let v = ev(r#"
let
src = { types = { existing = true; }; };
inherit (src) types;
types.added = true;
in types
"#);
if let Value::Attrs(attrs) = v {
assert_eq!(
force_value(attrs.get("added").unwrap()).unwrap(),
Value::Bool(true)
);
assert!(attrs.get("existing").is_none());
} else {
panic!("expected attrs");
}
}
#[test]
fn pattern_empty_no_args_no_ellipsis() {
assert_eq!(ev("({}: 1) {}"), Value::Int(1));
}
#[test]
fn pattern_empty_with_ellipsis_accepts_extra() {
assert_eq!(ev("({...}: 1) { a = 1; b = 2; }"), Value::Int(1));
}
#[test]
fn pattern_all_defaults() {
assert_eq!(
ev("({a ? 1, b ? 2}: a + b) {}"),
Value::Int(3),
);
}
#[test]
fn pattern_at_bind_before() {
assert_eq!(ev("(args @ { x }: args.x) { x = 7; }"), Value::Int(7));
}
#[test]
fn pattern_at_bind_after() {
assert_eq!(ev("({ x } @ args: args.x) { x = 7; }"), Value::Int(7));
}
#[test]
fn pattern_default_references_other_arg() {
assert_eq!(ev("({a, b ? a + 1}: b) {a = 10;}"), Value::Int(11));
}
#[test]
fn pattern_required_missing_errors() {
let result = eval("({ a, b }: a) { a = 1; }");
assert!(result.is_err());
}
#[test]
fn pattern_unexpected_errors_without_ellipsis() {
let result = eval("({ a }: a) { a = 1; b = 2; }");
assert!(result.is_err());
}
#[test]
fn apply_int_errors() {
let result = eval("42 5");
assert!(result.is_err());
}
#[test]
fn apply_string_errors() {
let result = eval(r#""hi" 5"#);
assert!(result.is_err());
}
#[test]
fn apply_attrset_without_functor_errors() {
let result = eval("{ x = 1; } 5");
assert!(result.is_err());
let msg = format!("{}", result.unwrap_err());
assert!(msg.contains("__functor") || msg.contains("cannot call"));
}
#[test]
fn select_multi_segment_with_default() {
assert_eq!(ev("{ a = { b = 1; }; }.a.c or 99"), Value::Int(99));
}
#[test]
fn select_from_int_errors() {
let result = eval("(1).x");
assert!(result.is_err());
}
#[test]
fn has_attr_on_non_set_returns_false() {
assert_eq!(ev("1 ? x"), Value::Bool(false));
}
#[test]
fn has_attr_nested_path_present() {
assert_eq!(ev("{ a = { b = 1; }; } ? a.b"), Value::Bool(true));
}
#[test]
fn has_attr_nested_path_missing() {
assert_eq!(ev("{ a = { b = 1; }; } ? a.c"), Value::Bool(false));
}
#[test]
fn has_attr_intermediate_missing_returns_false() {
assert_eq!(ev("{} ? a.b.c"), Value::Bool(false));
}
#[test]
fn list_with_function_value() {
let v = ev("[(x: x + 1)]");
if let Value::List(items) = v {
assert_eq!(items.len(), 1);
let forced = force_value(&items[0]).unwrap();
assert!(matches!(forced, Value::Lambda(_)));
} else {
panic!("expected list");
}
}
#[test]
fn inherit_unknown_name_errors() {
let result = eval("let x = 1; in let inherit nonexistent; in nonexistent");
assert!(result.is_err());
}
#[test]
fn string_concat_no_context_when_both_plain() {
let v = ev(r#""abc" + "def""#);
if let Value::String(ns) = v {
assert_eq!(ns.chars, "abcdef");
assert!(!ns.has_context());
} else {
panic!("expected string");
}
}
#[test]
fn parens_around_expression() {
assert_eq!(ev("(1 + 2)"), Value::Int(3));
}
#[test]
fn nested_parens() {
assert_eq!(ev("(((42)))"), Value::Int(42));
}
#[test]
fn throw_propagates_as_error() {
let result = eval(r#"builtins.throw "kaboom""#);
match result {
Err(EvalError::Throw(s)) => assert!(s.contains("kaboom")),
other => panic!("expected Throw, got {other:?}"),
}
}
#[test]
fn assert_failed_propagates_as_error() {
let result = eval("assert false; 1");
match result {
Err(EvalError::AssertionFailed(_)) => {}
other => panic!("expected AssertionFailed, got {other:?}"),
}
}
#[test]
fn string_no_interp_yields_no_context() {
let v = ev(r#""just literal""#);
if let Value::String(ns) = v {
assert!(!ns.has_context());
} else {
panic!("expected string");
}
}
#[test]
fn interp_path_copies_to_store_byte_matches_cppnix() {
let dir = std::env::temp_dir().join(format!("sui-r5-interp-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&dir);
std::fs::create_dir_all(&dir).unwrap();
let f = dir.join("data.txt");
std::fs::write(&f, b"hello\n").unwrap();
let expr = format!(r#""${{{}}}""#, f.display());
let v = eval(&expr).unwrap();
if let Value::String(ns) = v {
assert_eq!(
ns.chars.to_string(),
"/nix/store/y9dmvfhip31hg8ia4njwjz9vfa3ndphr-data.txt",
);
assert!(ns.has_context());
} else {
panic!("expected string");
}
let _ = std::fs::remove_dir_all(&dir);
}
#[test]
fn parse_error_unbalanced_braces() {
let result = eval("{ a = 1");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(matches!(err, EvalError::ParseError(_)));
}
#[test]
fn parse_error_dangling_let() {
let result = eval("let in");
assert!(result.is_err());
}
#[test]
fn parse_error_empty_input() {
let result = eval("");
assert!(result.is_err());
}
#[test]
fn float_int_subtraction() {
assert_eq!(ev("3.5 - 1"), Value::Float(2.5));
}
#[test]
fn int_float_subtraction() {
assert_eq!(ev("3 - 0.5"), Value::Float(2.5));
}
#[test]
fn float_float_division() {
assert_eq!(ev("6.0 / 2.0"), Value::Float(3.0));
}
#[test]
fn int_float_multiplication() {
assert_eq!(ev("3 * 2.5"), Value::Float(7.5));
}
#[test]
fn compare_int_float_less() {
assert_eq!(ev("1 < 1.5"), Value::Bool(true));
}
#[test]
fn compare_float_int_more() {
assert_eq!(ev("3.5 > 3"), Value::Bool(true));
}
#[test]
fn compare_equal_int_float() {
assert_eq!(ev("3 <= 3.0"), Value::Bool(true));
}
#[test]
fn equal_lists_same() {
assert_eq!(ev("[1 2 3] == [1 2 3]"), Value::Bool(true));
}
#[test]
fn equal_lists_diff_length() {
assert_eq!(ev("[1 2] == [1 2 3]"), Value::Bool(false));
}
#[test]
fn not_equal_lists() {
assert_eq!(ev("[1] != [2]"), Value::Bool(true));
}
#[test]
fn equal_attrsets_same() {
assert_eq!(ev("{a = 1; b = 2;} == {b = 2; a = 1;}"), Value::Bool(true));
}
#[test]
fn lambda_self_equality_in_attrset() {
assert_eq!(
ev("let f = x: x; in { a = 1; inherit f; } == { a = 1; inherit f; }"),
Value::Bool(true),
);
}
#[test]
fn lambda_self_reference_attrset_equality() {
assert_eq!(
ev("let x = { a = 1; f = y: y; }; in x == x"),
Value::Bool(true),
);
}
#[test]
fn lambda_different_closures_not_equal() {
assert_eq!(
ev("{ f = x: x; } == { f = x: x; }"),
Value::Bool(false),
);
}
#[test]
fn lambda_ne_does_not_force_unused_branch() {
assert_eq!(
ev("let ls = { a = 1; f = x: x; }; in if ls != ls then builtins.throw \"bug\" else 42"),
Value::Int(42),
);
}
#[test]
fn force_value_through_thunk() {
let root = rnix::Root::parse("1 + 2");
let expr = root.tree().expr().unwrap();
let thunk = Thunk::new_suspended(expr, Env::new());
let val = Value::Thunk(thunk);
assert_eq!(force_value(&val).unwrap(), Value::Int(3));
}
#[test]
fn try_eval_catches_thrown_error() {
let v = ev(r#"(builtins.tryEval (builtins.throw "oops")).success"#);
assert_eq!(v, Value::Bool(false));
}
#[test]
fn try_eval_returns_value_on_success() {
let v = ev("(builtins.tryEval 42).value");
assert_eq!(v, Value::Int(42));
}
#[test]
fn legacy_let_returns_body_attr() {
assert_eq!(ev("let { x = 1; body = x + 41; }"), Value::Int(42));
}
#[test]
fn legacy_let_missing_body_errors() {
let result = eval("let { x = 1; }");
assert!(result.is_err());
}
#[test]
fn legacy_let_with_inherit_from_scope() {
assert_eq!(
ev("let outer = 5; in let { inherit outer; body = outer * 2; }"),
Value::Int(10),
);
}
#[test]
fn interp_with_string_concat_preserves_order() {
assert_eq!(
ev(r#"let a = "x"; b = "y"; in "${a}-${b}""#),
Value::string("x-y"),
);
}
#[test]
fn interp_only_literal_part() {
assert_eq!(ev(r#""no interp here""#), Value::string("no interp here"));
}
#[test]
fn dynamic_attr_via_string_key_in_set() {
assert_eq!(ev(r#"{ "a" = 1; }.a"#), Value::Int(1));
}
#[test]
fn dynamic_attr_via_interpolated_key() {
let v = ev(r#"let k = "foo"; in { ${k} = 99; }.foo"#);
assert_eq!(v, Value::Int(99));
}
#[test]
fn select_with_string_key() {
let v = ev(r#"{ a = 42; }."a""#);
assert_eq!(v, Value::Int(42));
}
#[test]
fn apply_attrset_with_functor_works() {
let v = ev("let s = { __functor = self: x: x + 1; }; in s 5");
assert_eq!(v, Value::Int(6));
}
#[test]
fn double_negate_int() {
assert_eq!(ev("- (-5)"), Value::Int(5));
}
#[test]
fn inherit_in_let_makes_name_available() {
assert_eq!(
ev("let src = { a = 7; }; in let inherit (src) a; in a"),
Value::Int(7),
);
}
#[test]
fn path_plus_string_yields_path() {
let v = ev(r#"/foo + "/bar""#);
match v {
Value::Path(p) => assert_eq!(&*p, "/foo/bar"),
_ => panic!("expected path"),
}
}
#[test]
fn attrset_value_not_forced_unless_selected() {
assert_eq!(
ev(r#"{ bad = builtins.throw "boom"; good = 42; }.good"#),
Value::Int(42),
);
}
#[test]
fn lambda_recursive_via_let() {
assert_eq!(
ev("let fact = n: if n == 0 then 1 else n * fact (n - 1); in fact 5"),
Value::Int(120),
);
}
#[test]
fn select_with_dynamic_key_via_var() {
assert_eq!(ev(r#"let k = { x = 1; }; in k.x"#), Value::Int(1));
}
#[test]
fn compare_string_lex_greater_or_equal() {
assert_eq!(ev(r#""b" >= "a""#), Value::Bool(true));
assert_eq!(ev(r#""a" >= "a""#), Value::Bool(true));
assert_eq!(ev(r#""a" >= "b""#), Value::Bool(false));
}
#[test]
fn equal_int_string_false() {
assert_eq!(ev(r#"1 == "1""#), Value::Bool(false));
}
#[test]
fn equal_null_int_false() {
assert_eq!(ev("null == 0"), Value::Bool(false));
}
#[test]
fn update_with_let_bound_operands() {
assert_eq!(
ev("let a = { x = 1; }; b = { y = 2; }; in (a // b).y"),
Value::Int(2),
);
}
#[test]
fn concat_lists_from_let() {
assert_eq!(
ev("let a = [1 2]; b = [3 4]; in builtins.length (a ++ b)"),
Value::Int(4),
);
}
#[test]
fn interp_list_coerces_with_spaces() {
assert_eq!(
ev(r#""${toString [1 2 3]}""#),
Value::string("1 2 3"),
);
}
#[test]
fn interp_list_directly_coerces() {
assert_eq!(
ev(r#""${[1 2]}""#),
Value::string("1 2"),
);
}
#[test]
fn interp_outpath_attrset() {
assert_eq!(
ev(r#"let x = { outPath = "/nix/store/abc"; }; in "${x}""#),
Value::string("/nix/store/abc"),
);
}
#[test]
fn interp_tostring_takes_priority_over_outpath() {
assert_eq!(
ev(r#"let x = { __toString = self: "custom"; outPath = "/ignored"; }; in "${x}""#),
Value::string("custom"),
);
}
#[test]
fn interp_derivation_coerces_to_outpath() {
let result = eval(r#"
let drv = builtins.derivation {
name = "test";
system = "x86_64-linux";
builder = "/bin/sh";
};
in "${drv}"
"#).unwrap();
if let Value::String(s) = result {
assert!(s.chars.starts_with("/nix/store/"), "got: {}", s.chars);
} else {
panic!("expected string");
}
}
#[test]
fn interp_lambda_errors() {
let result = eval(r#""${x: x}""#);
assert!(result.is_err());
}
#[test]
fn force_value_int_returns_same() {
let v = Value::Int(42);
assert_eq!(force_value(&v).unwrap(), Value::Int(42));
}
#[test]
fn force_value_bool_returns_same() {
let v = Value::Bool(true);
assert_eq!(force_value(&v).unwrap(), Value::Bool(true));
}
#[test]
fn force_value_string_returns_same() {
let v = Value::string("hello");
assert_eq!(force_value(&v).unwrap(), Value::string("hello"));
}
#[test]
fn force_value_attrs_returns_same() {
let mut a = NixAttrs::new();
a.insert("x".to_string(), Value::Int(1));
let v = Value::Attrs(Rc::new(a.clone()));
assert_eq!(force_value(&v).unwrap(), Value::Attrs(Rc::new(a)));
}
#[test]
fn force_value_list_returns_same() {
let v = Value::list(vec![Value::Int(1), Value::Int(2)]);
assert_eq!(
force_value(&v).unwrap(),
Value::list(vec![Value::Int(1), Value::Int(2)]),
);
}
#[test]
fn force_value_null_returns_null() {
let v = Value::Null;
assert_eq!(force_value(&v).unwrap(), Value::Null);
}
#[test]
fn force_value_evaluated_thunk_returns_cached() {
let v = ev("let x = 1 + 2; in x");
assert_eq!(v, Value::Int(3));
assert_eq!(force_value(&v).unwrap(), Value::Int(3));
}
#[test]
fn tco_if_true_condition() {
assert_eq!(ev("if true then 42 else 0"), Value::Int(42));
}
#[test]
fn tco_if_false_condition() {
assert_eq!(ev("if false then 42 else 0"), Value::Int(0));
}
#[test]
fn tco_deeply_nested_if_else_chain() {
let mut expr = String::from("150");
for i in (1..150).rev() {
expr = format!("if false then {} else {}", i, expr);
}
let v = ev(&expr);
assert_eq!(v, Value::Int(150));
}
#[test]
fn tco_assert_true_passes_through() {
assert_eq!(ev("assert true; 42"), Value::Int(42));
}
#[test]
fn tco_assert_false_throws_assertion_failed() {
let result = eval("assert false; 42");
assert!(result.is_err());
let err = result.unwrap_err();
assert!(
matches!(err, EvalError::AssertionFailed(_)),
"expected AssertionFailed, got: {err}",
);
}
#[test]
fn tco_with_makes_scope_available() {
assert_eq!(ev("with { x = 10; y = 20; }; x + y"), Value::Int(30));
}
#[test]
fn tco_let_in_creates_bindings() {
assert_eq!(ev("let a = 5; in a"), Value::Int(5));
}
#[test]
fn tco_let_in_multiple_bindings() {
assert_eq!(ev("let a = 1; b = 2; c = 3; in a + b + c"), Value::Int(6));
}
#[test]
fn eval_attrset_empty() {
let v = ev("{}");
if let Value::Attrs(attrs) = v {
assert!(attrs.is_empty(), "expected empty attrset");
} else {
panic!("expected attrset, got {v:?}");
}
}
#[test]
fn eval_attrset_simple_kv() {
let v = ev("{ a = 1; b = 2; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected attrset, got {v:?}");
}
}
#[test]
fn eval_attrset_recursive() {
assert_eq!(ev("(rec { a = 1; b = a + 1; }).b"), Value::Int(2));
assert_eq!(ev("(rec { a = 1; b = a + 1; }).a"), Value::Int(1));
}
#[test]
fn eval_attrset_inherit_from_scope() {
assert_eq!(ev("let x = 1; in { inherit x; }.x"), Value::Int(1));
}
#[test]
fn eval_attrset_inherit_from_expr() {
assert_eq!(
ev("{ inherit (builtins) true; }.true"),
Value::Bool(true),
);
}
#[test]
fn eval_attrset_dotted_path() {
assert_eq!(ev("{ a.b.c = 1; }.a.b.c"), Value::Int(1));
}
#[test]
fn eval_attrset_update_merge() {
let v = ev("{ a = 1; } // { b = 2; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected attrset, got {v:?}");
}
}
#[test]
fn eval_apply_simple_function() {
assert_eq!(ev("(x: x + 1) 2"), Value::Int(3));
}
#[test]
fn eval_apply_pattern_destructuring() {
assert_eq!(ev("({a, b}: a + b) { a = 1; b = 2; }"), Value::Int(3));
}
#[test]
fn eval_apply_default_arguments() {
assert_eq!(ev("({a, b ? 0}: a + b) { a = 1; }"), Value::Int(1));
}
#[test]
fn eval_apply_ellipsis() {
assert_eq!(ev("({a, ...}: a) { a = 1; b = 2; }"), Value::Int(1));
}
#[test]
fn eval_select_single_key() {
assert_eq!(ev("{ a = 1; }.a"), Value::Int(1));
}
#[test]
fn eval_select_multi_level() {
assert_eq!(ev("{ a.b = 1; }.a.b"), Value::Int(1));
}
#[test]
fn eval_select_with_or_default() {
assert_eq!(ev("{}.a or 42"), Value::Int(42));
}
#[test]
fn eval_select_missing_key_without_default_throws() {
let result = eval("{}.a");
assert!(result.is_err());
}
#[test]
fn binop_add_ints() {
assert_eq!(ev("1 + 2"), Value::Int(3));
}
#[test]
fn binop_sub_ints() {
assert_eq!(ev("3 - 1"), Value::Int(2));
}
#[test]
fn binop_mul_ints() {
assert_eq!(ev("2 * 3"), Value::Int(6));
}
#[test]
fn binop_div_ints() {
assert_eq!(ev("6 / 2"), Value::Int(3));
}
#[test]
fn binop_float_arithmetic() {
assert_eq!(ev("1.5 + 2.5"), Value::Float(4.0));
}
#[test]
fn binop_string_concat() {
assert_eq!(
ev(r#""hello" + " " + "world""#),
Value::string("hello world"),
);
}
#[test]
fn binop_list_concat() {
assert_eq!(
ev("[1 2] ++ [3 4]"),
Value::list(vec![
Value::Int(1),
Value::Int(2),
Value::Int(3),
Value::Int(4),
]),
);
}
#[test]
fn binop_attrset_update() {
let v = ev("{ a = 1; } // { b = 2; }");
if let Value::Attrs(attrs) = v {
assert_eq!(attrs.get("a"), Some(&Value::Int(1)));
assert_eq!(attrs.get("b"), Some(&Value::Int(2)));
} else {
panic!("expected attrset, got {v:?}");
}
}
#[test]
fn binop_less_than() {
assert_eq!(ev("1 < 2"), Value::Bool(true));
assert_eq!(ev("2 < 1"), Value::Bool(false));
}
#[test]
fn binop_greater_than() {
assert_eq!(ev("2 > 1"), Value::Bool(true));
assert_eq!(ev("1 > 2"), Value::Bool(false));
}
#[test]
fn binop_equal() {
assert_eq!(ev("1 == 1"), Value::Bool(true));
assert_eq!(ev("1 == 2"), Value::Bool(false));
}
#[test]
fn binop_not_equal() {
assert_eq!(ev("1 != 2"), Value::Bool(true));
assert_eq!(ev("1 != 1"), Value::Bool(false));
}
#[test]
fn binop_logical_and() {
assert_eq!(ev("true && false"), Value::Bool(false));
assert_eq!(ev("true && true"), Value::Bool(true));
}
#[test]
fn binop_logical_or() {
assert_eq!(ev("true || false"), Value::Bool(true));
assert_eq!(ev("false || false"), Value::Bool(false));
}
#[test]
fn binop_logical_not() {
assert_eq!(ev("!true"), Value::Bool(false));
assert_eq!(ev("!false"), Value::Bool(true));
}
#[test]
fn binop_implication() {
assert_eq!(ev("false -> true"), Value::Bool(true));
assert_eq!(ev("false -> false"), Value::Bool(true));
assert_eq!(ev("true -> true"), Value::Bool(true));
assert_eq!(ev("true -> false"), Value::Bool(false));
}
}