feat(tvix/eval): implement correct toString behavior
Implement C++ Nix's `EvalState::coerceToString` minus some of the Path / store handling. This is currently only used for `toString` which does all possible coercions, but we've already prepared the weaker coercion variant which is e.g. used for builtins that expect string arguments. `EvalState::coerceToPath` is still missing for builtins that need a path, but it'll be easy to build on top of this. Change-Id: I78d15576b18921791d04b6b1e964b951fdef22c6 Reviewed-on: https://cl.tvl.fyi/c/depot/+/6571 Autosubmit: sterni <sternenseemann@systemli.org> Reviewed-by: tazjin <tazjin@tvl.su> Tested-by: BuildkiteCI
This commit is contained in:
parent
16da548f93
commit
da1d71a4e8
6 changed files with 196 additions and 14 deletions
|
|
@ -12,6 +12,8 @@ mod thunk;
|
|||
|
||||
use crate::errors::ErrorKind;
|
||||
use crate::opcode::StackIdx;
|
||||
use crate::upvalues::UpvalueCarrier;
|
||||
use crate::vm::VM;
|
||||
pub use attrs::NixAttrs;
|
||||
pub use builtin::Builtin;
|
||||
pub use function::{Closure, Lambda};
|
||||
|
|
@ -78,7 +80,147 @@ macro_rules! gen_is {
|
|||
};
|
||||
}
|
||||
|
||||
/// Describes what input types are allowed when coercing a `Value` to a string
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub enum CoercionKind {
|
||||
/// Only coerce already "stringly" types like strings and paths, but also
|
||||
/// coerce sets that have a `__toString` attribute. Equivalent to
|
||||
/// `!coerceMore` in C++ Nix.
|
||||
Weak,
|
||||
/// Coerce all value types included by `Weak`, but also coerce `null`,
|
||||
/// booleans, integers, floats and lists of coercible types. Equivalent to
|
||||
/// `coerceMore` in C++ Nix.
|
||||
Strong,
|
||||
}
|
||||
|
||||
impl Value {
|
||||
/// Coerce a `Value` to a string. See `CoercionKind` for a rundown of what
|
||||
/// input types are accepted under what circumstances.
|
||||
pub fn coerce_to_string(
|
||||
&self,
|
||||
kind: CoercionKind,
|
||||
vm: &mut VM,
|
||||
) -> Result<NixString, ErrorKind> {
|
||||
if let Value::Thunk(t) = self {
|
||||
t.force(vm)?;
|
||||
}
|
||||
|
||||
match (self, kind) {
|
||||
// deal with thunks
|
||||
(Value::Thunk(t), _) => t.value().coerce_to_string(kind, vm),
|
||||
|
||||
// coercions that are always done
|
||||
(Value::String(s), _) => Ok(s.clone()),
|
||||
// TODO(sterni): Think about proper encoding handling here. This needs
|
||||
// general consideration anyways, since one current discrepancy between
|
||||
// C++ Nix and Tvix is that the former's strings are arbitrary byte
|
||||
// sequences without NUL bytes, whereas Tvix only allows valid
|
||||
// Unicode. See also b/189.
|
||||
(Value::Path(p), _) => Ok(p.to_string_lossy().into_owned().into()),
|
||||
|
||||
// Attribute sets can be converted to strings if they either have an
|
||||
// `__toString` attribute which holds a function that receives the
|
||||
// set itself or an `outPath` attribute which should be a string.
|
||||
// `__toString` is preferred.
|
||||
(Value::Attrs(attrs), _) => {
|
||||
match (attrs.select("__toString"), attrs.select("outPath")) {
|
||||
(None, None) => Err(ErrorKind::NotCoercibleToString {
|
||||
from: "set",
|
||||
kind: kind,
|
||||
}),
|
||||
|
||||
(Some(f), _) => {
|
||||
// use a closure here to deal with the thunk borrow we need to do below
|
||||
let call_to_string = |value: &Value, vm: &mut VM| {
|
||||
// TODO(sterni): calling logic should be extracted into a helper
|
||||
let result = match value {
|
||||
Value::Closure(c) => {
|
||||
vm.push(self.clone());
|
||||
vm.call(c.lambda(), c.upvalues().clone(), 1)
|
||||
.map_err(|e| e.kind)
|
||||
}
|
||||
|
||||
Value::Builtin(b) => {
|
||||
vm.push(self.clone());
|
||||
vm.call_builtin(b.clone()).map_err(|e| e.kind)?;
|
||||
Ok(vm.pop())
|
||||
}
|
||||
|
||||
_ => Err(ErrorKind::NotCallable),
|
||||
}?;
|
||||
|
||||
match result {
|
||||
Value::String(s) => Ok(s),
|
||||
// Attribute set coercion actually works
|
||||
// recursively, e.g. you can even return
|
||||
// /another/ set with a __toString attr.
|
||||
_ => result.coerce_to_string(kind, vm),
|
||||
}
|
||||
};
|
||||
|
||||
if let Value::Thunk(t) = f {
|
||||
t.force(vm)?;
|
||||
let guard = t.value();
|
||||
call_to_string(&*guard, vm)
|
||||
} else {
|
||||
call_to_string(&f, vm)
|
||||
}
|
||||
}
|
||||
|
||||
// Similarly to `__toString` we also coerce recursively for `outPath`
|
||||
(None, Some(s)) => s.coerce_to_string(kind, vm),
|
||||
}
|
||||
}
|
||||
|
||||
// strong coercions
|
||||
(Value::Null, CoercionKind::Strong) | (Value::Bool(false), CoercionKind::Strong) => {
|
||||
Ok("".into())
|
||||
}
|
||||
(Value::Bool(true), CoercionKind::Strong) => Ok("1".into()),
|
||||
|
||||
(Value::Integer(i), CoercionKind::Strong) => Ok(format!("{i}").into()),
|
||||
(Value::Float(f), CoercionKind::Strong) => {
|
||||
// contrary to normal Display, coercing a float to a string will
|
||||
// result in unconditional 6 decimal places
|
||||
Ok(format!("{:.6}", f).into())
|
||||
}
|
||||
|
||||
// Lists are coerced by coercing their elements and interspersing spaces
|
||||
(Value::List(l), CoercionKind::Strong) => {
|
||||
// TODO(sterni): use intersperse when it becomes available?
|
||||
// https://github.com/rust-lang/rust/issues/79524
|
||||
l.iter()
|
||||
.map(|v| v.coerce_to_string(kind, vm))
|
||||
.reduce(|acc, string| {
|
||||
let a = acc?;
|
||||
let s = &string?;
|
||||
Ok(a.concat(&" ".into()).concat(s))
|
||||
})
|
||||
// None from reduce indicates empty iterator
|
||||
.unwrap_or(Ok("".into()))
|
||||
}
|
||||
|
||||
(Value::Closure(_), _)
|
||||
| (Value::Builtin(_), _)
|
||||
| (Value::Null, _)
|
||||
| (Value::Bool(_), _)
|
||||
| (Value::Integer(_), _)
|
||||
| (Value::Float(_), _)
|
||||
| (Value::List(_), _) => Err(ErrorKind::NotCoercibleToString {
|
||||
from: self.type_of(),
|
||||
kind: kind,
|
||||
}),
|
||||
|
||||
(Value::AttrPath(_), _)
|
||||
| (Value::AttrNotFound, _)
|
||||
| (Value::DynamicUpvalueMissing(_), _)
|
||||
| (Value::Blueprint(_), _)
|
||||
| (Value::DeferredUpvalue(_), _) => {
|
||||
panic!("tvix bug: .coerce_to_string() called on internal value")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn type_of(&self) -> &'static str {
|
||||
match self {
|
||||
Value::Null => "null",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue