use std::cell::{Cell, RefCell};
use std::collections::HashSet;
use std::path::PathBuf;
use std::rc::Rc;
use rustc_hash::FxHashMap;
use sui_intern::{intern, resolve, Symbol};
pub use crate::builtins::IrBuiltin;
use crate::file_eval::{current_eval_dir, push_eval_file};
use crate::ir::{
AttrName, BinOp, Binding, ExprId, Ir, Param, PathKind, PathPart, Program, StrPart, UnaryOp,
};
thread_local! {
static FORCE_STACK: RefCell<Vec<usize>> = const { RefCell::new(Vec::new()) };
static IN_PROMISE_EVAL: Cell<u32> = const { Cell::new(0) };
static PROMOTION_OCCURRED: Cell<bool> = const { Cell::new(false) };
}
const FIXPOINT_PROMOTE_NEST_CAP: u32 = 32;
const PROMOTION_RUNAWAY_FORCE_DEPTH: usize = 500;
fn in_promise_eval() -> bool {
IN_PROMISE_EVAL.with(|c| c.get() > 0)
}
fn promotion_occurred() -> bool {
PROMOTION_OCCURRED.with(|c| c.get())
}
fn force_stack_contains(thunk_id: usize) -> bool {
FORCE_STACK.with(|s| s.borrow().iter().any(|&id| id == thunk_id))
}
fn force_stack_depth() -> usize {
FORCE_STACK.with(|s| s.borrow().len())
}
struct ForceStackGuard;
impl Drop for ForceStackGuard {
fn drop(&mut self) {
FORCE_STACK.with(|s| {
s.borrow_mut().pop();
});
}
}
fn push_force_frame(thunk_id: usize) -> ForceStackGuard {
FORCE_STACK.with(|s| s.borrow_mut().push(thunk_id));
ForceStackGuard
}
#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
pub enum IrEvalError {
#[error("undefined variable '{0}'")]
UndefinedVar(String),
#[error("type error: {0}")]
TypeError(String),
#[error("expected {expected}, got {got}")]
TypeMismatch {
expected: &'static str,
got: &'static str,
},
#[error("attribute '{0}' not found")]
AttrNotFound(String),
#[error("assertion failed")]
AssertionFailed,
#[error("division by zero")]
DivisionByZero,
#[error("infinite recursion encountered")]
InfiniteRecursion,
#[error("evaluation aborted: {0}")]
Abort(String),
#[error("throw: {0}")]
Throw(String),
#[error("construct not supported by the pure-subset IR evaluator: {0}")]
Unsupported(&'static str),
#[error("builtin not implemented by the pure-subset IR evaluator: {0}")]
MissingBuiltin(String),
#[error("circular import: {0}")]
ImportCycle(String),
#[error("{context}: {message}")]
Io { context: String, message: String },
#[error("parse error: {0}")]
Parse(String),
}
pub type IrAttrs = std::collections::BTreeMap<String, IrValue>;
#[derive(Debug)]
pub struct IrClosure {
pub prog: Rc<Program>,
pub param: Param,
pub body: ExprId,
pub env: IrEnv,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum IrContextElem {
Plain(String),
Output { drv: String, output: String },
DrvDeep(String),
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct IrStringContext(Vec<IrContextElem>);
impl IrStringContext {
#[must_use]
pub fn new() -> Self {
Self(Vec::new())
}
pub fn merge(&mut self, other: &IrStringContext) {
for elem in &other.0 {
if !self.0.contains(elem) {
self.0.push(elem.clone());
}
}
}
pub fn add_plain(&mut self, path: impl Into<String>) {
let elem = IrContextElem::Plain(path.into());
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
pub fn add_output(&mut self, drv: impl Into<String>, output: impl Into<String>) {
let elem = IrContextElem::Output {
drv: drv.into(),
output: output.into(),
};
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
pub fn add_drv_deep(&mut self, drv: impl Into<String>) {
let elem = IrContextElem::DrvDeep(drv.into());
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
pub fn insert(&mut self, elem: IrContextElem) {
if !self.0.contains(&elem) {
self.0.push(elem);
}
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.0.is_empty()
}
pub fn iter(&self) -> impl Iterator<Item = &IrContextElem> {
self.0.iter()
}
}
#[derive(Debug, Clone, Default)]
pub enum IrValue {
#[default]
Null,
Bool(bool),
Int(i64),
Float(f64),
Str(Rc<String>, Option<Rc<IrStringContext>>),
Path(Rc<String>),
List(Rc<Vec<IrValue>>),
Attrs(Rc<IrAttrs>),
Lambda(Rc<IrClosure>),
Builtin(IrBuiltin, Rc<Vec<IrValue>>),
Thunk(IrThunk),
}
impl IrValue {
#[must_use]
pub fn string(s: impl Into<String>) -> Self {
IrValue::Str(Rc::new(s.into()), None)
}
#[must_use]
pub fn string_with_context(s: impl Into<String>, ctx: IrStringContext) -> Self {
let boxed = if ctx.is_empty() {
None
} else {
Some(Rc::new(ctx))
};
IrValue::Str(Rc::new(s.into()), boxed)
}
#[must_use]
pub fn str_context(&self) -> Option<&IrStringContext> {
match self {
IrValue::Str(_, Some(c)) => Some(c),
_ => None,
}
}
#[must_use]
pub fn type_name(&self) -> &'static str {
match self {
IrValue::Null => "null",
IrValue::Bool(_) => "bool",
IrValue::Int(_) => "int",
IrValue::Float(_) => "float",
IrValue::Str(..) => "string",
IrValue::Path(_) => "path",
IrValue::List(_) => "list",
IrValue::Attrs(_) => "set",
IrValue::Lambda(_) | IrValue::Builtin(..) => "lambda",
IrValue::Thunk(_) => "thunk",
}
}
pub fn force(&self) -> Result<IrValue, IrEvalError> {
let mut v = self.clone();
for _ in 0..100 {
match v {
IrValue::Thunk(t) => v = t.force_step()?,
other => return Ok(other),
}
}
Err(IrEvalError::InfiniteRecursion)
}
pub fn as_bool(&self) -> Result<bool, IrEvalError> {
match self {
IrValue::Bool(b) => Ok(*b),
other => Err(IrEvalError::TypeMismatch {
expected: "bool",
got: other.type_name(),
}),
}
}
}
enum IrThunkState {
Suspended {
prog: Rc<Program>,
expr: ExprId,
env: IrEnv,
recursive: bool,
},
InheritSelect { source: IrThunk, name: String },
WithIdent { name: String, env: IrEnv },
DeferredTail {
prog: Rc<Program>,
tail: Vec<AttrName>,
value: ExprId,
env: IrEnv,
},
NativeApply { func: IrValue, args: Vec<IrValue> },
Blackhole,
Promise(Rc<RefCell<IrValue>>),
Evaluated(IrValue),
Failed(IrEvalError),
}
#[derive(Clone)]
pub struct IrThunk(Rc<RefCell<IrThunkState>>);
impl std::fmt::Debug for IrThunk {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match &*self.0.borrow() {
IrThunkState::Suspended { expr, .. } => write!(f, "<thunk {expr:?}>"),
IrThunkState::InheritSelect { name, .. } => write!(f, "<inherit {name}>"),
IrThunkState::WithIdent { name, .. } => write!(f, "<with-ident {name}>"),
IrThunkState::DeferredTail { .. } => write!(f, "<deferred-tail>"),
IrThunkState::NativeApply { .. } => write!(f, "<native-apply>"),
IrThunkState::Blackhole => write!(f, "<blackhole>"),
IrThunkState::Promise(_) => write!(f, "<promise>"),
IrThunkState::Evaluated(v) => write!(f, "<evaluated {v:?}>"),
IrThunkState::Failed(e) => write!(f, "<failed {e}>"),
}
}
}
impl IrThunk {
#[must_use]
pub fn suspended(prog: Rc<Program>, expr: ExprId, env: IrEnv) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::Suspended {
prog,
expr,
env,
recursive: false,
})))
}
#[must_use]
pub fn suspended_recursive(prog: Rc<Program>, expr: ExprId, env: IrEnv) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::Suspended {
prog,
expr,
env,
recursive: true,
})))
}
fn inherit_select(source: IrThunk, name: String) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::InheritSelect {
source,
name,
})))
}
fn with_ident(name: String, env: IrEnv) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::WithIdent { name, env })))
}
fn deferred_tail(prog: Rc<Program>, tail: Vec<AttrName>, value: ExprId, env: IrEnv) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::DeferredTail {
prog,
tail,
value,
env,
})))
}
pub(crate) fn native_apply(func: IrValue, args: Vec<IrValue>) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::NativeApply {
func,
args,
})))
}
#[must_use]
pub fn failed(err: IrEvalError) -> Self {
Self(Rc::new(RefCell::new(IrThunkState::Failed(err))))
}
pub fn update_env(&self, new_env: &IrEnv) {
let mut state = self.0.borrow_mut();
match &mut *state {
IrThunkState::Suspended { env, .. } => *env = new_env.clone(),
IrThunkState::InheritSelect { source, .. } => {
let source = source.clone();
drop(state);
source.update_env(new_env);
}
_ => {}
}
}
pub fn force_step(&self) -> Result<IrValue, IrEvalError> {
enum Todo {
Eval {
prog: Rc<Program>,
expr: ExprId,
env: IrEnv,
promise: Option<Rc<RefCell<IrValue>>>,
},
Inherit {
source: IrThunk,
name: String,
},
WithIdent {
name: String,
env: IrEnv,
},
DeferredTail {
prog: Rc<Program>,
tail: Vec<AttrName>,
value: ExprId,
env: IrEnv,
},
NativeApply {
func: IrValue,
args: Vec<IrValue>,
},
}
let thunk_id = Rc::as_ptr(&self.0) as usize;
let todo = {
let mut state = self.0.borrow_mut();
let todo = match &*state {
IrThunkState::Evaluated(v) => return Ok(v.clone()),
IrThunkState::Failed(e) => return Err(e.clone()),
IrThunkState::Blackhole => {
if force_stack_contains(thunk_id)
&& IN_PROMISE_EVAL.with(|c| c.get()) < FIXPOINT_PROMOTE_NEST_CAP
{
let cell = Rc::new(RefCell::new(IrValue::Attrs(Rc::new(IrAttrs::new()))));
*state = IrThunkState::Promise(cell.clone());
IN_PROMISE_EVAL.with(|c| c.set(c.get() + 1));
PROMOTION_OCCURRED.with(|c| c.set(true));
return Ok(cell.borrow().clone());
}
return Err(IrEvalError::InfiniteRecursion);
}
IrThunkState::Promise(cell) => return Ok(cell.borrow().clone()),
IrThunkState::Suspended {
prog,
expr,
env,
recursive,
} => Todo::Eval {
prog: prog.clone(),
expr: *expr,
env: env.clone(),
promise: if *recursive {
Some(Rc::new(RefCell::new(IrValue::Attrs(Rc::new(
IrAttrs::new(),
)))))
} else {
None
},
},
IrThunkState::InheritSelect { source, name } => Todo::Inherit {
source: source.clone(),
name: name.clone(),
},
IrThunkState::WithIdent { name, env } => Todo::WithIdent {
name: name.clone(),
env: env.clone(),
},
IrThunkState::DeferredTail {
prog,
tail,
value,
env,
} => Todo::DeferredTail {
prog: prog.clone(),
tail: tail.clone(),
value: *value,
env: env.clone(),
},
IrThunkState::NativeApply { func, args } => Todo::NativeApply {
func: func.clone(),
args: args.clone(),
},
};
*state = match &todo {
Todo::Eval {
promise: Some(cell),
..
} => IrThunkState::Promise(cell.clone()),
_ => IrThunkState::Blackhole,
};
todo
};
let result = match todo {
Todo::Eval {
prog,
expr,
env,
promise,
} => {
let is_promise = promise.is_some();
let _force_guard = push_force_frame(thunk_id);
if promotion_occurred() && force_stack_depth() > PROMOTION_RUNAWAY_FORCE_DEPTH {
Err(IrEvalError::InfiniteRecursion)
} else {
let _file_guard = env.eval_file().map(|f| push_eval_file((*f).clone()));
if is_promise {
IN_PROMISE_EVAL.with(|c| c.set(c.get() + 1));
}
let r = eval_ir(&prog, expr, &env);
if is_promise {
IN_PROMISE_EVAL.with(|c| c.set(c.get().saturating_sub(1)));
}
let became_promise =
!is_promise && matches!(&*self.0.borrow(), IrThunkState::Promise(_));
if became_promise {
IN_PROMISE_EVAL.with(|c| c.set(c.get().saturating_sub(1)));
}
if let Ok(v) = &r {
if is_promise {
if let Some(cell) = &promise {
*cell.borrow_mut() = v.clone();
}
} else if became_promise {
if let IrThunkState::Promise(cell) = &*self.0.borrow() {
*cell.borrow_mut() = v.clone();
}
}
}
r
}
}
Todo::Inherit { source, name } => {
match IrValue::Thunk(source).force()? {
IrValue::Attrs(attrs) => attrs
.get(&name)
.cloned()
.ok_or(IrEvalError::AttrNotFound(name)),
other => Err(IrEvalError::TypeMismatch {
expected: "set",
got: other.type_name(),
}),
}
}
Todo::WithIdent { name, env } => match env.lookup(&name) {
Some(v) => Ok(v),
None if in_promise_eval() => Ok(IrValue::Null),
None => Err(IrEvalError::UndefinedVar(name)),
},
Todo::DeferredTail {
prog,
tail,
value,
env,
} => build_tail_attrs(&prog, &tail, value, &env),
Todo::NativeApply { func, args } => {
let mut result = Ok(func);
for arg in args {
result = result.and_then(|f| apply(f, arg));
}
result
}
};
let mut state = self.0.borrow_mut();
match &result {
Ok(v) => *state = IrThunkState::Evaluated(v.clone()),
Err(e) => *state = IrThunkState::Failed(e.clone()),
}
result
}
}
#[derive(Clone)]
struct IrWithScope {
value: IrValue,
cached: Rc<RefCell<Option<Rc<IrAttrs>>>>,
}
#[derive(Default)]
struct IrEnvInner {
bindings: FxHashMap<Symbol, IrValue>,
with_scopes: Vec<IrWithScope>,
eval_file: Option<Rc<PathBuf>>,
}
#[derive(Clone, Default)]
pub struct IrEnv(Rc<IrEnvInner>);
impl std::fmt::Debug for IrEnv {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"IrEnv({} bindings, {} with-scopes)",
self.0.bindings.len(),
self.0.with_scopes.len()
)
}
}
impl IrEnv {
#[must_use]
pub fn new() -> Self {
Self::default()
}
#[must_use]
pub fn with_pure_builtins() -> Self {
crate::builtins::base_env()
}
#[must_use]
pub fn child(&self) -> Self {
Self(Rc::new(IrEnvInner {
bindings: self.0.bindings.clone(),
with_scopes: self.0.with_scopes.clone(),
eval_file: self.0.eval_file.clone(),
}))
}
#[must_use]
pub fn with_scope(&self, value: IrValue) -> Self {
let mut inner = IrEnvInner {
bindings: self.0.bindings.clone(),
with_scopes: self.0.with_scopes.clone(),
eval_file: self.0.eval_file.clone(),
};
inner.with_scopes.push(IrWithScope {
value,
cached: Rc::new(RefCell::new(None)),
});
Self(Rc::new(inner))
}
pub fn set_eval_file(&mut self, file: Option<Rc<PathBuf>>) {
match Rc::get_mut(&mut self.0) {
Some(inner) => inner.eval_file = file,
None => {
let inner = IrEnvInner {
bindings: self.0.bindings.clone(),
with_scopes: self.0.with_scopes.clone(),
eval_file: file,
};
self.0 = Rc::new(inner);
}
}
}
#[must_use]
pub fn eval_file(&self) -> Option<Rc<PathBuf>> {
self.0.eval_file.clone()
}
pub fn bind_sym(&mut self, sym: Symbol, value: IrValue) {
match Rc::get_mut(&mut self.0) {
Some(inner) => {
inner.bindings.insert(sym, value);
}
None => {
let mut inner = IrEnvInner {
bindings: self.0.bindings.clone(),
with_scopes: self.0.with_scopes.clone(),
eval_file: self.0.eval_file.clone(),
};
inner.bindings.insert(sym, value);
self.0 = Rc::new(inner);
}
}
}
pub fn bind(&mut self, name: &str, value: IrValue) {
self.bind_sym(intern(name), value);
}
#[must_use]
pub fn lookup_lexical(&self, sym: Symbol) -> Option<IrValue> {
self.0.bindings.get(&sym).cloned()
}
#[must_use]
pub fn has_with_scope(&self) -> bool {
!self.0.with_scopes.is_empty()
}
#[must_use]
pub fn lookup(&self, name: &str) -> Option<IrValue> {
let sym = intern(name);
if let Some(v) = self.0.bindings.get(&sym) {
return Some(v.clone());
}
for scope in self.0.with_scopes.iter().rev() {
let cached = scope.cached.borrow().clone();
let attrs = if let Some(attrs) = cached {
attrs
} else {
match scope.value.force() {
Ok(IrValue::Attrs(attrs)) => {
*scope.cached.borrow_mut() = Some(attrs.clone());
attrs
}
_ => continue,
}
};
if let Some(v) = attrs.get(name) {
return Some(v.clone());
}
}
None
}
}
pub fn eval_ir(prog: &Rc<Program>, id: ExprId, env: &IrEnv) -> Result<IrValue, IrEvalError> {
match prog.expr(id) {
Ir::Int(n) => Ok(IrValue::Int(*n)),
Ir::Float(f) => Ok(IrValue::Float(*f)),
Ir::Uri(u) => Ok(IrValue::string(u.clone())),
Ir::Ident(sym) => {
let name = resolve(*sym);
match name.as_str() {
"true" => Ok(IrValue::Bool(true)),
"false" => Ok(IrValue::Bool(false)),
"null" => Ok(IrValue::Null),
_ => match env.lookup(&name) {
Some(v) => Ok(v),
None if in_promise_eval() => Ok(IrValue::Null),
None => Err(IrEvalError::UndefinedVar(name)),
},
}
}
Ir::Str(parts) => {
let (s, ctx) = eval_str_parts_ctx(prog, parts, env)?;
Ok(IrValue::string_with_context(s, ctx))
}
Ir::Path { kind, parts } => eval_path_parts(prog, *kind, parts, env),
Ir::SearchPath(raw) => {
let name = raw
.strip_prefix('<')
.and_then(|s| s.strip_suffix('>'))
.unwrap_or(raw);
match crate::path::resolve_search_path(name) {
Some(resolved) => Ok(IrValue::Path(Rc::new(resolved))),
None => Err(IrEvalError::Throw(format!(
"search path '{raw}' not in NIX_PATH"
))),
}
}
Ir::CurPos => Err(IrEvalError::Unsupported("__curPos")),
Ir::LegacyLet { .. } => Err(IrEvalError::Unsupported("legacy-let")),
Ir::Paren(inner) => eval_ir(prog, *inner, env),
Ir::List(items) => Ok(IrValue::List(Rc::new(
items
.iter()
.map(|item| IrValue::Thunk(IrThunk::suspended(prog.clone(), *item, env.clone())))
.collect(),
))),
Ir::Lambda { param, body } => Ok(IrValue::Lambda(Rc::new(IrClosure {
prog: prog.clone(),
param: param.clone(),
body: *body,
env: env.clone(),
}))),
Ir::Apply { func, arg } => {
let f = eval_ir(prog, *func, env)?.force()?;
let arg_value = IrValue::Thunk(IrThunk::suspended(prog.clone(), *arg, env.clone()));
apply(f, arg_value)
}
Ir::IfElse {
condition,
then_body,
else_body,
} => {
if eval_ir(prog, *condition, env)?.force()?.as_bool()? {
eval_ir(prog, *then_body, env)
} else {
eval_ir(prog, *else_body, env)
}
}
Ir::Assert { condition, body } => {
if eval_ir(prog, *condition, env)?.force()?.as_bool()? {
eval_ir(prog, *body, env)
} else {
Err(IrEvalError::AssertionFailed)
}
}
Ir::With { namespace, body } => {
let scope = IrValue::Thunk(IrThunk::suspended(prog.clone(), *namespace, env.clone()));
let new_env = env.child().with_scope(scope);
eval_ir(prog, *body, &new_env)
}
Ir::UnaryOp { op, expr } => {
let v = eval_ir(prog, *expr, env)?.force()?;
match op {
UnaryOp::Negate => match v {
IrValue::Int(n) => Ok(IrValue::Int(-n)),
IrValue::Float(f) => Ok(IrValue::Float(-f)),
other => Err(IrEvalError::TypeError(format!(
"cannot negate {}",
other.type_name()
))),
},
UnaryOp::Invert => Ok(IrValue::Bool(!v.as_bool()?)),
}
}
Ir::BinOp { op, lhs, rhs } => eval_binop(prog, *op, *lhs, *rhs, env),
Ir::Select {
subject,
path,
or_default,
} => eval_select(prog, *subject, path, *or_default, env),
Ir::HasAttr { subject, path } => {
let base = eval_ir(prog, *subject, env)?.force()?;
match traverse_attrpath(prog, base, path, env)? {
Traverse::Found(_) => Ok(IrValue::Bool(true)),
Traverse::Missing(_) | Traverse::NotAttrs => Ok(IrValue::Bool(false)),
}
}
Ir::LetIn { bindings, body } => {
let new_env = eval_let_bindings(prog, bindings, env)?;
eval_ir(prog, *body, &new_env)
}
Ir::AttrSet { rec, bindings } => eval_attrset(prog, *rec, bindings, env),
}
}
fn eval_str_parts(
prog: &Rc<Program>,
parts: &[StrPart],
env: &IrEnv,
) -> Result<String, IrEvalError> {
eval_str_parts_ctx(prog, parts, env).map(|(s, _)| s)
}
fn eval_str_parts_ctx(
prog: &Rc<Program>,
parts: &[StrPart],
env: &IrEnv,
) -> Result<(String, IrStringContext), IrEvalError> {
let mut out = String::new();
let mut ctx = IrStringContext::new();
for part in parts {
match part {
StrPart::Literal(text) => out.push_str(text),
StrPart::Interp(e) => {
let v = eval_ir(prog, *e, env)?.force()?;
let (s, c) = coerce_to_string_ctx(&v, true)?;
out.push_str(&s);
ctx.merge(&c);
}
}
}
Ok((out, ctx))
}
fn eval_path_parts(
prog: &Rc<Program>,
kind: PathKind,
parts: &[PathPart],
env: &IrEnv,
) -> Result<IrValue, IrEvalError> {
let has_interp = parts.iter().any(|p| matches!(p, PathPart::Interp(_)));
let mut text = String::new();
for part in parts {
match part {
PathPart::Literal(t) => text.push_str(t),
PathPart::Interp(e) => {
let v = eval_ir(prog, *e, env)?.force()?;
text.push_str(&coerce_to_string_impl(&v, false)?);
}
}
}
let resolved = match kind {
PathKind::Abs => crate::path::canon_abs(&text),
PathKind::Rel => match current_eval_dir() {
Some(dir) => crate::path::normalize(&dir.join(&text))
.to_string_lossy()
.into_owned(),
None => text,
},
PathKind::Home => {
if has_interp {
crate::path::normalize(std::path::Path::new(&text))
.to_string_lossy()
.into_owned()
} else {
text
}
}
};
Ok(IrValue::Path(Rc::new(resolved)))
}
pub(crate) fn coerce_to_string_plain(v: &IrValue) -> Result<String, IrEvalError> {
coerce_to_string_impl(v, false)
}
fn coerce_to_string_impl(v: &IrValue, copy_to_store: bool) -> Result<String, IrEvalError> {
coerce_to_string_ctx(v, copy_to_store).map(|(s, _)| s)
}
pub(crate) fn coerce_to_string_ctx(
v: &IrValue,
copy_to_store: bool,
) -> Result<(String, IrStringContext), IrEvalError> {
let mut ctx = IrStringContext::new();
let s = match v {
IrValue::Str(s, c) => {
if let Some(c) = c {
ctx.merge(c);
}
(**s).clone()
}
IrValue::Path(p) => {
if copy_to_store {
let raw: &str = p;
let pb = std::path::Path::new(raw);
let abs = if pb.is_absolute() {
pb.to_path_buf()
} else if let Some(dir) = current_eval_dir() {
dir.join(pb)
} else {
std::env::current_dir()
.map_err(|e| IrEvalError::Io {
context: {
let mut s = String::from("copy-to-store coercion of ");
s.push_str(raw);
s
},
message: e.to_string(),
})?
.join(pb)
};
let canon = abs.canonicalize().map_err(|_| {
IrEvalError::TypeError(format!("path '{}' does not exist", abs.display()))
})?;
let name = canon
.file_name()
.map(|n| n.to_string_lossy().into_owned())
.unwrap_or_else(|| "source".to_string());
let src = sui_compat::source::nar_hash_source_tree(&canon, &name)
.map_err(|e| {
IrEvalError::TypeError(format!(
"copy-to-store coercion of '{}': {e}",
canon.display()
))
})?;
ctx.add_plain(src.store_path.clone());
src.store_path
} else {
ctx.add_plain((**p).clone());
(**p).clone()
}
}
IrValue::Int(n) => n.to_string(),
IrValue::Float(f) => format!("{f:.6}"),
IrValue::Bool(true) => "1".to_string(),
IrValue::Bool(false) | IrValue::Null => String::new(),
IrValue::Attrs(attrs) => {
if let Some(to_str) = attrs.get("__toString") {
let r = apply(to_str.force()?, IrValue::Attrs(attrs.clone()))?.force()?;
let (s, c) = coerce_to_string_ctx(&r, copy_to_store)?;
ctx.merge(&c);
s
} else if let Some(out_path) = attrs.get("outPath") {
let (s, c) = coerce_to_string_ctx(&out_path.force()?, copy_to_store)?;
ctx.merge(&c);
s
} else {
return Err(IrEvalError::TypeError(
"cannot coerce set to string (no __toString or outPath)".into(),
));
}
}
IrValue::List(items) => {
let mut parts = Vec::with_capacity(items.len());
for item in items.iter() {
let (s, c) = coerce_to_string_ctx(&item.force()?, copy_to_store)?;
ctx.merge(&c);
parts.push(s);
}
parts.join(" ")
}
IrValue::Thunk(_) => {
let (s, c) = coerce_to_string_ctx(&v.force()?, copy_to_store)?;
ctx.merge(&c);
s
}
other => {
return Err(IrEvalError::TypeError(format!(
"cannot coerce {} to string",
other.type_name()
)))
}
};
Ok((s, ctx))
}
fn eval_attr_maybe_null(
prog: &Rc<Program>,
attr: &AttrName,
env: &IrEnv,
) -> Result<Option<String>, IrEvalError> {
match attr {
AttrName::Ident(sym) => Ok(Some(resolve(*sym))),
AttrName::Str(parts) => Ok(Some(eval_str_parts(prog, parts, env)?)),
AttrName::Dynamic(e) => {
let v = eval_ir(prog, *e, env)?.force()?;
match v {
IrValue::Null => Ok(None),
IrValue::Str(s, _) => Ok(Some((*s).clone())),
other => Err(IrEvalError::TypeMismatch {
expected: "string",
got: other.type_name(),
}),
}
}
}
}
fn eval_attr(prog: &Rc<Program>, attr: &AttrName, env: &IrEnv) -> Result<String, IrEvalError> {
eval_attr_maybe_null(prog, attr, env)?
.ok_or_else(|| IrEvalError::TypeError("null dynamic attribute name".into()))
}
enum Traverse {
Found(IrValue),
Missing(String),
NotAttrs,
}
fn traverse_attrpath(
prog: &Rc<Program>,
base: IrValue,
path: &[AttrName],
env: &IrEnv,
) -> Result<Traverse, IrEvalError> {
let mut value = base;
for (i, attr) in path.iter().enumerate() {
let key = eval_attr(prog, attr, env)?;
let forced = value.force()?;
match forced {
IrValue::Attrs(attrs) => match attrs.get(&key) {
Some(v) => {
value = if i < path.len() - 1 {
v.force()?
} else {
v.clone()
};
}
None => return Ok(Traverse::Missing(key)),
},
_ => return Ok(Traverse::NotAttrs),
}
}
Ok(Traverse::Found(value))
}
fn eval_select(
prog: &Rc<Program>,
subject: ExprId,
path: &[AttrName],
or_default: Option<ExprId>,
env: &IrEnv,
) -> Result<IrValue, IrEvalError> {
let base = match eval_ir(prog, subject, env).and_then(|v| v.force()) {
Ok(v) => v,
Err(IrEvalError::InfiniteRecursion) if or_default.is_some() => {
return eval_ir(prog, or_default.expect("checked"), env);
}
Err(e) => return Err(e),
};
let base_type = base.type_name();
match traverse_attrpath(prog, base, path, env) {
Ok(Traverse::Found(v)) => Ok(v),
Ok(Traverse::Missing(key)) => match or_default {
Some(def) => eval_ir(prog, def, env),
None => Err(IrEvalError::AttrNotFound(key)),
},
Ok(Traverse::NotAttrs) => match or_default {
Some(def) => eval_ir(prog, def, env),
None => Err(IrEvalError::TypeError(format!(
"cannot select from {base_type}"
))),
},
Err(IrEvalError::InfiniteRecursion) if or_default.is_some() => {
eval_ir(prog, or_default.expect("checked"), env)
}
Err(e) => Err(e),
}
}
pub fn apply(func: IrValue, arg: IrValue) -> Result<IrValue, IrEvalError> {
let func = func.force()?;
match func {
IrValue::Lambda(closure) => {
let mut call_env = closure.env.child();
let _file_guard = closure
.env
.eval_file()
.map(|f| push_eval_file((*f).clone()));
match &closure.param {
Param::Ident(sym) => {
call_env.bind_sym(*sym, arg);
}
Param::Pattern {
entries,
ellipsis,
bind,
} => {
let forced = arg.force()?;
let IrValue::Attrs(attrs) = &forced else {
return Err(IrEvalError::TypeMismatch {
expected: "set",
got: forced.type_name(),
});
};
if let Some(bind_sym) = bind {
call_env.bind_sym(*bind_sym, forced.clone());
}
let mut default_thunks: Vec<IrThunk> = Vec::new();
for entry in entries {
let name = resolve(entry.name);
let value = if let Some(v) = attrs.get(&name) {
v.clone()
} else if let Some(default_expr) = entry.default {
let t = IrThunk::suspended(
closure.prog.clone(),
default_expr,
call_env.clone(),
);
default_thunks.push(t.clone());
IrValue::Thunk(t)
} else {
return Err(IrEvalError::TypeError(format!(
"missing argument '{name}'"
)));
};
call_env.bind_sym(entry.name, value);
}
for t in &default_thunks {
t.update_env(&call_env);
}
if !ellipsis {
let entry_names: HashSet<String> =
entries.iter().map(|e| resolve(e.name)).collect();
for key in attrs.keys() {
if !entry_names.contains(key) {
return Err(IrEvalError::TypeError(format!(
"unexpected argument '{key}'"
)));
}
}
}
}
}
eval_ir(&closure.prog, closure.body, &call_env)
}
IrValue::Builtin(kind, captured) => {
let arg = if kind.wants_unforced_arg(captured.len()) {
arg
} else {
arg.force()?
};
crate::builtins::apply_builtin(kind, &captured, arg)
}
IrValue::Attrs(attrs) => {
if let Some(functor) = attrs.get("__functor") {
let f = apply(functor.force()?, IrValue::Attrs(attrs.clone()))?;
apply(f, arg)
} else if in_promise_eval() {
Ok(IrValue::Null)
} else {
Err(IrEvalError::TypeError(
"attempt to call something which is not a function but a set".into(),
))
}
}
_ if in_promise_eval() => Ok(IrValue::Null),
other => Err(IrEvalError::TypeError(format!(
"attempt to call something which is not a function but a {}",
other.type_name()
))),
}
}
#[allow(clippy::too_many_lines)]
fn eval_binop(
prog: &Rc<Program>,
op: BinOp,
lhs: ExprId,
rhs: ExprId,
env: &IrEnv,
) -> Result<IrValue, IrEvalError> {
match op {
BinOp::And => {
let l = eval_ir(prog, lhs, env)?.force()?.as_bool()?;
return if l {
eval_ir(prog, rhs, env)
} else {
Ok(IrValue::Bool(false))
};
}
BinOp::Or => {
let l = eval_ir(prog, lhs, env)?.force()?.as_bool()?;
return if l {
Ok(IrValue::Bool(true))
} else {
eval_ir(prog, rhs, env)
};
}
BinOp::Implication => {
let l = eval_ir(prog, lhs, env)?.force()?.as_bool()?;
return if l {
eval_ir(prog, rhs, env)
} else {
Ok(IrValue::Bool(true))
};
}
_ => {}
}
let l = eval_ir(prog, lhs, env)?.force()?;
let r = eval_ir(prog, rhs, env)?.force()?;
match op {
BinOp::Add => match (&l, &r) {
(IrValue::Int(a), IrValue::Int(b)) => a
.checked_add(*b)
.map(IrValue::Int)
.ok_or_else(|| int_overflow("adding", *a, '+', *b)),
(IrValue::Float(a), IrValue::Float(b)) => Ok(IrValue::Float(a + b)),
(IrValue::Int(a), IrValue::Float(b)) => Ok(IrValue::Float(*a as f64 + b)),
(IrValue::Float(a), IrValue::Int(b)) => Ok(IrValue::Float(a + *b as f64)),
(IrValue::Str(a, ca), IrValue::Str(b, cb)) => {
let mut s = String::with_capacity(a.len() + b.len());
s.push_str(a);
s.push_str(b);
let mut ctx = IrStringContext::new();
if let Some(ca) = ca {
ctx.merge(ca);
}
if let Some(cb) = cb {
ctx.merge(cb);
}
Ok(IrValue::string_with_context(s, ctx))
}
(IrValue::Path(a), IrValue::Str(b, _)) => {
Ok(IrValue::Path(Rc::new(format_concat(a, b))))
}
(IrValue::Path(a), IrValue::Path(b)) => {
let mut s = String::with_capacity(a.len() + b.len() + 1);
s.push_str(a);
s.push('/');
s.push_str(b);
Ok(IrValue::Path(Rc::new(s)))
}
(IrValue::Attrs(_), _) | (_, IrValue::Attrs(_)) => {
let ls = coerce_to_string_plain(&l)?;
let rs = coerce_to_string_plain(&r)?;
Ok(IrValue::string(format_concat(&ls, &rs)))
}
_ => Err(op_type_error("add", &l, &r)),
},
BinOp::Sub => num_op(&l, &r, i64::checked_sub, |a, b| a - b, "subtracting", '-'),
BinOp::Mul => num_op(&l, &r, i64::checked_mul, |a, b| a * b, "multiplying", '*'),
BinOp::Div => {
let rhs_is_zero = match &r {
IrValue::Int(0) => true,
IrValue::Float(f) => *f == 0.0,
_ => false,
};
if rhs_is_zero {
return Err(IrEvalError::DivisionByZero);
}
num_op(&l, &r, i64::checked_div, |a, b| a / b, "dividing", '/')
}
BinOp::Equal => Ok(IrValue::Bool(ir_eq(&l, &r))),
BinOp::NotEqual => Ok(IrValue::Bool(!ir_eq(&l, &r))),
BinOp::Less => compare(&l, &r, |o| o == std::cmp::Ordering::Less),
BinOp::LessOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Greater),
BinOp::More => compare(&l, &r, |o| o == std::cmp::Ordering::Greater),
BinOp::MoreOrEq => compare(&l, &r, |o| o != std::cmp::Ordering::Less),
BinOp::Update => {
let IrValue::Attrs(la) = &l else {
return Err(IrEvalError::TypeMismatch {
expected: "set",
got: l.type_name(),
});
};
let IrValue::Attrs(ra) = &r else {
return Err(IrEvalError::TypeMismatch {
expected: "set",
got: r.type_name(),
});
};
let mut merged = (**la).clone();
for (k, v) in ra.iter() {
merged.insert(k.clone(), v.clone());
}
Ok(IrValue::Attrs(Rc::new(merged)))
}
BinOp::Concat => {
let IrValue::List(la) = &l else {
return Err(IrEvalError::TypeMismatch {
expected: "list",
got: l.type_name(),
});
};
let IrValue::List(ra) = &r else {
return Err(IrEvalError::TypeMismatch {
expected: "list",
got: r.type_name(),
});
};
let mut out = Vec::with_capacity(la.len() + ra.len());
out.extend(la.iter().cloned());
out.extend(ra.iter().cloned());
Ok(IrValue::List(Rc::new(out)))
}
BinOp::And | BinOp::Or | BinOp::Implication => unreachable!("handled above"),
BinOp::PipeRight | BinOp::PipeLeft => Err(IrEvalError::Unsupported("pipe-operators")),
}
}
fn format_concat(ls: &str, rs: &str) -> String {
let mut s = String::with_capacity(ls.len() + rs.len());
s.push_str(ls);
s.push_str(rs);
s
}
fn int_overflow(verb: &str, a: i64, sym: char, b: i64) -> IrEvalError {
IrEvalError::Abort(format!("integer overflow in {verb} {a} {sym} {b}"))
}
fn op_type_error(op: &str, l: &IrValue, r: &IrValue) -> IrEvalError {
IrEvalError::TypeError(format!(
"cannot {op} {} and {}",
l.type_name(),
r.type_name()
))
}
fn num_op(
l: &IrValue,
r: &IrValue,
int_op: impl Fn(i64, i64) -> Option<i64>,
float_op: impl Fn(f64, f64) -> f64,
verb: &'static str,
sym: char,
) -> Result<IrValue, IrEvalError> {
match (l, r) {
(IrValue::Int(a), IrValue::Int(b)) => int_op(*a, *b)
.map(IrValue::Int)
.ok_or_else(|| int_overflow(verb, *a, sym, *b)),
(IrValue::Float(a), IrValue::Float(b)) => Ok(IrValue::Float(float_op(*a, *b))),
(IrValue::Int(a), IrValue::Float(b)) => Ok(IrValue::Float(float_op(*a as f64, *b))),
(IrValue::Float(a), IrValue::Int(b)) => Ok(IrValue::Float(float_op(*a, *b as f64))),
_ => Err(op_type_error("perform arithmetic on", l, r)),
}
}
fn compare(
l: &IrValue,
r: &IrValue,
pred: impl Fn(std::cmp::Ordering) -> bool,
) -> Result<IrValue, IrEvalError> {
let ord = match (l, r) {
(IrValue::Int(a), IrValue::Int(b)) => a.cmp(b),
(IrValue::Float(a), IrValue::Float(b)) => {
a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal)
}
(IrValue::Int(a), IrValue::Float(b)) => (*a as f64)
.partial_cmp(b)
.unwrap_or(std::cmp::Ordering::Equal),
(IrValue::Float(a), IrValue::Int(b)) => a
.partial_cmp(&(*b as f64))
.unwrap_or(std::cmp::Ordering::Equal),
(IrValue::Str(a, _), IrValue::Str(b, _)) => a.cmp(b),
_ => return Err(op_type_error("compare", l, r)),
};
Ok(IrValue::Bool(pred(ord)))
}
#[must_use]
pub fn ir_eq(l: &IrValue, r: &IrValue) -> bool {
let l = l.force().unwrap_or(IrValue::Null);
let r = r.force().unwrap_or(IrValue::Null);
match (&l, &r) {
(IrValue::Null, IrValue::Null) => true,
(IrValue::Bool(a), IrValue::Bool(b)) => a == b,
(IrValue::Int(a), IrValue::Int(b)) => a == b,
(IrValue::Float(a), IrValue::Float(b)) => a == b,
(IrValue::Int(a), IrValue::Float(b)) | (IrValue::Float(b), IrValue::Int(a)) => {
(*a as f64) == *b
}
(IrValue::Str(a, _), IrValue::Str(b, _)) => a == b,
(IrValue::Path(a), IrValue::Path(b)) => a == b,
(IrValue::List(a), IrValue::List(b)) => {
Rc::ptr_eq(a, b) || (a.len() == b.len() && a.iter().zip(b.iter()).all(|(x, y)| ir_eq(x, y)))
}
(IrValue::Attrs(a), IrValue::Attrs(b)) => {
if Rc::ptr_eq(a, b) {
return true;
}
if let (Some(pa), Some(pb)) = (derivation_out_path(a), derivation_out_path(b)) {
return pa == pb;
}
a.len() == b.len()
&& a.iter()
.all(|(k, v)| b.get(k).is_some_and(|w| ir_eq(v, w)))
}
(IrValue::Lambda(a), IrValue::Lambda(b)) => Rc::ptr_eq(a, b),
_ => false,
}
}
fn derivation_out_path(attrs: &IrAttrs) -> Option<String> {
let ty = attrs.get("type")?.force().ok()?;
let IrValue::Str(s, _) = ty else { return None };
if &**s != "derivation" {
return None;
}
let out = attrs.get("outPath")?.force().ok()?;
match out {
IrValue::Str(p, _) => Some((*p).clone()),
_ => None,
}
}
fn referenced_idents(prog: &Program, id: ExprId, out: &mut HashSet<Symbol>) {
if let Ir::Ident(sym) = prog.expr(id) {
out.insert(*sym);
}
for child in prog.children(id) {
referenced_idents(prog, child, out);
}
}
fn eval_let_bindings(
prog: &Rc<Program>,
bindings: &[Binding],
env: &IrEnv,
) -> Result<IrEnv, IrEvalError> {
let mut new_env = env.child();
let mut thunks: Vec<IrThunk> = Vec::new();
let mut dotted_attrs = IrAttrs::new();
let mut let_scope_names: HashSet<String> = HashSet::new();
for binding in bindings {
match binding {
Binding::Path { path, .. } => {
if let Some(first) = path.first() {
if let Ok(Some(name)) = eval_attr_maybe_null(prog, first, env) {
let_scope_names.insert(name);
}
}
}
Binding::Inherit { attrs, .. } => {
for attr in attrs {
if let Ok(Some(name)) = eval_attr_maybe_null(prog, attr, env) {
let_scope_names.insert(name);
}
}
}
}
}
for binding in bindings {
match binding {
Binding::Path { path, value } => {
let mut path_keys = Vec::with_capacity(path.len());
for attr in path {
path_keys.push(eval_attr(prog, attr, env)?);
}
if path_keys.len() == 1 {
let key = path_keys.pop().expect("len checked");
let mut referenced = HashSet::new();
referenced_idents(prog, *value, &mut referenced);
let in_mutual_cycle = std::iter::once(&key)
.chain(let_scope_names.iter())
.any(|n| referenced.contains(&intern(n)));
let thunk = if in_mutual_cycle {
IrThunk::suspended_recursive(prog.clone(), *value, env.clone())
} else {
IrThunk::suspended(prog.clone(), *value, env.clone())
};
thunks.push(thunk.clone());
new_env.bind(&key, IrValue::Thunk(thunk));
} else {
let key = path_keys[0].clone();
let inner =
build_nested_attr_thunk(prog, &path_keys[1..], *value, env, &mut thunks);
merge_nested_insert(&mut dotted_attrs, key, inner)?;
}
}
Binding::Inherit { from, attrs } => {
if let Some(source_expr) = from {
let source_thunk =
IrThunk::suspended(prog.clone(), *source_expr, env.clone());
for attr in attrs {
let name = eval_attr(prog, attr, env)?;
let t = IrThunk::inherit_select(source_thunk.clone(), name.clone());
thunks.push(t.clone());
new_env.bind(&name, IrValue::Thunk(t));
}
} else {
for attr in attrs {
let name = eval_attr(prog, attr, env)?;
let value = env
.lookup(&name)
.ok_or_else(|| IrEvalError::UndefinedVar(name.clone()))?;
new_env.bind(&name, value);
}
}
}
}
}
for (key, value) in &dotted_attrs {
new_env.bind(key, value.clone());
}
for thunk in &thunks {
thunk.update_env(&new_env);
}
Ok(new_env)
}
fn build_nested_attr_thunk(
prog: &Rc<Program>,
path: &[String],
value: ExprId,
env: &IrEnv,
thunks: &mut Vec<IrThunk>,
) -> IrValue {
if path.is_empty() {
let t = IrThunk::suspended(prog.clone(), value, env.clone());
thunks.push(t.clone());
return IrValue::Thunk(t);
}
let inner = build_nested_attr_thunk(prog, &path[1..], value, env, thunks);
let mut attrs = IrAttrs::new();
attrs.insert(path[0].clone(), inner);
IrValue::Attrs(Rc::new(attrs))
}
fn build_nested_attr(prog: &Rc<Program>, path: &[String], value: ExprId, env: &IrEnv) -> IrValue {
if path.is_empty() {
return IrValue::Thunk(IrThunk::suspended(prog.clone(), value, env.clone()));
}
let inner = build_nested_attr(prog, &path[1..], value, env);
let mut attrs = IrAttrs::new();
attrs.insert(path[0].clone(), inner);
IrValue::Attrs(Rc::new(attrs))
}
fn merge_nested_insert(
target: &mut IrAttrs,
key: String,
value: IrValue,
) -> Result<(), IrEvalError> {
let Some(existing) = target.get(&key).cloned() else {
target.insert(key, value);
return Ok(());
};
let value = match &value {
IrValue::Thunk(_) => match value.force() {
Ok(v @ IrValue::Attrs(_)) => v,
_ => value,
},
_ => value,
};
if !matches!(value, IrValue::Attrs(_)) {
target.insert(key, value);
return Ok(());
}
let existing_concrete = match &existing {
IrValue::Attrs(_) => existing.clone(),
IrValue::Thunk(_) => match existing.force() {
Ok(v @ IrValue::Attrs(_)) => v,
_ => {
target.insert(key, value);
return Ok(());
}
},
_ => {
target.insert(key, value);
return Ok(());
}
};
let IrValue::Attrs(existing_rc) = existing_concrete else {
unreachable!()
};
let IrValue::Attrs(new_rc) = value else {
unreachable!()
};
let mut merged = (*existing_rc).clone();
for (k, v) in new_rc.iter() {
merge_nested_insert(&mut merged, k.clone(), v.clone())?;
}
target.insert(key, IrValue::Attrs(Rc::new(merged)));
Ok(())
}
fn build_tail_attrs(
prog: &Rc<Program>,
tail: &[AttrName],
value: ExprId,
env: &IrEnv,
) -> Result<IrValue, IrEvalError> {
let Some((head, rest)) = tail.split_first() else {
return eval_ir(prog, value, env).and_then(|v| v.force());
};
let mut attrs = IrAttrs::new();
if let Some(key) = eval_attr_maybe_null(prog, head, env)? {
let inner = if rest.is_empty() {
IrValue::Thunk(IrThunk::suspended(prog.clone(), value, env.clone()))
} else {
IrValue::Thunk(IrThunk::deferred_tail(
prog.clone(),
rest.to_vec(),
value,
env.clone(),
))
};
attrs.insert(key, inner);
}
Ok(IrValue::Attrs(Rc::new(attrs)))
}
fn attrname_is_dynamic(attr: &AttrName) -> bool {
match attr {
AttrName::Ident(_) => false,
AttrName::Dynamic(_) => true,
AttrName::Str(parts) => parts.iter().any(|p| matches!(p, StrPart::Interp(_))),
}
}
#[allow(clippy::too_many_lines)]
fn eval_attrset(
prog: &Rc<Program>,
rec: bool,
bindings: &[Binding],
env: &IrEnv,
) -> Result<IrValue, IrEvalError> {
let mut attrs = IrAttrs::new();
if rec {
let mut rec_env = env.child();
let mut thunks: Vec<IrThunk> = Vec::new();
let mut defined_so_far: HashSet<String> = HashSet::new();
let mut dotted_attrs = IrAttrs::new();
for binding in bindings {
match binding {
Binding::Path { path, value } => {
let mut path_keys = Vec::with_capacity(path.len());
let mut skip = false;
for attr in path {
match eval_attr_maybe_null(prog, attr, env)? {
Some(k) => path_keys.push(k),
None => {
skip = true;
break;
}
}
}
if skip || path_keys.is_empty() {
continue;
}
if path_keys.len() == 1 {
let key = path_keys.pop().expect("len checked");
let mut referenced = HashSet::new();
referenced_idents(prog, *value, &mut referenced);
let is_recursive = referenced.contains(&intern(&key))
|| defined_so_far.iter().any(|n| referenced.contains(&intern(n)));
let thunk = if is_recursive {
IrThunk::suspended_recursive(prog.clone(), *value, env.clone())
} else {
IrThunk::suspended(prog.clone(), *value, env.clone())
};
thunks.push(thunk.clone());
let v = IrValue::Thunk(thunk);
rec_env.bind(&key, v.clone());
attrs.insert(key.clone(), v);
defined_so_far.insert(key);
} else {
let key = path_keys[0].clone();
let inner = build_nested_attr_thunk(
prog,
&path_keys[1..],
*value,
env,
&mut thunks,
);
merge_nested_insert(&mut dotted_attrs, key, inner)?;
}
}
Binding::Inherit { from, attrs: names } => {
eval_inherit(
prog,
from.as_ref(),
names,
env,
&mut attrs,
Some(&mut rec_env),
Some(&mut thunks),
)?;
}
}
}
for (key, value) in &dotted_attrs {
attrs.insert(key.clone(), value.clone());
rec_env.bind(key, value.clone());
}
for thunk in &thunks {
thunk.update_env(&rec_env);
}
} else {
for binding in bindings {
match binding {
Binding::Path { path, value } => {
let tail_is_dynamic = path.len() > 1 && path[1..].iter().any(attrname_is_dynamic);
let Some(head_key) = eval_attr_maybe_null(prog, &path[0], env)? else {
continue;
};
if tail_is_dynamic && !attrs.contains_key(&head_key) {
let t = IrThunk::deferred_tail(
prog.clone(),
path[1..].to_vec(),
*value,
env.clone(),
);
attrs.insert(head_key, IrValue::Thunk(t));
continue;
}
if tail_is_dynamic {
let sub = build_tail_attrs(prog, &path[1..], *value, env)?;
merge_nested_insert(&mut attrs, head_key, sub)?;
continue;
}
let mut path_keys = vec![head_key];
let mut skip = false;
for attr in &path[1..] {
match eval_attr_maybe_null(prog, attr, env)? {
Some(k) => path_keys.push(k),
None => {
skip = true;
break;
}
}
}
if skip {
continue;
}
if path_keys.len() == 1 {
let key = path_keys.pop().expect("len checked");
let value =
IrValue::Thunk(IrThunk::suspended(prog.clone(), *value, env.clone()));
if matches!(attrs.get(&key), Some(IrValue::Attrs(_))) {
let forced = value.force()?;
merge_nested_insert(&mut attrs, key, forced)?;
} else {
attrs.insert(key, value);
}
} else {
let key = path_keys[0].clone();
let value = build_nested_attr(prog, &path_keys[1..], *value, env);
if matches!(attrs.get(&key), Some(IrValue::Thunk(_))) {
let existing = attrs.get(&key).cloned().expect("just matched");
let forced = existing.force()?;
attrs.insert(key.clone(), forced);
}
merge_nested_insert(&mut attrs, key, value)?;
}
}
Binding::Inherit { from, attrs: names } => {
eval_inherit(prog, from.as_ref(), names, env, &mut attrs, None, None)?;
}
}
}
}
Ok(IrValue::Attrs(Rc::new(attrs)))
}
fn eval_inherit(
prog: &Rc<Program>,
from: Option<&ExprId>,
names: &[AttrName],
env: &IrEnv,
attrs: &mut IrAttrs,
mut bind_env: Option<&mut IrEnv>,
mut thunks: Option<&mut Vec<IrThunk>>,
) -> Result<(), IrEvalError> {
if let Some(source_expr) = from {
let source_thunk = IrThunk::suspended(prog.clone(), *source_expr, env.clone());
for attr in names {
let name = eval_attr(prog, attr, env)?;
let t = IrThunk::inherit_select(source_thunk.clone(), name.clone());
let value = IrValue::Thunk(t.clone());
attrs.insert(name.clone(), value.clone());
if let Some(e) = bind_env.as_deref_mut() {
e.bind(&name, value);
}
if let Some(ts) = thunks.as_deref_mut() {
ts.push(t);
}
}
} else {
for attr in names {
let name = eval_attr(prog, attr, env)?;
let value = if let Some(v) = env.lookup(&name) {
v
} else if env.has_with_scope() {
IrValue::Thunk(IrThunk::with_ident(name.clone(), env.clone()))
} else {
return Err(IrEvalError::UndefinedVar(name));
};
attrs.insert(name.clone(), value.clone());
if let Some(e) = bind_env.as_deref_mut() {
e.bind(&name, value);
}
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::lower_file;
fn ev(src: &str) -> Result<IrValue, IrEvalError> {
let prog = Rc::new(lower_file(src).expect("lowers"));
let env = IrEnv::with_pure_builtins();
eval_ir(&prog, prog.root, &env).and_then(|v| v.force())
}
fn ev_int(src: &str) -> i64 {
match ev(src) {
Ok(IrValue::Int(n)) => n,
other => panic!("expected int for {src:?}, got {other:?}"),
}
}
fn ev_str(src: &str) -> String {
match ev(src) {
Ok(IrValue::Str(s, _)) => (*s).clone(),
other => panic!("expected string for {src:?}, got {other:?}"),
}
}
#[test]
fn literals() {
assert_eq!(ev_int("42"), 42);
assert!(matches!(ev("1.5"), Ok(IrValue::Float(f)) if (f - 1.5).abs() < f64::EPSILON));
assert!(matches!(ev("true"), Ok(IrValue::Bool(true))));
assert!(matches!(ev("null"), Ok(IrValue::Null)));
assert_eq!(ev_str("\"hi\""), "hi");
}
#[test]
fn arithmetic_and_precedence() {
assert_eq!(ev_int("1 + 2 * 3"), 7);
assert_eq!(ev_int("(1 + 2) * 3"), 9);
assert!(matches!(ev("1 / 0"), Err(IrEvalError::DivisionByZero)));
assert!(matches!(
ev("9223372036854775807 + 1"),
Err(IrEvalError::Abort(_))
));
}
#[test]
fn short_circuit_mirrors_walker() {
assert_eq!(ev_int("false || 1"), 1);
assert_eq!(ev_int("true && 1"), 1);
assert_eq!(ev_int("true -> 1"), 1);
assert!(matches!(ev("1 && true"), Err(IrEvalError::TypeMismatch { .. })));
}
#[test]
fn let_lambda_apply() {
assert_eq!(ev_int("let f = x: x + 1; in f 41"), 42);
assert_eq!(ev_int("let f = { a ? 3 }: a; in f { }"), 3);
assert_eq!(ev_int("(args @ { a, ... }: args.a) { a = 5; b = 6; }"), 5);
assert_eq!(ev_int("let boom = 1 / 0; in 7"), 7);
}
#[test]
fn attrsets_select_hasattr() {
assert_eq!(ev_int("{ a = { b = 2; }; }.a.b"), 2);
assert_eq!(ev_int("{ a = 1; }.b or 9"), 9);
assert_eq!(ev_int("rec { a = b; b = 3; }.a"), 3);
assert!(matches!(ev("{ a = 1; } ? a"), Ok(IrValue::Bool(true))));
assert!(matches!(ev("1 ? a"), Ok(IrValue::Bool(false))));
assert_eq!(ev_int("let s = { a.b = 1; a = { c = 2; }; }; in s.a.b + s.a.c"), 3);
}
#[test]
fn with_and_inherit() {
assert_eq!(ev_int("with { a = 1; }; a"), 1);
assert_eq!(ev_int("let a = 2; in with { a = 1; }; a"), 2); assert_eq!(ev_int("let a = 4; s = { inherit a; }; in s.a"), 4);
assert_eq!(ev_int("let k = { x = 8; }; in (rec { inherit (k) x; y = x; }).y"), 8);
}
#[test]
fn string_interp_and_tostring() {
assert_eq!(ev_str(r#"let x = "b"; in "a${x}c""#), "abc");
assert_eq!(ev_str(r#""n=${toString 1}""#), "n=1");
assert_eq!(ev_str(r#""${1}""#), "1");
assert_eq!(ev_str(r#""${1.5}""#), "1.500000");
}
#[test]
fn self_alias_is_infinite_recursion_like_walker() {
assert!(matches!(
ev("let x = x; in x"),
Err(IrEvalError::InfiniteRecursion)
));
assert!(matches!(
ev("let a = b; b = a; in a"),
Err(IrEvalError::InfiniteRecursion)
));
}
#[test]
fn typed_gaps() {
assert!(matches!(
ev("<sui-ir-slice4-definitely-absent>"),
Err(IrEvalError::Throw(_))
));
assert!(matches!(
ev("let { body = 1; }"),
Err(IrEvalError::Unsupported("legacy-let"))
));
assert!(matches!(
ev("__curPos"),
Err(IrEvalError::Unsupported("__curPos"))
));
assert!(matches!(
ev("builtins.sort (a: b: a < b) [ 2 1 ]"),
Ok(IrValue::List(_))
));
assert!(matches!(
ev("derivation { }"),
Err(IrEvalError::AttrNotFound(n)) if n == "name"
));
assert!(matches!(
ev("(derivation { name = \"x\"; system = \"aarch64-darwin\"; builder = \"/bin/sh\"; }).type"),
Ok(IrValue::Str(s, _)) if *s == "derivation"
));
assert!(matches!(
ev("builtins.getEnv \"SUI_IR_SURELY_UNSET_VAR_XYZ\""),
Ok(IrValue::Str(s, _)) if s.is_empty()
));
assert!(!matches!(
ev(r#""${./x}""#),
Err(IrEvalError::Unsupported("path-copy-to-store"))
));
assert!(matches!(
ev("builtins.unsafeGetAttrPos \"a\" { a = 1; }"),
Ok(IrValue::Null)
));
}
#[test]
fn paths_evaluate_like_the_walker() {
assert!(matches!(ev("./x"), Ok(IrValue::Path(p)) if *p == "./x"));
assert!(matches!(ev("~/dir/file"), Ok(IrValue::Path(p)) if *p == "~/dir/file"));
assert!(matches!(ev("/foo/../bar"), Ok(IrValue::Path(p)) if *p == "/bar"));
assert!(matches!(ev("/.."), Ok(IrValue::Path(p)) if *p == "/"));
assert!(
matches!(ev("toString /bar/${/tmp/foo}"), Ok(IrValue::Str(s, _)) if *s == "/bar/tmp/foo")
);
assert_eq!(ev_str(r#"let x = "foo"; in toString /a/${x}/b"#), "/a/foo/b");
assert!(matches!(ev(r#"./x + "/y""#), Ok(IrValue::Path(p)) if *p == "./x/y"));
assert!(matches!(ev("/a + /b"), Ok(IrValue::Path(p)) if *p == "/a//b"));
assert!(matches!(ev(r#""s" + /a"#), Err(IrEvalError::TypeError(_))));
assert_eq!(ev_str("builtins.typeOf ./x"), "path");
assert!(matches!(ev("/a/b == /a/b"), Ok(IrValue::Bool(true))));
assert!(matches!(ev(r#"/a/b == "/a/b""#), Ok(IrValue::Bool(false))));
}
#[test]
fn builtins_bridge_basics() {
assert_eq!(ev_int("builtins.length [ 1 2 3 ]"), 3);
assert_eq!(ev_str("builtins.concatStringsSep \"-\" [ \"a\" \"b\" ]"), "a-b");
assert_eq!(ev_int("builtins.foldl' (a: b: a + b) 0 [ 1 2 3 ]"), 6);
assert_eq!(ev_str("builtins.substring 1 2 \"abcd\""), "bc");
assert!(matches!(ev("builtins ? sort"), Ok(IrValue::Bool(true))));
assert!(matches!(ev("builtins ? builtins"), Ok(IrValue::Bool(true))));
assert!(matches!(ev("builtins.builtins ? builtins"), Ok(IrValue::Bool(false))));
}
#[test]
fn map_is_lazy_and_correct() {
let v = ev("map (x: x + 1) [ 1 2 3 ]").expect("maps");
let IrValue::List(items) = v else { panic!("expected list") };
let forced: Vec<i64> = items
.iter()
.map(|v| match v.force().expect("forces") {
IrValue::Int(n) => n,
other => panic!("expected int, got {other:?}"),
})
.collect();
assert_eq!(forced, vec![2, 3, 4]);
}
}