use std::rc::Rc;
use sui_compat::versions::{
compare_versions, cppnix_format_float, parse_drv_name, split_version,
};
use crate::eval_ir::{
apply, coerce_to_string_plain, ir_eq, IrAttrs, IrContextElem, IrEnv, IrEvalError, IrThunk,
IrValue,
};
use crate::file_eval;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IrBuiltin {
ToString,
TypeOf,
IsNull,
IsInt,
IsFloat,
IsBool,
IsString,
IsList,
IsAttrs,
IsFunction,
IsPath,
Length,
Head,
Tail,
AttrNames,
AttrValues,
ConcatLists,
ListToAttrs,
StringLength,
Import,
Throw,
Abort,
TryEval,
Ceil,
Floor,
ToJson,
FromJson,
ToXml,
FunctionArgs,
GenericClosure,
SplitVersion,
ParseDrvName,
ConcatStrings,
HasContext,
GetContext,
UnsafeDiscardStringContext,
ToLower,
ToUpper,
BaseNameOf,
DirOf,
Derivation,
DerivationStrict,
ConvertHash,
PathExists,
ReadFile,
ReadDir,
ReadFileType,
GetEnv,
Placeholder,
AddErrorContext,
UnsafeGetAttrPos,
Map,
Filter,
ElemAt,
HasAttr,
GetAttr,
IntersectAttrs,
MapAttrs,
RemoveAttrs,
GenList,
Seq,
DeepSeq,
ConcatStringsSep,
Split,
Add,
Sub,
Mul,
Div,
LessThan,
BitAnd,
BitOr,
BitXor,
Elem,
Sort,
All,
Any,
Partition,
GroupBy,
ConcatMap,
CatAttrs,
ZipAttrsWith,
FilterAttrs,
CompareVersions,
Match,
HasPrefix,
HasSuffix,
FindFile,
Trace,
TraceVerbose,
HashString,
Foldl,
Substring,
ReplaceStrings,
}
impl IrBuiltin {
#[must_use]
pub fn arity(self) -> usize {
use IrBuiltin as B;
match self {
B::ToString
| B::TypeOf
| B::IsNull
| B::IsInt
| B::IsFloat
| B::IsBool
| B::IsString
| B::IsList
| B::IsAttrs
| B::IsFunction
| B::IsPath
| B::Length
| B::Head
| B::Tail
| B::AttrNames
| B::AttrValues
| B::ConcatLists
| B::ListToAttrs
| B::StringLength
| B::Import
| B::Throw
| B::Abort
| B::TryEval
| B::Ceil
| B::Floor
| B::ToJson
| B::FromJson
| B::ToXml
| B::FunctionArgs
| B::GenericClosure
| B::SplitVersion
| B::ParseDrvName
| B::ConcatStrings
| B::HasContext
| B::GetContext
| B::UnsafeDiscardStringContext
| B::ToLower
| B::ToUpper
| B::BaseNameOf
| B::DirOf
| B::Derivation
| B::DerivationStrict
| B::ConvertHash
| B::PathExists
| B::ReadFile
| B::ReadDir
| B::ReadFileType
| B::GetEnv
| B::Placeholder => 1,
B::Map
| B::Filter
| B::ElemAt
| B::HasAttr
| B::GetAttr
| B::IntersectAttrs
| B::MapAttrs
| B::RemoveAttrs
| B::GenList
| B::Seq
| B::DeepSeq
| B::ConcatStringsSep
| B::Split
| B::Add
| B::Sub
| B::Mul
| B::Div
| B::LessThan
| B::BitAnd
| B::BitOr
| B::BitXor
| B::Elem
| B::Sort
| B::All
| B::Any
| B::Partition
| B::GroupBy
| B::ConcatMap
| B::CatAttrs
| B::ZipAttrsWith
| B::FilterAttrs
| B::CompareVersions
| B::Match
| B::HasPrefix
| B::HasSuffix
| B::FindFile
| B::Trace
| B::TraceVerbose
| B::AddErrorContext
| B::UnsafeGetAttrPos
| B::HashString => 2,
B::Foldl | B::Substring | B::ReplaceStrings => 3,
}
}
#[must_use]
pub fn registry_name(self) -> &'static str {
use IrBuiltin as B;
match self {
B::ToString => "toString",
B::TypeOf => "typeOf",
B::IsNull => "isNull",
B::IsInt => "isInt",
B::IsFloat => "isFloat",
B::IsBool => "isBool",
B::IsString => "isString",
B::IsList => "isList",
B::IsAttrs => "isAttrs",
B::IsFunction => "isFunction",
B::IsPath => "isPath",
B::Length => "length",
B::Head => "head",
B::Tail => "tail",
B::AttrNames => "attrNames",
B::AttrValues => "attrValues",
B::ConcatLists => "concatLists",
B::ListToAttrs => "listToAttrs",
B::StringLength => "stringLength",
B::Import => "import",
B::Throw => "throw",
B::Abort => "abort",
B::TryEval => "tryEval",
B::Ceil => "ceil",
B::Floor => "floor",
B::ToJson => "toJSON",
B::FromJson => "fromJSON",
B::ToXml => "toXML",
B::FunctionArgs => "functionArgs",
B::GenericClosure => "genericClosure",
B::SplitVersion => "splitVersion",
B::ParseDrvName => "parseDrvName",
B::ConcatStrings => "concatStrings",
B::HasContext => "hasContext",
B::GetContext => "getContext",
B::UnsafeDiscardStringContext => "unsafeDiscardStringContext",
B::ToLower => "toLower",
B::ToUpper => "toUpper",
B::BaseNameOf => "baseNameOf",
B::DirOf => "dirOf",
B::Derivation => "derivation",
B::DerivationStrict => "derivationStrict",
B::ConvertHash => "convertHash",
B::HashString => "hashString",
B::PathExists => "pathExists",
B::ReadFile => "readFile",
B::ReadDir => "readDir",
B::ReadFileType => "readFileType",
B::GetEnv => "getEnv",
B::Placeholder => "placeholder",
B::AddErrorContext => "addErrorContext",
B::UnsafeGetAttrPos => "unsafeGetAttrPos",
B::Add => "add",
B::Sub => "sub",
B::Mul => "mul",
B::Div => "div",
B::LessThan => "lessThan",
B::BitAnd => "bitAnd",
B::BitOr => "bitOr",
B::BitXor => "bitXor",
B::Elem => "elem",
B::Sort => "sort",
B::All => "all",
B::Any => "any",
B::Partition => "partition",
B::GroupBy => "groupBy",
B::ConcatMap => "concatMap",
B::CatAttrs => "catAttrs",
B::ZipAttrsWith => "zipAttrsWith",
B::FilterAttrs => "filterAttrs",
B::CompareVersions => "compareVersions",
B::Match => "match",
B::HasPrefix => "hasPrefix",
B::HasSuffix => "hasSuffix",
B::FindFile => "findFile",
B::Trace => "trace",
B::TraceVerbose => "traceVerbose",
B::Map => "map",
B::Filter => "filter",
B::ElemAt => "elemAt",
B::HasAttr => "hasAttr",
B::GetAttr => "getAttr",
B::IntersectAttrs => "intersectAttrs",
B::MapAttrs => "mapAttrs",
B::RemoveAttrs => "removeAttrs",
B::GenList => "genList",
B::Seq => "seq",
B::DeepSeq => "deepSeq",
B::ConcatStringsSep => "concatStringsSep",
B::Split => "split",
B::Foldl => "foldl'",
B::Substring => "substring",
B::ReplaceStrings => "replaceStrings",
}
}
#[must_use]
pub fn display_name(self, captured: usize) -> &'static str {
use IrBuiltin as B;
if captured == 0 {
return self.registry_name();
}
match (self, captured) {
(B::Map, _) => "map<partial>",
(B::Filter, _) => "filter<partial>",
(B::ElemAt, _) => "elemAt<partial>",
(B::HasAttr, _) => "hasAttr<partial>",
(B::GetAttr, _) => "getAttr<partial>",
(B::IntersectAttrs, _) => "intersectAttrs<partial>",
(B::MapAttrs, _) => "mapAttrs<partial>",
(B::RemoveAttrs, _) => "removeAttrs<partial>",
(B::GenList, _) => "genList<partial>",
(B::Seq, _) => "seq<partial>",
(B::DeepSeq, _) => "deepSeq<partial>",
(B::ConcatStringsSep, _) => "concatStringsSep<partial>",
(B::Split, _) => "curried<partial>",
(
B::Add
| B::Sub
| B::Mul
| B::Div
| B::LessThan
| B::BitAnd
| B::BitOr
| B::BitXor
| B::Match
| B::FindFile
| B::HashString,
_,
) => "curried<partial>",
(B::Elem, _) => "elem<partial>",
(B::Sort, _) => "sort<partial>",
(B::All, _) => "all<partial>",
(B::Any, _) => "any<partial>",
(B::Partition, _) => "partition<partial>",
(B::GroupBy, _) => "groupBy<partial>",
(B::ConcatMap, _) => "concatMap<partial>",
(B::CatAttrs, _) => "catAttrs<partial>",
(B::ZipAttrsWith, _) => "zipAttrsWith<partial>",
(B::FilterAttrs, _) => "filterAttrs<partial>",
(B::CompareVersions, _) => "compareVersions<partial>",
(B::HasPrefix, _) => "hasPrefix<partial>",
(B::HasSuffix, _) => "hasSuffix<partial>",
(B::Trace, _) => "trace<partial>",
(B::TraceVerbose, _) => "traceVerbose<partial>",
(B::Foldl, 1) => "foldl'<p1>",
(B::Foldl, _) => "foldl'<p2>",
(B::Substring, 1) => "substring<p1>",
(B::Substring, _) => "substring<p2>",
(B::ReplaceStrings, 1) => "replaceStrings<p1>",
(B::ReplaceStrings, _) => "replaceStrings<p2>",
_ => self.registry_name(),
}
}
#[must_use]
pub fn wants_unforced_arg(self, captured: usize) -> bool {
(matches!(self, IrBuiltin::Seq | IrBuiltin::DeepSeq) && captured == 1)
|| (matches!(self, IrBuiltin::TryEval) && captured == 0)
}
}
const ALL_IMPLEMENTED: &[IrBuiltin] = &[
IrBuiltin::ToString,
IrBuiltin::TypeOf,
IrBuiltin::IsNull,
IrBuiltin::IsInt,
IrBuiltin::IsFloat,
IrBuiltin::IsBool,
IrBuiltin::IsString,
IrBuiltin::IsList,
IrBuiltin::IsAttrs,
IrBuiltin::IsFunction,
IrBuiltin::IsPath,
IrBuiltin::Length,
IrBuiltin::Head,
IrBuiltin::Tail,
IrBuiltin::AttrNames,
IrBuiltin::AttrValues,
IrBuiltin::ConcatLists,
IrBuiltin::ListToAttrs,
IrBuiltin::StringLength,
IrBuiltin::Import,
IrBuiltin::Throw,
IrBuiltin::Abort,
IrBuiltin::TryEval,
IrBuiltin::Ceil,
IrBuiltin::Floor,
IrBuiltin::ToJson,
IrBuiltin::FromJson,
IrBuiltin::ToXml,
IrBuiltin::FunctionArgs,
IrBuiltin::GenericClosure,
IrBuiltin::SplitVersion,
IrBuiltin::ParseDrvName,
IrBuiltin::ConcatStrings,
IrBuiltin::HasContext,
IrBuiltin::GetContext,
IrBuiltin::UnsafeDiscardStringContext,
IrBuiltin::ToLower,
IrBuiltin::ToUpper,
IrBuiltin::BaseNameOf,
IrBuiltin::DirOf,
IrBuiltin::Derivation,
IrBuiltin::DerivationStrict,
IrBuiltin::ConvertHash,
IrBuiltin::HashString,
IrBuiltin::PathExists,
IrBuiltin::ReadFile,
IrBuiltin::ReadDir,
IrBuiltin::ReadFileType,
IrBuiltin::GetEnv,
IrBuiltin::Placeholder,
IrBuiltin::AddErrorContext,
IrBuiltin::UnsafeGetAttrPos,
IrBuiltin::Map,
IrBuiltin::Filter,
IrBuiltin::ElemAt,
IrBuiltin::HasAttr,
IrBuiltin::GetAttr,
IrBuiltin::IntersectAttrs,
IrBuiltin::MapAttrs,
IrBuiltin::RemoveAttrs,
IrBuiltin::GenList,
IrBuiltin::Seq,
IrBuiltin::DeepSeq,
IrBuiltin::ConcatStringsSep,
IrBuiltin::Split,
IrBuiltin::Add,
IrBuiltin::Sub,
IrBuiltin::Mul,
IrBuiltin::Div,
IrBuiltin::LessThan,
IrBuiltin::BitAnd,
IrBuiltin::BitOr,
IrBuiltin::BitXor,
IrBuiltin::Elem,
IrBuiltin::Sort,
IrBuiltin::All,
IrBuiltin::Any,
IrBuiltin::Partition,
IrBuiltin::GroupBy,
IrBuiltin::ConcatMap,
IrBuiltin::CatAttrs,
IrBuiltin::ZipAttrsWith,
IrBuiltin::FilterAttrs,
IrBuiltin::CompareVersions,
IrBuiltin::Match,
IrBuiltin::HasPrefix,
IrBuiltin::HasSuffix,
IrBuiltin::FindFile,
IrBuiltin::Trace,
IrBuiltin::TraceVerbose,
IrBuiltin::Foldl,
IrBuiltin::Substring,
IrBuiltin::ReplaceStrings,
];
const MISSING_BUILTIN_NAMES: &[&str] = &[
"addDrvOutputDependencies",
"appendContext",
"break",
"currentTime",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fetchTree",
"fetchurl",
"filterSource",
"flakeRefToString",
"fromTOML",
"getFlake",
"hashFile",
"parseFlakeRef",
"path",
"resolveFlakeRef",
"scopedImport",
"storePath",
"sui",
"toFile",
"toPath",
"unsafeDiscardOutputDependency",
"warn",
];
const DEFAULT_SCOPE: &[&str] = &[
"abort",
"baseNameOf",
"derivation",
"derivationStrict",
"dirOf",
"false",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fetchTree",
"fromTOML",
"import",
"isNull",
"map",
"null",
"placeholder",
"removeAttrs",
"scopedImport",
"throw",
"toString",
"true",
];
#[must_use]
pub fn current_system() -> &'static str {
if cfg!(target_os = "macos") {
if cfg!(target_arch = "aarch64") {
"aarch64-darwin"
} else {
"x86_64-darwin"
}
} else if cfg!(target_arch = "aarch64") {
"aarch64-linux"
} else {
"x86_64-linux"
}
}
#[must_use]
fn nix_path_value() -> IrValue {
let raw = std::env::var("NIX_PATH").unwrap_or_default();
let list: Vec<IrValue> = crate::path::parse_nix_path(&raw)
.into_iter()
.map(|(prefix, path)| {
let mut a = IrAttrs::new();
a.insert("prefix".to_string(), IrValue::string(prefix));
a.insert("path".to_string(), IrValue::string(path));
IrValue::Attrs(Rc::new(a))
})
.collect();
IrValue::List(Rc::new(list))
}
#[must_use]
pub fn builtins_attrs() -> Rc<IrAttrs> {
let mut set = IrAttrs::new();
for kind in ALL_IMPLEMENTED {
set.insert(
kind.registry_name().to_string(),
IrValue::Builtin(*kind, Rc::new(Vec::new())),
);
}
set.insert("storeDir".to_string(), IrValue::string("/nix/store"));
set.insert("nixVersion".to_string(), IrValue::string("2.34.7"));
set.insert(
"currentSystem".to_string(),
IrValue::string(current_system()),
);
set.insert("langVersion".to_string(), IrValue::Int(6));
set.insert("true".to_string(), IrValue::Bool(true));
set.insert("false".to_string(), IrValue::Bool(false));
set.insert("null".to_string(), IrValue::Null);
set.insert("nixPath".to_string(), nix_path_value());
for name in MISSING_BUILTIN_NAMES {
set.insert(
(*name).to_string(),
IrValue::Thunk(IrThunk::failed(IrEvalError::MissingBuiltin(
(*name).to_string(),
))),
);
}
let snapshot = IrValue::Attrs(Rc::new(set.clone()));
set.insert("builtins".to_string(), snapshot);
Rc::new(set)
}
#[must_use]
pub fn base_env() -> IrEnv {
let attrs = builtins_attrs();
let mut env = IrEnv::new();
for name in DEFAULT_SCOPE {
if let Some(v) = attrs.get(*name) {
env.bind(name, v.clone());
}
}
env.bind("builtins", IrValue::Attrs(attrs));
env
}
fn as_list(v: &IrValue) -> Result<&Rc<Vec<IrValue>>, IrEvalError> {
match v {
IrValue::List(items) => Ok(items),
other => Err(IrEvalError::TypeMismatch {
expected: "list",
got: other.type_name(),
}),
}
}
fn as_attrs(v: &IrValue) -> Result<&Rc<IrAttrs>, IrEvalError> {
match v {
IrValue::Attrs(attrs) => Ok(attrs),
other => Err(IrEvalError::TypeMismatch {
expected: "set",
got: other.type_name(),
}),
}
}
fn as_str(v: &IrValue) -> Result<&str, IrEvalError> {
match v {
IrValue::Str(s, _) => Ok(s),
other => Err(IrEvalError::TypeMismatch {
expected: "string",
got: other.type_name(),
}),
}
}
fn as_path_string(v: &IrValue) -> Result<String, IrEvalError> {
match v {
IrValue::Path(p) => Ok((**p).clone()),
IrValue::Str(s, _) => Ok((**s).clone()),
other => Err(IrEvalError::TypeMismatch {
expected: "path",
got: other.type_name(),
}),
}
}
fn as_int(v: &IrValue) -> Result<i64, IrEvalError> {
match v {
IrValue::Int(n) => Ok(*n),
other => Err(IrEvalError::TypeMismatch {
expected: "int",
got: other.type_name(),
}),
}
}
fn force_str(v: &IrValue) -> Result<String, IrEvalError> {
let forced = v.force()?;
as_str(&forced).map(ToOwned::to_owned)
}
fn deep_force(v: &IrValue) -> Result<(), IrEvalError> {
let forced = v.force()?;
match &forced {
IrValue::Attrs(attrs) => {
for value in attrs.values() {
deep_force(value)?;
}
}
IrValue::List(items) => {
for item in items.iter() {
deep_force(item)?;
}
}
_ => {}
}
Ok(())
}
pub fn apply_builtin(
kind: IrBuiltin,
captured: &[IrValue],
arg: IrValue,
) -> Result<IrValue, IrEvalError> {
let stage = captured.len();
if stage + 1 < kind.arity() {
capture_check(kind, stage, &arg)?;
let mut caps = Vec::with_capacity(stage + 1);
caps.extend_from_slice(captured);
caps.push(arg);
return Ok(IrValue::Builtin(kind, Rc::new(caps)));
}
run_saturated(kind, captured, arg)
}
fn capture_check(kind: IrBuiltin, stage: usize, arg: &IrValue) -> Result<(), IrEvalError> {
use IrBuiltin as B;
match (kind, stage) {
(B::ElemAt, 0) => as_list(arg).map(|_| ()),
(
B::HasAttr
| B::GetAttr
| B::ConcatStringsSep
| B::CatAttrs
| B::CompareVersions
| B::HasPrefix
| B::HasSuffix,
0,
) => as_str(arg).map(|_| ()),
(B::IntersectAttrs | B::RemoveAttrs, 0) => as_attrs(arg).map(|_| ()),
(B::Substring, 0 | 1) => as_int(arg).map(|_| ()),
(B::DeepSeq, 0) => deep_force(arg),
(B::ReplaceStrings, 0) => {
for item in as_list(arg)?.iter() {
force_str(item)?;
}
Ok(())
}
(B::ReplaceStrings, 1) => {
for item in as_list(arg)?.iter() {
coerce_to_string_plain(&item.force()?)?;
}
Ok(())
}
_ => Ok(()),
}
}
#[allow(clippy::too_many_lines)]
fn run_saturated(
kind: IrBuiltin,
captured: &[IrValue],
arg: IrValue,
) -> Result<IrValue, IrEvalError> {
use IrBuiltin as B;
match kind {
B::ToString => Ok(IrValue::string(coerce_to_string_plain(&arg)?)),
B::TypeOf => Ok(IrValue::string(arg.type_name())),
B::IsNull => Ok(IrValue::Bool(matches!(arg, IrValue::Null))),
B::IsInt => Ok(IrValue::Bool(matches!(arg, IrValue::Int(_)))),
B::IsFloat => Ok(IrValue::Bool(matches!(arg, IrValue::Float(_)))),
B::IsBool => Ok(IrValue::Bool(matches!(arg, IrValue::Bool(_)))),
B::IsString => Ok(IrValue::Bool(matches!(arg, IrValue::Str(..)))),
B::IsList => Ok(IrValue::Bool(matches!(arg, IrValue::List(_)))),
B::IsAttrs => Ok(IrValue::Bool(matches!(arg, IrValue::Attrs(_)))),
B::IsFunction => Ok(IrValue::Bool(matches!(
arg,
IrValue::Lambda(_) | IrValue::Builtin(..)
))),
B::IsPath => Ok(IrValue::Bool(matches!(arg, IrValue::Path(_)))),
B::Length => Ok(IrValue::Int(as_list(&arg)?.len() as i64)),
B::Head => as_list(&arg)?
.first()
.cloned()
.ok_or_else(|| IrEvalError::TypeError("head: empty list".to_string())),
B::Tail => {
let list = as_list(&arg)?;
if list.is_empty() {
return Err(IrEvalError::TypeError("tail: empty list".to_string()));
}
Ok(IrValue::List(Rc::new(list[1..].to_vec())))
}
B::AttrNames => Ok(IrValue::List(Rc::new(
as_attrs(&arg)?
.keys()
.map(|k| IrValue::string(k.clone()))
.collect(),
))),
B::AttrValues => Ok(IrValue::List(Rc::new(
as_attrs(&arg)?.values().cloned().collect(),
))),
B::ConcatLists => {
let outer = as_list(&arg)?;
let mut result = Vec::new();
for item in outer.iter() {
let forced = item.force()?;
result.extend(as_list(&forced)?.iter().cloned());
}
Ok(IrValue::List(Rc::new(result)))
}
B::ListToAttrs => {
let list = as_list(&arg)?;
let mut attrs = IrAttrs::new();
for item in list.iter() {
let forced = item.force()?;
let item_attrs = as_attrs(&forced)?;
let name_value = item_attrs
.get("name")
.ok_or_else(|| IrEvalError::AttrNotFound("name".to_string()))?;
let name = force_str(name_value)?;
let value = item_attrs
.get("value")
.ok_or_else(|| IrEvalError::AttrNotFound("value".to_string()))?
.clone();
attrs.entry(name).or_insert(value);
}
Ok(IrValue::Attrs(Rc::new(attrs)))
}
B::StringLength => Ok(IrValue::Int(as_str(&arg)?.len() as i64)),
B::Import => {
let raw = coerce_import_path(&arg)?;
file_eval::import(&raw)
}
B::Map => {
let func = captured[0].clone();
let list = as_list(&arg)?;
Ok(IrValue::List(Rc::new(
list.iter()
.map(|v| {
IrValue::Thunk(IrThunk::native_apply(func.clone(), vec![v.clone()]))
})
.collect(),
)))
}
B::Filter => {
let pred = captured[0].clone();
let list = as_list(&arg)?;
let mut result = Vec::new();
for v in list.iter() {
if apply(pred.clone(), v.clone())?.force()?.as_bool()? {
result.push(v.clone());
}
}
Ok(IrValue::List(Rc::new(result)))
}
B::ElemAt => {
let list = as_list(&captured[0])?;
let idx = as_int(&arg)?;
usize::try_from(idx)
.ok()
.and_then(|i| list.get(i))
.cloned()
.ok_or_else(|| {
IrEvalError::TypeError(elem_at_oob(idx))
})
}
B::HasAttr => {
let name = as_str(&captured[0])?;
Ok(IrValue::Bool(as_attrs(&arg)?.contains_key(name)))
}
B::GetAttr => {
let name = as_str(&captured[0])?;
as_attrs(&arg)?
.get(name)
.cloned()
.ok_or_else(|| IrEvalError::AttrNotFound(name.to_string()))
}
B::IntersectAttrs => {
let a = as_attrs(&captured[0])?;
let b = as_attrs(&arg)?;
let mut result = IrAttrs::new();
for (k, v) in b.iter() {
if a.contains_key(k) {
result.insert(k.clone(), v.clone());
}
}
Ok(IrValue::Attrs(Rc::new(result)))
}
B::MapAttrs => {
let func = captured[0].clone();
let attrs = as_attrs(&arg)?;
let mut result = IrAttrs::new();
for (k, v) in attrs.iter() {
result.insert(
k.clone(),
IrValue::Thunk(IrThunk::native_apply(
func.clone(),
vec![IrValue::string(k.clone()), v.clone()],
)),
);
}
Ok(IrValue::Attrs(Rc::new(result)))
}
B::RemoveAttrs => {
let mut result = (**as_attrs(&captured[0])?).clone();
for name in as_list(&arg)?.iter() {
if let Ok(s) = force_str(name) {
result.remove(&s);
}
}
Ok(IrValue::Attrs(Rc::new(result)))
}
B::GenList => {
let func = captured[0].clone();
let n = as_int(&arg)?;
if n < 0 {
return Err(IrEvalError::TypeError(genlist_negative(n)));
}
let mut result = Vec::with_capacity(usize::try_from(n).unwrap_or(0));
for i in 0..n {
result.push(apply(func.clone(), IrValue::Int(i))?);
}
Ok(IrValue::List(Rc::new(result)))
}
B::Seq | B::DeepSeq => Ok(arg),
B::ConcatStringsSep => {
let sep = as_str(&captured[0])?;
let list = as_list(&arg)?;
let mut result = String::new();
let mut ctx = crate::eval_ir::IrStringContext::new();
for (i, v) in list.iter().enumerate() {
if i > 0 {
result.push_str(sep);
}
let (s, c) = crate::eval_ir::coerce_to_string_ctx(&v.force()?, false)?;
result.push_str(&s);
ctx.merge(&c);
}
Ok(IrValue::string_with_context(result, ctx))
}
B::Split => {
let pattern = as_str(&captured[0])?;
let input = as_str(&arg)?;
split_impl(pattern, input)
}
B::Foldl => {
let func = captured[0].clone();
let mut acc = captured[1].clone();
for v in as_list(&arg)?.iter() {
let partial = apply(func.clone(), acc)?;
acc = apply(partial, v.clone())?.force()?;
}
Ok(acc)
}
B::Substring => {
let start_i = as_int(&captured[0])?;
let len_i = as_int(&captured[1])?;
let s = as_str(&arg)?;
if start_i < 0 {
return Err(IrEvalError::TypeError(
"substring: negative start position".to_string(),
));
}
let s_len = s.len();
let start = usize::try_from(start_i).unwrap_or(usize::MAX).min(s_len);
let end = if len_i < 0 {
s_len
} else {
start
.saturating_add(usize::try_from(len_i).unwrap_or(usize::MAX))
.min(s_len)
};
Ok(IrValue::string(s[start..end].to_string()))
}
B::ReplaceStrings => {
let from: Vec<String> = as_list(&captured[0])?
.iter()
.map(force_str)
.collect::<Result<_, _>>()?;
let to: Vec<String> = as_list(&captured[1])?
.iter()
.map(|v| coerce_to_string_plain(&v.force()?))
.collect::<Result<_, _>>()?;
let subject = coerce_to_string_plain(&arg)?;
Ok(IrValue::string(replace_strings_impl(&from, &to, &subject)))
}
B::Throw => {
let msg = as_str(&arg)?;
Err(IrEvalError::Throw(format!("throw: {msg}")))
}
B::Abort => {
let msg = as_str(&arg)?;
Err(IrEvalError::Abort(format!(
"evaluation aborted with the following error message: '{msg}'"
)))
}
B::TryEval => {
match arg.force() {
Ok(v) => Ok(try_eval_result(true, v)),
Err(IrEvalError::Throw(_) | IrEvalError::AssertionFailed) => {
Ok(try_eval_result(false, IrValue::Bool(false)))
}
Err(e) => Err(e),
}
}
B::Trace | B::TraceVerbose => Ok(arg),
B::Add => numeric_binop(&captured[0], &arg, |a, b| a + b, |a, b| a + b, "add"),
B::Sub => numeric_binop(&captured[0], &arg, |a, b| a - b, |a, b| a - b, "sub"),
B::Mul => numeric_binop(&captured[0], &arg, |a, b| a * b, |a, b| a * b, "mul"),
B::Div => div_impl(&captured[0], &arg),
B::LessThan => less_than(&captured[0], &arg),
B::BitAnd => Ok(IrValue::Int(as_int(&captured[0])? & as_int(&arg)?)),
B::BitOr => Ok(IrValue::Int(as_int(&captured[0])? | as_int(&arg)?)),
B::BitXor => Ok(IrValue::Int(as_int(&captured[0])? ^ as_int(&arg)?)),
B::Ceil => Ok(IrValue::Int(to_float(&arg)?.ceil() as i64)),
B::Floor => Ok(IrValue::Int(to_float(&arg)?.floor() as i64)),
B::Elem => {
let needle = &captured[0];
let list = as_list(&arg)?;
Ok(IrValue::Bool(list.iter().any(|v| ir_eq(needle, v))))
}
B::Sort => sort_impl(&captured[0], &arg),
B::All => {
let pred = captured[0].clone();
for v in as_list(&arg)?.iter() {
if !apply(pred.clone(), v.clone())?.force()?.as_bool()? {
return Ok(IrValue::Bool(false));
}
}
Ok(IrValue::Bool(true))
}
B::Any => {
let pred = captured[0].clone();
for v in as_list(&arg)?.iter() {
if apply(pred.clone(), v.clone())?.force()?.as_bool()? {
return Ok(IrValue::Bool(true));
}
}
Ok(IrValue::Bool(false))
}
B::Partition => {
let pred = captured[0].clone();
let mut right = Vec::new();
let mut wrong = Vec::new();
for v in as_list(&arg)?.iter() {
if apply(pred.clone(), v.clone())?.force()?.as_bool()? {
right.push(v.clone());
} else {
wrong.push(v.clone());
}
}
let mut result = IrAttrs::new();
result.insert("right".to_string(), IrValue::List(Rc::new(right)));
result.insert("wrong".to_string(), IrValue::List(Rc::new(wrong)));
Ok(IrValue::Attrs(Rc::new(result)))
}
B::GroupBy => {
let func = captured[0].clone();
let mut groups: std::collections::BTreeMap<String, Vec<IrValue>> =
std::collections::BTreeMap::new();
for v in as_list(&arg)?.iter() {
let key = apply(func.clone(), v.clone())?.force()?;
let key_str = as_str(&key)?.to_string();
groups.entry(key_str).or_default().push(v.clone());
}
let mut result = IrAttrs::new();
for (k, vs) in groups {
result.insert(k, IrValue::List(Rc::new(vs)));
}
Ok(IrValue::Attrs(Rc::new(result)))
}
B::ConcatMap => {
let func = captured[0].clone();
let mut result = Vec::new();
for v in as_list(&arg)?.iter() {
let mapped = apply(func.clone(), v.clone())?.force()?;
result.extend(as_list(&mapped)?.iter().cloned());
}
Ok(IrValue::List(Rc::new(result)))
}
B::CatAttrs => {
let name = as_str(&captured[0])?;
let mut result = Vec::new();
for item in as_list(&arg)?.iter() {
if let Ok(forced) = item.force() {
if let IrValue::Attrs(a) = &forced {
if let Some(v) = a.get(name) {
result.push(v.clone());
}
}
}
}
Ok(IrValue::List(Rc::new(result)))
}
B::ZipAttrsWith => {
let func = captured[0].clone();
let mut collected: std::collections::BTreeMap<String, Vec<IrValue>> =
std::collections::BTreeMap::new();
for item in as_list(&arg)?.iter() {
let forced = item.force()?;
for (k, v) in as_attrs(&forced)?.iter() {
collected.entry(k.clone()).or_default().push(v.clone());
}
}
let mut result = IrAttrs::new();
for (k, vs) in collected {
let thunk = IrThunk::native_apply(
func.clone(),
vec![IrValue::string(k.clone()), IrValue::List(Rc::new(vs))],
);
result.insert(k, IrValue::Thunk(thunk));
}
Ok(IrValue::Attrs(Rc::new(result)))
}
B::FilterAttrs => {
let pred = captured[0].clone();
let attrs = as_attrs(&arg)?;
let mut result = IrAttrs::new();
for (k, v) in attrs.iter() {
let partial = apply(pred.clone(), IrValue::string(k.clone()))?;
if apply(partial, v.clone())?.force()?.as_bool()? {
result.insert(k.clone(), v.clone());
}
}
Ok(IrValue::Attrs(Rc::new(result)))
}
B::FunctionArgs => function_args(&arg),
B::GenericClosure => generic_closure(&arg),
B::ConcatStrings => {
let list = as_list(&arg)?;
let mut result = String::new();
let mut ctx = crate::eval_ir::IrStringContext::new();
for v in list.iter() {
let (s, c) = crate::eval_ir::coerce_to_string_ctx(v, false)?;
result.push_str(&s);
ctx.merge(&c);
}
Ok(IrValue::string_with_context(result, ctx))
}
B::ToLower => Ok(IrValue::string(as_str(&arg)?.to_lowercase())),
B::ToUpper => Ok(IrValue::string(as_str(&arg)?.to_uppercase())),
B::HasPrefix => Ok(IrValue::Bool(as_str(&arg)?.starts_with(as_str(&captured[0])?))),
B::HasSuffix => Ok(IrValue::Bool(as_str(&arg)?.ends_with(as_str(&captured[0])?))),
B::Match => match_impl(as_str(&captured[0])?, as_str(&arg)?),
B::CompareVersions => {
let a = as_str(&captured[0])?;
let b = as_str(&arg)?;
Ok(IrValue::Int(compare_versions(a, b)))
}
B::SplitVersion => {
let s = as_str(&arg)?;
Ok(IrValue::List(Rc::new(
split_version(s).into_iter().map(IrValue::string).collect(),
)))
}
B::ParseDrvName => {
let s = as_str(&arg)?;
let (name, version) = parse_drv_name(s);
let mut result = IrAttrs::new();
result.insert("name".to_string(), IrValue::string(name));
result.insert("version".to_string(), IrValue::string(version));
Ok(IrValue::Attrs(Rc::new(result)))
}
B::HasContext => match &arg {
IrValue::Str(_, c) => Ok(IrValue::Bool(c.is_some())),
other => Err(IrEvalError::TypeError(format!(
"hasContext: expected string, got {}",
other.type_name()
))),
},
B::GetContext => match &arg {
IrValue::Str(_, c) => {
use std::collections::{BTreeMap, BTreeSet};
let mut plains: BTreeSet<String> = BTreeSet::new();
let mut om: BTreeMap<String, Vec<String>> = BTreeMap::new();
let mut deep: BTreeSet<String> = BTreeSet::new();
if let Some(c) = c {
for elem in c.iter() {
match elem {
IrContextElem::Plain(p) => {
plains.insert(p.clone());
}
IrContextElem::Output { drv, output } => {
om.entry(drv.clone()).or_default().push(output.clone());
}
IrContextElem::DrvDeep(d) => {
deep.insert(d.clone());
}
}
}
}
let mut result = IrAttrs::new();
for p in &plains {
let mut a = IrAttrs::new();
a.insert("path".to_string(), IrValue::Bool(true));
result.insert(p.clone(), IrValue::Attrs(Rc::new(a)));
}
for (d, os) in &om {
let mut a = IrAttrs::new();
a.insert(
"outputs".to_string(),
IrValue::List(Rc::new(
os.iter().map(|o| IrValue::string(o.clone())).collect(),
)),
);
result.insert(d.clone(), IrValue::Attrs(Rc::new(a)));
}
for d in &deep {
let mut a = IrAttrs::new();
a.insert("allOutputs".to_string(), IrValue::Bool(true));
result.insert(d.clone(), IrValue::Attrs(Rc::new(a)));
}
Ok(IrValue::Attrs(Rc::new(result)))
}
other => Err(IrEvalError::TypeError(format!(
"getContext: expected string, got {}",
other.type_name()
))),
},
B::UnsafeDiscardStringContext => match &arg {
IrValue::Str(s, _) => Ok(IrValue::Str(s.clone(), None)),
other => Err(IrEvalError::TypeError(format!(
"unsafeDiscardStringContext: expected string, got {}",
other.type_name()
))),
},
B::Derivation | B::DerivationStrict => crate::derivation::build_derivation(&arg),
B::HashString => hash_string(as_str(&captured[0])?, as_str(&arg)?),
B::ConvertHash => convert_hash(as_attrs(&arg)?),
B::BaseNameOf => match &arg {
IrValue::Str(s, _) => Ok(IrValue::string(base_name_of(s))),
IrValue::Path(p) => Ok(IrValue::string(base_name_of(p))),
other => Err(IrEvalError::TypeError(format!(
"baseNameOf: expected string or path, got {}",
other.type_name()
))),
},
B::DirOf => match &arg {
IrValue::Str(s, _) => Ok(IrValue::string(dir_of(s))),
IrValue::Path(p) => Ok(IrValue::Path(Rc::new(dir_of(p)))),
other => Err(IrEvalError::TypeError(format!(
"dirOf: expected string or path, got {}",
other.type_name()
))),
},
B::FindFile => find_file_impl(as_list(&captured[0])?, as_str(&arg)?),
B::PathExists => {
let p = as_path_string(&arg)?;
Ok(IrValue::Bool(std::path::Path::new(&p).exists()))
}
B::ReadFile => {
let p = as_path_string(&arg)?;
let contents = std::fs::read_to_string(&p).map_err(|e| IrEvalError::Io {
context: "readFile".to_string(),
message: e.to_string(),
})?;
Ok(IrValue::string(contents))
}
B::ReadFileType => {
let p = as_path_string(&arg)?;
match std::fs::symlink_metadata(&p) {
Ok(meta) => {
let kind = if meta.is_symlink() {
"symlink"
} else if meta.is_dir() {
"directory"
} else if meta.is_file() {
"regular"
} else {
"unknown"
};
Ok(IrValue::string(kind))
}
Err(e) => Err(IrEvalError::Io {
context: "readFileType".to_string(),
message: e.to_string(),
}),
}
}
B::ReadDir => {
let p = as_path_string(&arg)?;
let mut attrs = IrAttrs::new();
for entry in std::fs::read_dir(&p).map_err(|e| IrEvalError::Io {
context: "readDir".to_string(),
message: e.to_string(),
})? {
let entry = entry.map_err(|e| IrEvalError::Io {
context: "readDir".to_string(),
message: e.to_string(),
})?;
let name = entry.file_name().to_string_lossy().to_string();
let ft = entry.file_type().map_err(|e| IrEvalError::Io {
context: "readDir".to_string(),
message: e.to_string(),
})?;
let type_str = if ft.is_dir() {
"directory"
} else if ft.is_symlink() {
"symlink"
} else {
"regular"
};
attrs.insert(name, IrValue::string(type_str));
}
Ok(IrValue::Attrs(Rc::new(attrs)))
}
B::GetEnv => {
let name = as_str(&arg)?;
Ok(IrValue::string(std::env::var(name).unwrap_or_default()))
}
B::Placeholder => {
let output = as_str(&arg)?;
use sha2::{Digest, Sha256};
let hash = Sha256::digest(format!("nix-output:{output}").as_bytes());
Ok(IrValue::string(format!(
"/{}",
sui_compat::store_path::nix_base32_encode(hash.as_slice())
)))
}
B::AddErrorContext => Ok(arg),
B::UnsafeGetAttrPos => Ok(IrValue::Null),
B::ToJson => {
let json = ir_to_json(&arg)?;
let s = serde_json::to_string(&json).unwrap_or_else(|_| "null".to_string());
Ok(IrValue::string(s))
}
B::FromJson => {
let s = as_str(&arg)?;
let json: serde_json::Value = serde_json::from_str(s)
.map_err(|e| IrEvalError::TypeError(format!("fromJSON: {e}")))?;
Ok(json_to_ir(&json))
}
B::ToXml => Ok(IrValue::string(to_xml(&arg))),
}
}
fn elem_at_oob(idx: i64) -> String {
let mut s = String::from("elemAt: index ");
s.push_str(&idx.to_string());
s.push_str(" out of bounds");
s
}
fn genlist_negative(n: i64) -> String {
let mut s = String::from("genList: negative list length ");
s.push_str(&n.to_string());
s
}
fn hash_string(algo: &str, input: &str) -> Result<IrValue, IrEvalError> {
let hex = match algo {
"md5" => {
use md5::{Digest, Md5};
format!("{:x}", Md5::digest(input.as_bytes()))
}
"sha1" => {
use sha1::{Digest, Sha1};
format!("{:x}", Sha1::digest(input.as_bytes()))
}
"sha256" => {
use sha2::{Digest, Sha256};
format!("{:x}", Sha256::digest(input.as_bytes()))
}
"sha512" => {
use sha2::{Digest, Sha512};
format!("{:x}", Sha512::digest(input.as_bytes()))
}
other => {
return Err(IrEvalError::TypeError(format!(
"hashString: unsupported algorithm: {other}"
)))
}
};
Ok(IrValue::string(hex))
}
fn convert_hash(attrs: &IrAttrs) -> Result<IrValue, IrEvalError> {
let hash_str = as_str(&attr_force(attrs, "hash")?)?.to_string();
let to_format = as_str(&attr_force(attrs, "toHashFormat")?)?.to_string();
let (algo, raw_hash): (String, String) = if let Some(av) = attrs.get("hashAlgo") {
(as_str(&av.force()?)?.to_string(), hash_str.clone())
} else if let Some(stripped) = hash_str.strip_prefix("sha256-") {
("sha256".to_string(), stripped.to_string())
} else if let Some(stripped) = hash_str.strip_prefix("sha512-") {
("sha512".to_string(), stripped.to_string())
} else {
return Err(IrEvalError::TypeError("convertHash: missing hashAlgo".into()));
};
let expected_len = match algo.as_str() {
"md5" => 16,
"sha1" => 20,
"sha256" => 32,
"sha512" => 64,
other => {
return Err(IrEvalError::TypeError(format!(
"convertHash: unsupported algo {other}"
)))
}
};
let bytes: Vec<u8> = if raw_hash.len() == expected_len * 2
&& raw_hash.chars().all(|c| c.is_ascii_hexdigit())
{
(0..raw_hash.len())
.step_by(2)
.map(|i| u8::from_str_radix(&raw_hash[i..i + 2], 16))
.collect::<Result<Vec<_>, _>>()
.map_err(|e| IrEvalError::TypeError(format!("convertHash hex: {e}")))?
} else if let Ok(b) = sui_compat::store_path::nix_base32_decode(&raw_hash) {
if expected_len != 20 {
return Err(IrEvalError::TypeError(
"convertHash: nix32 only supported for 20-byte (sha1) hashes".into(),
));
}
b.to_vec()
} else if let Ok(b) = sui_compat::hash::base64_decode(&raw_hash) {
b
} else {
return Err(IrEvalError::TypeError(format!(
"convertHash: cannot decode hash '{raw_hash}'"
)));
};
if bytes.len() != expected_len {
return Err(IrEvalError::TypeError(format!(
"convertHash: decoded {} bytes, expected {expected_len} for {algo}",
bytes.len()
)));
}
let out = match to_format.as_str() {
"base16" => {
let mut s = String::with_capacity(bytes.len() * 2);
for b in &bytes {
s.push_str(&format!("{b:02x}"));
}
s
}
"nix32" => {
if expected_len != 20 {
return Err(IrEvalError::TypeError(
"convertHash: nix32 output only supported for 20-byte hashes".into(),
));
}
sui_compat::store_path::nix_base32_encode(&bytes)
}
"base64" => sui_compat::hash::base64_encode(&bytes),
"sri" => format!("{algo}-{}", sui_compat::hash::base64_encode(&bytes)),
other => {
return Err(IrEvalError::TypeError(format!(
"convertHash: unsupported toHashFormat {other}"
)))
}
};
Ok(IrValue::string(out))
}
fn attr_force(attrs: &IrAttrs, key: &str) -> Result<IrValue, IrEvalError> {
attrs
.get(key)
.ok_or_else(|| IrEvalError::AttrNotFound(key.to_string()))?
.force()
}
fn coerce_import_path(v: &IrValue) -> Result<String, IrEvalError> {
match v {
IrValue::Path(p) => Ok((**p).clone()),
IrValue::Str(s, _) => Ok((**s).clone()),
IrValue::Attrs(attrs) => match attrs.get("outPath") {
Some(out) => coerce_import_path(&out.force()?),
None => Err(IrEvalError::TypeError(
"import: expected path or string, got set without outPath".to_string(),
)),
},
other => {
let mut msg = String::from("import: expected path or string, got ");
msg.push_str(other.type_name());
Err(IrEvalError::TypeError(msg))
}
}
}
fn cached_regex(pattern: &str) -> Result<regex::Regex, IrEvalError> {
use std::cell::RefCell;
use std::collections::HashMap;
thread_local! {
static REGEX_CACHE: RefCell<HashMap<String, regex::Regex>> =
RefCell::new(HashMap::new());
}
REGEX_CACHE.with(|cache| {
let mut cache = cache.borrow_mut();
if let Some(re) = cache.get(pattern) {
return Ok(re.clone());
}
let re = regex::Regex::new(pattern).map_err(|e| {
let mut msg = String::from("invalid regex '");
msg.push_str(pattern);
msg.push_str("': ");
msg.push_str(&e.to_string());
IrEvalError::TypeError(msg)
})?;
cache.insert(pattern.to_string(), re.clone());
Ok(re)
})
}
fn split_impl(pattern: &str, input: &str) -> Result<IrValue, IrEvalError> {
let re = cached_regex(pattern)?;
let mut result: Vec<IrValue> = Vec::new();
let mut last_end = 0;
for m in re.find_iter(input) {
result.push(IrValue::string(input[last_end..m.start()].to_string()));
if let Some(caps) = re.captures(&input[m.start()..]) {
let groups: Vec<IrValue> = (1..caps.len())
.map(|i| match caps.get(i) {
Some(g) => IrValue::string(g.as_str().to_string()),
None => IrValue::Null,
})
.collect();
result.push(IrValue::List(Rc::new(groups)));
}
last_end = m.end();
}
result.push(IrValue::string(input[last_end..].to_string()));
Ok(IrValue::List(Rc::new(result)))
}
fn replace_strings_impl(from: &[String], to: &[String], subject: &str) -> String {
let bytes = subject.as_bytes();
let mut result = String::with_capacity(subject.len());
let mut i = 0usize;
while i < bytes.len() {
let mut matched = false;
for (idx, f) in from.iter().enumerate() {
if !f.is_empty() && subject[i..].starts_with(f.as_str()) {
result.push_str(&to[idx]);
i += f.len();
matched = true;
break;
}
}
if !matched {
if let Some(empty_idx) = from.iter().position(String::is_empty) {
result.push_str(&to[empty_idx]);
}
let ch_len = subject[i..].chars().next().map_or(1, char::len_utf8);
result.push_str(&subject[i..i + ch_len]);
i += ch_len;
}
}
if let Some(empty_idx) = from.iter().position(String::is_empty) {
result.push_str(&to[empty_idx]);
}
result
}
fn try_eval_result(success: bool, value: IrValue) -> IrValue {
let mut result = IrAttrs::new();
result.insert("success".to_string(), IrValue::Bool(success));
result.insert("value".to_string(), value);
IrValue::Attrs(Rc::new(result))
}
fn to_float(v: &IrValue) -> Result<f64, IrEvalError> {
match v {
IrValue::Int(n) => Ok(*n as f64),
IrValue::Float(f) => Ok(*f),
other => Err(IrEvalError::TypeMismatch {
expected: "number",
got: other.type_name(),
}),
}
}
fn numeric_binop(
a: &IrValue,
b: &IrValue,
int_op: fn(i64, i64) -> i64,
float_op: fn(f64, f64) -> f64,
name: &str,
) -> Result<IrValue, IrEvalError> {
match (a, b) {
(IrValue::Int(x), IrValue::Int(y)) => Ok(IrValue::Int(int_op(*x, *y))),
(IrValue::Float(x), IrValue::Float(y)) => Ok(IrValue::Float(float_op(*x, *y))),
(IrValue::Int(x), IrValue::Float(y)) => Ok(IrValue::Float(float_op(*x as f64, *y))),
(IrValue::Float(x), IrValue::Int(y)) => Ok(IrValue::Float(float_op(*x, *y as f64))),
_ => Err(IrEvalError::TypeError(format!("{name}: expected numbers"))),
}
}
fn div_impl(a: &IrValue, b: &IrValue) -> Result<IrValue, IrEvalError> {
match (a, b) {
(IrValue::Int(x), IrValue::Int(y)) => {
if *y == 0 {
return Err(IrEvalError::DivisionByZero);
}
Ok(IrValue::Int(x / y))
}
(IrValue::Float(x), IrValue::Float(y)) => Ok(IrValue::Float(x / y)),
(IrValue::Int(x), IrValue::Float(y)) => Ok(IrValue::Float(*x as f64 / *y)),
(IrValue::Float(x), IrValue::Int(y)) => Ok(IrValue::Float(*x / *y as f64)),
_ => Err(IrEvalError::TypeError("div: expected numbers".to_string())),
}
}
fn less_than(a: &IrValue, b: &IrValue) -> Result<IrValue, IrEvalError> {
let r = match (a, b) {
(IrValue::Int(x), IrValue::Int(y)) => x < y,
(IrValue::Float(x), IrValue::Float(y)) => x < y,
(IrValue::Int(x), IrValue::Float(y)) => (*x as f64) < *y,
(IrValue::Float(x), IrValue::Int(y)) => *x < (*y as f64),
(IrValue::Str(x, _), IrValue::Str(y, _)) => x < y,
_ => {
return Err(IrEvalError::TypeError(
"lessThan: expected comparable types".to_string(),
))
}
};
Ok(IrValue::Bool(r))
}
fn sort_impl(cmp: &IrValue, arg: &IrValue) -> Result<IrValue, IrEvalError> {
let cmp = cmp.clone();
let mut list = as_list(arg)?.to_vec();
if list.len() <= 1 {
return Ok(IrValue::List(Rc::new(list)));
}
let mut err: Option<IrEvalError> = None;
list.sort_by(|a, b| {
if err.is_some() {
return std::cmp::Ordering::Equal;
}
match apply(cmp.clone(), a.clone())
.and_then(|partial| apply(partial, b.clone()))
.and_then(|v| v.force())
.and_then(|v| {
v.as_bool().map_err(|_| {
IrEvalError::TypeError("sort comparator must return bool".to_string())
})
}) {
Ok(true) => std::cmp::Ordering::Less,
Ok(false) => std::cmp::Ordering::Greater,
Err(e) => {
err = Some(e);
std::cmp::Ordering::Equal
}
}
});
if let Some(e) = err {
return Err(e);
}
Ok(IrValue::List(Rc::new(list)))
}
fn function_args(v: &IrValue) -> Result<IrValue, IrEvalError> {
match v {
IrValue::Lambda(closure) => {
let mut result = IrAttrs::new();
if let crate::ir::Param::Pattern { entries, .. } = &closure.param {
for entry in entries {
result.insert(
sui_intern::resolve(entry.name),
IrValue::Bool(entry.default.is_some()),
);
}
}
Ok(IrValue::Attrs(Rc::new(result)))
}
IrValue::Builtin(..) => Ok(IrValue::Attrs(Rc::new(IrAttrs::new()))),
other => Err(IrEvalError::TypeError(format!(
"functionArgs: expected function, got {}",
other.type_name()
))),
}
}
fn generic_closure(arg: &IrValue) -> Result<IrValue, IrEvalError> {
use std::collections::{BTreeSet, VecDeque};
let input = as_attrs(arg)?;
let start = input
.get("startSet")
.ok_or_else(|| IrEvalError::AttrNotFound("startSet".to_string()))?
.force()?;
let start_set = as_list(&start)?.to_vec();
let operator = input
.get("operator")
.ok_or_else(|| IrEvalError::AttrNotFound("operator".to_string()))?
.clone();
let mut result: Vec<IrValue> = Vec::new();
let mut work_list: VecDeque<IrValue> = start_set.into();
let mut seen: BTreeSet<String> = BTreeSet::new();
while let Some(item) = work_list.pop_front() {
let item_forced = item.force()?;
let item_attrs = as_attrs(&item_forced)?;
let key_val = item_attrs
.get("key")
.ok_or_else(|| IrEvalError::AttrNotFound("key".to_string()))?
.clone();
let key_str = display_ir_value(&key_val.force()?);
if seen.contains(&key_str) {
continue;
}
seen.insert(key_str);
result.push(item.clone());
let new_items = apply(operator.clone(), item)?.force()?;
work_list.extend(as_list(&new_items)?.iter().cloned());
}
Ok(IrValue::List(Rc::new(result)))
}
fn display_ir_value(v: &IrValue) -> String {
match v {
IrValue::Null => "null".to_string(),
IrValue::Bool(b) => b.to_string(),
IrValue::Int(n) => n.to_string(),
IrValue::Float(f) => cppnix_format_float(*f),
IrValue::Str(s, _) => {
let mut out = String::from("\"");
out.push_str(&s.replace('\\', "\\\\").replace('"', "\\\""));
out.push('"');
out
}
IrValue::Path(p) => (**p).clone(),
IrValue::List(items) => {
let mut out = String::from("[ ");
for item in items.iter() {
out.push_str(&display_forced(item));
out.push(' ');
}
out.push(']');
out
}
IrValue::Attrs(attrs) => {
let mut out = String::from("{ ");
for (k, val) in attrs.iter() {
out.push_str(k);
out.push_str(" = ");
out.push_str(&display_forced(val));
out.push_str("; ");
}
out.push('}');
out
}
IrValue::Lambda(_) => "<<lambda>>".to_string(),
IrValue::Builtin(kind, captured) => {
let mut out = String::from("<<builtin ");
out.push_str(kind.display_name(captured.len()));
out.push_str(">>");
out
}
IrValue::Thunk(_) => display_forced(v),
}
}
fn display_forced(v: &IrValue) -> String {
match v.force() {
Ok(f) => display_ir_value(&f),
Err(_) => "<<thunk:error>>".to_string(),
}
}
fn match_impl(pattern: &str, input: &str) -> Result<IrValue, IrEvalError> {
let anchored = format!("^{pattern}$");
let re = cached_regex(&anchored)?;
match re.captures(input) {
Some(caps) => {
let groups: Vec<IrValue> = (1..caps.len())
.map(|i| match caps.get(i) {
Some(m) => IrValue::string(m.as_str().to_string()),
None => IrValue::Null,
})
.collect();
Ok(IrValue::List(Rc::new(groups)))
}
None => Ok(IrValue::Null),
}
}
fn find_file_impl(entries: &[IrValue], name: &str) -> Result<IrValue, IrEvalError> {
for entry in entries {
let forced = entry.force()?;
let attrs = as_attrs(&forced)?;
let prefix = force_str(
attrs
.get("prefix")
.ok_or_else(|| IrEvalError::AttrNotFound("prefix".to_string()))?,
)?;
let path = force_str(
attrs
.get("path")
.ok_or_else(|| IrEvalError::AttrNotFound("path".to_string()))?,
)?;
if name == prefix || name.starts_with(&format!("{prefix}/")) {
let suffix = if name == prefix {
String::new()
} else {
name[prefix.len()..].to_string()
};
let full_path = format!("{path}{suffix}");
if std::path::Path::new(&full_path).exists() {
return Ok(IrValue::Path(Rc::new(full_path)));
}
}
}
Err(IrEvalError::TypeError(format!(
"findFile: file '{name}' not found in search path"
)))
}
fn base_name_of(s: &str) -> String {
let trimmed = s.trim_end_matches('/');
trimmed.rsplit('/').next().unwrap_or(trimmed).to_string()
}
fn dir_of(s: &str) -> String {
match s.rfind('/') {
Some(0) => "/".to_string(),
Some(i) => s[..i].to_string(),
None => ".".to_string(),
}
}
fn ir_to_json(v: &IrValue) -> Result<serde_json::Value, IrEvalError> {
let forced = v.force()?;
Ok(match &forced {
IrValue::Null => serde_json::Value::Null,
IrValue::Bool(b) => serde_json::Value::Bool(*b),
IrValue::Int(n) => serde_json::json!(*n),
IrValue::Float(f) => serde_json::json!(*f),
IrValue::Str(s, _) => serde_json::Value::String((**s).clone()),
IrValue::Path(_) => return Err(IrEvalError::Unsupported("path-copy-to-store")),
IrValue::List(items) => {
let mut arr = Vec::with_capacity(items.len());
for item in items.iter() {
arr.push(ir_to_json(item)?);
}
serde_json::Value::Array(arr)
}
IrValue::Attrs(attrs) => {
if attrs.contains_key("__toString") || attrs.contains_key("outPath") {
return Ok(serde_json::Value::String(coerce_to_string_plain(&forced)?));
}
let mut map = serde_json::Map::new();
for (k, val) in attrs.iter() {
map.insert(k.clone(), ir_to_json(val)?);
}
serde_json::Value::Object(map)
}
IrValue::Lambda(_) | IrValue::Builtin(..) => {
return Err(IrEvalError::TypeError(format!(
"cannot serialize {} to JSON",
forced.type_name()
)));
}
IrValue::Thunk(_) => unreachable!("force() returned a thunk"),
})
}
fn json_to_ir(json: &serde_json::Value) -> IrValue {
match json {
serde_json::Value::Null => IrValue::Null,
serde_json::Value::Bool(b) => IrValue::Bool(*b),
serde_json::Value::Number(n) => {
if let Some(i) = n.as_i64() {
IrValue::Int(i)
} else {
IrValue::Float(n.as_f64().unwrap_or(0.0))
}
}
serde_json::Value::String(s) => IrValue::string(s.clone()),
serde_json::Value::Array(arr) => {
IrValue::List(Rc::new(arr.iter().map(json_to_ir).collect()))
}
serde_json::Value::Object(obj) => {
let mut attrs = IrAttrs::new();
for (k, v) in obj {
attrs.insert(k.clone(), json_to_ir(v));
}
IrValue::Attrs(Rc::new(attrs))
}
}
}
fn xml_escape(s: &str) -> String {
s.replace('&', "&")
.replace('<', "<")
.replace('>', ">")
.replace('"', """)
}
fn value_to_xml(v: &IrValue, indent: usize) -> String {
let pad = " ".repeat(indent);
match v {
IrValue::Null => format!("{pad}<null />"),
IrValue::Bool(b) => format!("{pad}<bool value=\"{b}\" />"),
IrValue::Int(n) => format!("{pad}<int value=\"{n}\" />"),
IrValue::Float(f) => format!("{pad}<float value=\"{f}\" />"),
IrValue::Str(s, _) => format!("{pad}<string value=\"{}\" />", xml_escape(s)),
IrValue::Path(p) => format!("{pad}<path value=\"{}\" />", xml_escape(p)),
IrValue::List(items) => {
let mut out = format!("{pad}<list>\n");
for item in items.iter() {
out.push_str(&value_to_xml(item, indent + 2));
out.push('\n');
}
out.push_str(&format!("{pad}</list>"));
out
}
IrValue::Attrs(attrs) => {
let mut out = format!("{pad}<attrs>\n");
for (k, val) in attrs.iter() {
out.push_str(&format!("{pad} <attr name=\"{}\">\n", xml_escape(k)));
out.push_str(&value_to_xml(val, indent + 4));
out.push('\n');
out.push_str(&format!("{pad} </attr>\n"));
}
out.push_str(&format!("{pad}</attrs>"));
out
}
IrValue::Lambda(_) | IrValue::Builtin(..) => format!("{pad}<function />"),
IrValue::Thunk(_) => format!("{pad}<thunk />"),
}
}
fn to_xml(v: &IrValue) -> String {
format!(
"<?xml version='1.0' encoding='utf-8'?>\n{}\n",
value_to_xml(v, 0)
)
}