snix/tvix/eval/src/builtins/mod.rs
sterni f3c27f1717 fix(tvix/eval): bring foldl' strictness in line with C++ Nix
Working on https://github.com/NixOS/nix/pull/7158, I discovered that C++
Nix actually is strict in the accumulator, just not in the first value.
This seems due to the fact that in the C++ evaluator, function calls
don't seem to be thunked unconditionally and foldl' just elects not to
wrap it in a thunk (don't quote me on this summary, even though it seems
to line up with the code for primop_foldlStrict and testable behavior).

It doesn't seem worth it to risk breaking the odd Nix expression just to
be strict in one more value per invocation of foldl' (i.e. the initial
accumulator value `nul`), so let's match the existing C++ Nix behavior
here.

Change-Id: If59e62271a90d97cb440f0ca72a58ec7840d1690
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7022
Autosubmit: sterni <sternenseemann@systemli.org>
Tested-by: BuildkiteCI
Reviewed-by: tazjin <tazjin@tvl.su>
2022-10-15 14:12:23 +00:00

601 lines
22 KiB
Rust

//! This module implements the builtins exposed in the Nix language.
//!
//! See //tvix/eval/docs/builtins.md for a some context on the
//! available builtins in Nix.
use std::cmp;
use std::collections::{BTreeMap, HashMap, HashSet};
use std::path::PathBuf;
use crate::{
errors::ErrorKind,
value::{Builtin, CoercionKind, NixAttrs, NixList, NixString, Value},
vm::VM,
};
use crate::{arithmetic_op, cmp_op};
use self::versions::{VersionPart, VersionPartsIter};
#[cfg(feature = "impure")]
pub mod impure;
pub mod versions;
/// Coerce a Nix Value to a plain path, e.g. in order to access the
/// file it points to via either `builtins.toPath` or an impure
/// builtin. This coercion can _never_ be performed in a Nix program
/// without using builtins (i.e. the trick `path: /. + path` to
/// convert from a string to a path wouldn't hit this code).
pub fn coerce_value_to_path(v: &Value, vm: &mut VM) -> Result<PathBuf, ErrorKind> {
let value = v.force(vm)?;
match &*value {
Value::Thunk(t) => coerce_value_to_path(&t.value(), vm),
Value::Path(p) => Ok(p.clone()),
_ => value
.coerce_to_string(CoercionKind::Weak, vm)
.map(|s| PathBuf::from(s.as_str()))
.and_then(|path| {
if path.is_absolute() {
Ok(path)
} else {
Err(ErrorKind::NotAnAbsolutePath(path))
}
}),
}
}
/// Return all pure builtins, that is all builtins that do not rely on
/// I/O outside of the VM and which can be used in any contexts (e.g.
/// WASM).
fn pure_builtins() -> Vec<Builtin> {
vec![
Builtin::new("abort", &[true], |args: Vec<Value>, _: &mut VM| {
Err(ErrorKind::Abort(args[0].to_str()?.to_string()))
}),
Builtin::new(
"add",
&[false, false],
|args: Vec<Value>, vm: &mut VM| arithmetic_op!(&*args[0].force(vm)?, &*args[1].force(vm)?, +),
),
Builtin::new("all", &[true, true], |args: Vec<Value>, vm: &mut VM| {
for value in args[1].to_list()?.into_iter() {
let pred_result = vm.call_with(&args[0], [value])?;
if !pred_result.force(vm)?.as_bool()? {
return Ok(Value::Bool(false));
}
}
Ok(Value::Bool(true))
}),
Builtin::new("any", &[true, true], |args: Vec<Value>, vm: &mut VM| {
for value in args[1].to_list()?.into_iter() {
let pred_result = vm.call_with(&args[0], [value])?;
if pred_result.force(vm)?.as_bool()? {
return Ok(Value::Bool(true));
}
}
Ok(Value::Bool(false))
}),
Builtin::new("attrNames", &[true], |args: Vec<Value>, _: &mut VM| {
let xs = args[0].to_attrs()?;
let mut output = Vec::with_capacity(xs.len());
for (key, _val) in xs.iter() {
output.push(Value::String(key.clone()));
}
Ok(Value::List(NixList::construct(output.len(), output)))
}),
Builtin::new("attrValues", &[true], |args: Vec<Value>, _: &mut VM| {
let xs = args[0].to_attrs()?;
let mut output = Vec::with_capacity(xs.len());
for (_key, val) in xs.iter() {
output.push(val.clone());
}
Ok(Value::List(NixList::construct(output.len(), output)))
}),
Builtin::new("baseNameOf", &[true], |args: Vec<Value>, vm: &mut VM| {
let s = args[0].coerce_to_string(CoercionKind::Weak, vm)?;
let result: String = s.rsplit_once('/').map(|(_, x)| x).unwrap_or(&s).into();
Ok(result.into())
}),
Builtin::new("bitAnd", &[true, true], |args: Vec<Value>, _: &mut VM| {
Ok(Value::Integer(args[0].as_int()? & args[1].as_int()?))
}),
Builtin::new("bitOr", &[true, true], |args: Vec<Value>, _: &mut VM| {
Ok(Value::Integer(args[0].as_int()? | args[1].as_int()?))
}),
Builtin::new("bitXor", &[true, true], |args: Vec<Value>, _: &mut VM| {
Ok(Value::Integer(args[0].as_int()? ^ args[1].as_int()?))
}),
Builtin::new(
"catAttrs",
&[true, true],
|args: Vec<Value>, vm: &mut VM| {
let key = args[0].to_str()?;
let list = args[1].to_list()?;
let mut output = vec![];
for item in list.into_iter() {
let set = item.force(vm)?.to_attrs()?;
if let Some(value) = set.select(key.as_str()) {
output.push(value.clone());
}
}
Ok(Value::List(NixList::construct(output.len(), output)))
},
),
Builtin::new(
"compareVersions",
&[true, true],
|args: Vec<Value>, _: &mut VM| {
let s1 = args[0].to_str()?;
let s1 = VersionPartsIter::new_for_cmp(s1.as_str());
let s2 = args[1].to_str()?;
let s2 = VersionPartsIter::new_for_cmp(s2.as_str());
match s1.cmp(s2) {
std::cmp::Ordering::Less => Ok(Value::Integer(-1)),
std::cmp::Ordering::Equal => Ok(Value::Integer(0)),
std::cmp::Ordering::Greater => Ok(Value::Integer(1)),
}
},
),
Builtin::new("concatLists", &[true], |args: Vec<Value>, vm: &mut VM| {
let list = args[0].to_list()?;
let lists = list
.into_iter()
.map(|elem| {
let value = elem.force(vm)?;
value.to_list()
})
.collect::<Result<Vec<NixList>, ErrorKind>>()?;
Ok(Value::List(NixList::from(
lists.into_iter().flatten().collect::<Vec<Value>>(),
)))
}),
Builtin::new(
"concatMap",
&[true, true],
|args: Vec<Value>, vm: &mut VM| {
let list = args[1].to_list()?;
let mut res = Vec::new();
for val in list {
res.extend(vm.call_with(&args[0], [val])?.force(vm)?.to_list()?);
}
Ok(Value::List(res.into()))
},
),
Builtin::new(
"concatStringsSep",
&[true, true],
|args: Vec<Value>, vm: &mut VM| {
let separator = args[0].to_str()?;
let list = args[1].to_list()?;
let mut res = String::new();
for (i, val) in list.into_iter().enumerate() {
if i != 0 {
res.push_str(&separator);
}
res.push_str(&val.force(vm)?.coerce_to_string(CoercionKind::Weak, vm)?);
}
Ok(res.into())
},
),
Builtin::new(
"div",
&[false, false],
|args: Vec<Value>, vm: &mut VM| arithmetic_op!(&*args[0].force(vm)?, &*args[1].force(vm)?, /),
),
Builtin::new("dirOf", &[true], |args: Vec<Value>, vm: &mut VM| {
let s = args[0].coerce_to_string(CoercionKind::Weak, vm)?;
let result = s
.rsplit_once('/')
.map(|(x, _)| match x {
"" => "/",
_ => x,
})
.unwrap_or(".");
if args[0].is_path() {
Ok(Value::Path(result.into()))
} else {
Ok(result.into())
}
}),
Builtin::new("elem", &[true, true], |args: Vec<Value>, vm: &mut VM| {
for val in args[1].to_list()? {
if val.nix_eq(&args[0], vm)? {
return Ok(true.into());
}
}
Ok(false.into())
}),
Builtin::new("elemAt", &[true, true], |args: Vec<Value>, _: &mut VM| {
let xs = args[0].to_list()?;
let i = args[1].as_int()?;
if i < 0 {
Err(ErrorKind::IndexOutOfBounds { index: i })
} else {
match xs.get(i as usize) {
Some(x) => Ok(x.clone()),
None => Err(ErrorKind::IndexOutOfBounds { index: i }),
}
}
}),
Builtin::new("filter", &[true, true], |args: Vec<Value>, vm: &mut VM| {
let list: NixList = args[1].to_list()?;
list.into_iter()
.filter_map(|elem| {
let result = match vm.call_with(&args[0], [elem.clone()]) {
Err(err) => return Some(Err(err)),
Ok(result) => result,
};
// Must be assigned to a local to avoid a borrowcheck
// failure related to the ForceResult destructor.
let result = match result.force(vm) {
Err(err) => Some(Err(vm.error(err))),
Ok(value) => match value.as_bool() {
Ok(true) => Some(Ok(elem)),
Ok(false) => None,
Err(err) => Some(Err(vm.error(err))),
},
};
result
})
.collect::<Result<Vec<Value>, _>>()
.map(|list| Value::List(NixList::from(list)))
.map_err(Into::into)
}),
Builtin::new(
"foldl'",
&[true, false, true],
|mut args: Vec<Value>, vm: &mut VM| {
let list = args.pop().unwrap().to_list()?;
let mut res = args.pop().unwrap();
let op = args.pop().unwrap();
for val in list {
res = vm.call_with(&op, [val, res])?;
res.force(vm)?;
}
Ok(res)
},
),
Builtin::new("genList", &[true, true], |args: Vec<Value>, vm: &mut VM| {
let len = args[1].as_int()?;
(0..len)
.map(|i| vm.call_with(&args[0], [i.into()]))
.collect::<Result<Vec<Value>, _>>()
.map(|list| Value::List(NixList::from(list)))
.map_err(Into::into)
}),
Builtin::new("getAttr", &[true, true], |args: Vec<Value>, _: &mut VM| {
let k = args[0].to_str()?;
let xs = args[1].to_attrs()?;
match xs.select(k.as_str()) {
Some(x) => Ok(x.clone()),
None => Err(ErrorKind::AttributeNotFound {
name: k.to_string(),
}),
}
}),
Builtin::new("hasAttr", &[true, true], |args: Vec<Value>, _: &mut VM| {
let k = args[0].to_str()?;
let xs = args[1].to_attrs()?;
Ok(Value::Bool(xs.contains(k.as_str())))
}),
Builtin::new("head", &[true], |args: Vec<Value>, _: &mut VM| {
match args[0].to_list()?.get(0) {
Some(x) => Ok(x.clone()),
None => Err(ErrorKind::IndexOutOfBounds { index: 0 }),
}
}),
// For `is*` predicates we force manually, as Value::force also unwraps any Thunks
Builtin::new("isAttrs", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::Attrs(_))))
}),
Builtin::new("isBool", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::Bool(_))))
}),
Builtin::new("isFloat", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::Float(_))))
}),
Builtin::new("isFunction", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(
*value,
Value::Closure(_) | Value::Builtin(_)
)))
}),
Builtin::new("isInt", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::Integer(_))))
}),
Builtin::new("isList", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::List(_))))
}),
Builtin::new("isNull", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::Null)))
}),
Builtin::new("isPath", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::Path(_))))
}),
Builtin::new("isString", &[false], |args: Vec<Value>, vm: &mut VM| {
let value = args[0].force(vm)?;
Ok(Value::Bool(matches!(*value, Value::String(_))))
}),
Builtin::new("length", &[true], |args: Vec<Value>, _: &mut VM| {
Ok(Value::Integer(args[0].to_list()?.len() as i64))
}),
Builtin::new(
"lessThan",
&[false, false],
|args: Vec<Value>, vm: &mut VM| cmp_op!(&*args[0].force(vm)?, &*args[1].force(vm)?, <),
),
Builtin::new("listToAttrs", &[true], |args: Vec<Value>, vm: &mut VM| {
let list = args[0].to_list()?;
let mut map = BTreeMap::new();
for val in list {
let attrs = val.force(vm)?.to_attrs()?;
let get = |key| {
attrs
.select(key)
.ok_or(ErrorKind::AttributeNotFound { name: key.into() })
};
let name = get("name")?.to_str()?;
let value = get("value")?.clone();
// Map entries earlier in the list take precedence over entries later in the list
map.entry(name).or_insert(value);
}
Ok(Value::attrs(NixAttrs::from_map(map)))
}),
Builtin::new("map", &[true, true], |args: Vec<Value>, vm: &mut VM| {
let list: NixList = args[1].to_list()?;
list.into_iter()
.map(|val| vm.call_with(&args[0], [val]))
.collect::<Result<Vec<Value>, _>>()
.map(|list| Value::List(NixList::from(list)))
.map_err(Into::into)
}),
Builtin::new(
"mul",
&[false, false],
|args: Vec<Value>, vm: &mut VM| arithmetic_op!(&*args[0].force(vm)?, &*args[1].force(vm)?, *),
),
Builtin::new("parseDrvName", &[true], |args: Vec<Value>, _vm: &mut VM| {
// This replicates cppnix's (mis?)handling of codepoints
// above U+007f following 0x2d ('-')
let s = args[0].to_str()?;
let slice: &[u8] = s.as_str().as_ref();
let (name, dash_and_version) = slice.split_at(
slice
.windows(2)
.enumerate()
.find_map(|x| match x {
(idx, [b'-', c1]) if !c1.is_ascii_alphabetic() => Some(idx),
_ => None,
})
.unwrap_or(slice.len()),
);
let version = dash_and_version
.split_first()
.map(|x| core::str::from_utf8(x.1))
.unwrap_or(Ok(""))?
.into();
Ok(Value::attrs(NixAttrs::from_map(BTreeMap::from([
(NixString::NAME, core::str::from_utf8(name)?.into()),
("version".into(), version),
]))))
}),
Builtin::new(
"removeAttrs",
&[true, true],
|args: Vec<Value>, _: &mut VM| {
let attrs = args[0].to_attrs()?;
let keys = args[1]
.to_list()?
.into_iter()
.map(|v| v.to_str())
.collect::<Result<HashSet<_>, _>>()?;
let mut res = BTreeMap::new();
for (k, v) in attrs.iter() {
if !keys.contains(k) {
res.insert(k.clone(), v.clone());
}
}
Ok(Value::attrs(NixAttrs::from_map(res)))
},
),
Builtin::new("splitVersion", &[true], |args: Vec<Value>, _: &mut VM| {
let s = args[0].to_str()?;
let s = VersionPartsIter::new(s.as_str());
let parts = s
.map(|s| {
Value::String(match s {
VersionPart::Number(n) => n.into(),
VersionPart::Word(w) => w.into(),
})
})
.collect::<Vec<Value>>();
Ok(Value::List(NixList::construct(parts.len(), parts)))
}),
Builtin::new("stringLength", &[false], |args: Vec<Value>, vm: &mut VM| {
// also forces the value
let s = args[0].coerce_to_string(CoercionKind::Weak, vm)?;
Ok(Value::Integer(s.as_str().len() as i64))
}),
Builtin::new(
"sub",
&[false, false],
|args: Vec<Value>, vm: &mut VM| arithmetic_op!(&*args[0].force(vm)?, &*args[1].force(vm)?, -),
),
Builtin::new(
"substring",
&[true, true, true],
|args: Vec<Value>, _: &mut VM| {
let beg = args[0].as_int()?;
let len = args[1].as_int()?;
let x = args[2].to_str()?;
if beg < 0 {
return Err(ErrorKind::IndexOutOfBounds { index: beg });
}
let beg = beg as usize;
// Nix doesn't assert that the length argument is
// non-negative when the starting index is GTE the
// string's length.
if beg >= x.as_str().len() {
return Ok(Value::String("".into()));
}
if len < 0 {
return Err(ErrorKind::NegativeLength { length: len });
}
let len = len as usize;
let end = cmp::min(beg + len, x.as_str().len());
Ok(Value::String(
x.as_str()[(beg as usize)..(end as usize)].into(),
))
},
),
Builtin::new("tail", &[true], |args: Vec<Value>, _: &mut VM| {
let xs = args[0].to_list()?;
if xs.len() == 0 {
Err(ErrorKind::TailEmptyList)
} else {
let output = xs.into_iter().skip(1).collect::<Vec<_>>();
Ok(Value::List(NixList::construct(output.len(), output)))
}
}),
Builtin::new("throw", &[true], |args: Vec<Value>, _: &mut VM| {
Err(ErrorKind::Throw(args[0].to_str()?.to_string()))
}),
// coerce_to_string forces for us
Builtin::new("toString", &[false], |args: Vec<Value>, vm: &mut VM| {
args[0]
.coerce_to_string(CoercionKind::Strong, vm)
.map(Value::String)
}),
Builtin::new(
"trace",
&[true, true],
|mut args: Vec<Value>, _: &mut VM| {
let value = args.pop().unwrap();
let trace_value = args.pop().unwrap();
// TODO(grfn): `trace` should be pluggable and capturable, probably via a method on
// the VM
println!("trace: {} :: {}", trace_value, trace_value.type_of());
Ok(value)
},
),
Builtin::new("tryEval", &[false], |args: Vec<Value>, vm: &mut VM| {
let mut res = BTreeMap::new();
match args[0].force(vm) {
Ok(value) => {
res.insert("value".into(), (*value).clone());
res.insert("success".into(), true.into());
}
Err(e) if e.is_catchable() => {
res.insert("value".into(), false.into());
res.insert("success".into(), false.into());
}
Err(e) => return Err(e),
}
Ok(Value::attrs(NixAttrs::from_map(res)))
}),
Builtin::new("toPath", &[false], |args: Vec<Value>, vm: &mut VM| {
let path: Value = crate::value::canon_path(coerce_value_to_path(&args[0], vm)?).into();
Ok(path.coerce_to_string(CoercionKind::Weak, vm)?.into())
}),
Builtin::new("typeOf", &[false], |args: Vec<Value>, vm: &mut VM| {
// We force manually here because it also unwraps the Thunk
// representation, if any.
// TODO(sterni): it'd be nice if we didn't have to worry about this
let value = args[0].force(vm)?;
Ok(Value::String(value.type_of().into()))
}),
]
}
fn builtins_set() -> NixAttrs {
let mut map: BTreeMap<NixString, Value> = BTreeMap::new();
// Pure-value builtins
map.insert(
"nixVersion".into(),
Value::String("2.3-compat-tvix-0.1".into()),
);
let mut add_builtins = |builtins: Vec<Builtin>| {
for builtin in builtins {
map.insert(builtin.name().into(), Value::Builtin(builtin));
}
};
add_builtins(pure_builtins());
#[cfg(feature = "impure")]
{
map.extend(impure::builtins());
}
NixAttrs::from_map(map)
}
/// Set of Nix builtins that are globally available.
pub fn global_builtins() -> HashMap<&'static str, Value> {
let builtins = builtins_set();
let mut globals: HashMap<&'static str, Value> = HashMap::new();
// known global builtins from the builtins set.
for global in &[
"abort",
"baseNameOf",
"derivation",
"derivationStrict",
"dirOf",
"fetchGit",
"fetchMercurial",
"fetchTarball",
"fromTOML",
"import",
"isNull",
"map",
"placeholder",
"removeAttrs",
"scopedImport",
"throw",
"toString",
] {
if let Some(builtin) = builtins.select(global) {
globals.insert(global, builtin.clone());
}
}
globals.insert("builtins", Value::attrs(builtins));
globals
}