about summary refs log tree commit diff
path: root/tvix/eval/src/value
diff options
context:
space:
mode:
Diffstat (limited to 'tvix/eval/src/value')
-rw-r--r--tvix/eval/src/value/mod.rs142
1 files changed, 142 insertions, 0 deletions
diff --git a/tvix/eval/src/value/mod.rs b/tvix/eval/src/value/mod.rs
index 6baa8b666ed6..f8fb9c7b40b7 100644
--- a/tvix/eval/src/value/mod.rs
+++ b/tvix/eval/src/value/mod.rs
@@ -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",