diff options
-rw-r--r-- | tvix/eval/src/builtins/mod.rs | 93 | ||||
-rw-r--r-- | tvix/eval/src/errors.rs | 5 | ||||
-rw-r--r-- | tvix/eval/src/opcode.rs | 3 | ||||
-rw-r--r-- | tvix/eval/src/value/json.rs | 9 | ||||
-rw-r--r-- | tvix/eval/src/value/mod.rs | 52 | ||||
-rw-r--r-- | tvix/eval/src/vm/generators.rs | 17 | ||||
-rw-r--r-- | tvix/eval/src/vm/mod.rs | 63 | ||||
-rw-r--r-- | tvix/glue/src/builtins/derivation.rs | 19 |
8 files changed, 207 insertions, 54 deletions
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs index 27b7d5648f48..6cdb434f42b4 100644 --- a/tvix/eval/src/builtins/mod.rs +++ b/tvix/eval/src/builtins/mod.rs @@ -41,6 +41,8 @@ pub const CURRENT_PLATFORM: &str = env!("TVIX_CURRENT_SYSTEM"); /// 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). +/// +/// This operation doesn't import a Nix path value into the store. pub async fn coerce_value_to_path( co: &GenCo, v: Value, @@ -50,7 +52,16 @@ pub async fn coerce_value_to_path( return Ok(Ok(*p)); } - match generators::request_string_coerce(co, value, CoercionKind::Weak).await { + match generators::request_string_coerce( + co, + value, + CoercionKind { + strong: false, + import_paths: false, + }, + ) + .await + { Ok(vs) => { let path = PathBuf::from(vs.as_str()); if path.is_absolute() { @@ -71,6 +82,7 @@ mod pure_builtins { #[builtin("abort")] async fn builtin_abort(co: GenCo, message: Value) -> Result<Value, ErrorKind> { + // TODO(sterni): coerces to string Err(ErrorKind::Abort(message.to_str()?.to_string())) } @@ -136,14 +148,15 @@ mod pure_builtins { let span = generators::request_span(&co).await; let s = match s { val @ Value::Catchable(_) => return Ok(val), - // it is important that builtins.baseNameOf does not - // coerce paths into strings, since this will turn them - // into store paths, and `builtins.baseNameOf - // ./config.nix` will return a hash-prefixed value like - // "hpmyf3vlqg5aadri97xglcvvjbk8xw3g-config.nix" - Value::Path(p) => NixString::from(p.to_string_lossy().to_string()), _ => s - .coerce_to_string(co, CoercionKind::Weak, span) + .coerce_to_string( + co, + CoercionKind { + strong: false, + import_paths: false, + }, + span, + ) .await? .to_str()?, }; @@ -239,7 +252,16 @@ mod pure_builtins { if i != 0 { res.push_str(&separator); } - match generators::request_string_coerce(&co, val, CoercionKind::Weak).await { + match generators::request_string_coerce( + &co, + val, + CoercionKind { + strong: false, + import_paths: true, + }, + ) + .await + { Ok(s) => res.push_str(s.as_str()), Err(c) => return Ok(Value::Catchable(c)), } @@ -263,7 +285,14 @@ mod pure_builtins { let is_path = s.is_path(); let span = generators::request_span(&co).await; let str = s - .coerce_to_string(co, CoercionKind::Weak, span) + .coerce_to_string( + co, + CoercionKind { + strong: false, + import_paths: false, + }, + span, + ) .await? .to_str()?; let result = str @@ -983,7 +1012,16 @@ mod pure_builtins { async fn builtin_string_length(co: GenCo, #[lazy] s: Value) -> Result<Value, ErrorKind> { // also forces the value let span = generators::request_span(&co).await; - let s = s.coerce_to_string(co, CoercionKind::Weak, span).await?; + let s = s + .coerce_to_string( + co, + CoercionKind { + strong: false, + import_paths: true, + }, + span, + ) + .await?; Ok(Value::Integer(s.to_str()?.as_str().len() as i64)) } @@ -1002,7 +1040,16 @@ mod pure_builtins { let beg = start.as_int()?; let len = len.as_int()?; let span = generators::request_span(&co).await; - let x = s.coerce_to_string(co, CoercionKind::Weak, span).await?; + let x = s + .coerce_to_string( + co, + CoercionKind { + strong: false, + import_paths: true, + }, + span, + ) + .await?; if x.is_catchable() { return Ok(x); } @@ -1043,6 +1090,7 @@ mod pure_builtins { #[builtin("throw")] async fn builtin_throw(co: GenCo, message: Value) -> Result<Value, ErrorKind> { + // TODO(sterni): coerces to string Ok(Value::Catchable(CatchableErrorKind::Throw( message.to_str()?.to_string(), ))) @@ -1052,7 +1100,15 @@ mod pure_builtins { async fn builtin_to_string(co: GenCo, #[lazy] x: Value) -> Result<Value, ErrorKind> { // coerce_to_string forces for us let span = generators::request_span(&co).await; - x.coerce_to_string(co, CoercionKind::Strong, span).await + x.coerce_to_string( + co, + CoercionKind { + strong: true, + import_paths: false, + }, + span, + ) + .await } #[builtin("toXML")] @@ -1085,7 +1141,16 @@ mod pure_builtins { Ok(path) => { let path: Value = crate::value::canon_path(path).into(); let span = generators::request_span(&co).await; - Ok(path.coerce_to_string(co, CoercionKind::Weak, span).await?) + Ok(path + .coerce_to_string( + co, + CoercionKind { + strong: false, + import_paths: false, + }, + span, + ) + .await?) } } } diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs index 23905e438edb..6ac29492dfc3 100644 --- a/tvix/eval/src/errors.rs +++ b/tvix/eval/src/errors.rs @@ -378,10 +378,7 @@ to a missing value in the attribute set(s) included via `with`."#, ErrorKind::BytecodeError(_) => write!(f, "while evaluating this Nix code"), ErrorKind::NotCoercibleToString { kind, from } => { - let kindly = match kind { - CoercionKind::Strong => "strongly", - CoercionKind::Weak => "weakly", - }; + let kindly = if kind.strong { "strongly" } else { "weakly" }; let hint = if *from == "set" { ", missing a `__toString` or `outPath` attribute" diff --git a/tvix/eval/src/opcode.rs b/tvix/eval/src/opcode.rs index 467798177550..b57a79a3baec 100644 --- a/tvix/eval/src/opcode.rs +++ b/tvix/eval/src/opcode.rs @@ -202,7 +202,8 @@ pub enum OpCode { OpInterpolate(Count), /// Force the Value on the stack and coerce it to a string, always using - /// `CoercionKind::Weak`. + /// `CoercionKind::Weak { import_paths: true }`. This is the behavior + /// necessary for path interpolation. OpCoerceToString, // Paths diff --git a/tvix/eval/src/value/json.rs b/tvix/eval/src/value/json.rs index f7ecf121de5e..54b29131109a 100644 --- a/tvix/eval/src/value/json.rs +++ b/tvix/eval/src/value/json.rs @@ -47,7 +47,14 @@ impl Value { if attrs.select("__toString").is_some() { let span = generators::request_span(co).await; match Value::Attrs(attrs) - .coerce_to_string_(co, CoercionKind::Weak, span) + .coerce_to_string_( + co, + CoercionKind { + strong: false, + import_paths: false, + }, + span, + ) .await? { Value::Catchable(cek) => return Ok(Err(cek)), diff --git a/tvix/eval/src/value/mod.rs b/tvix/eval/src/value/mod.rs index 300824b88ff7..0007735cc096 100644 --- a/tvix/eval/src/value/mod.rs +++ b/tvix/eval/src/value/mod.rs @@ -162,15 +162,22 @@ macro_rules! gen_is { /// Describes what input types are allowed when coercing a `Value` to a string #[derive(Clone, Copy, PartialEq, 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, +pub struct CoercionKind { + /// If false only coerce already "stringly" types like strings and paths, but + /// also coerce sets that have a `__toString` attribute. In Tvix, this is + /// usually called a weak coercion. Equivalent to passing `false` as the + /// `coerceMore` argument of `EvalState::coerceToString` in C++ Nix. + /// + /// If true coerce all value types included by a weak coercion, but also + /// coerce `null`, booleans, integers, floats and lists of coercible types. + /// Consequently, we call this a strong coercion. Equivalent to passing + /// `true` as `coerceMore` in C++ Nix. + pub strong: bool, + + /// If `import_paths` is `true`, paths are imported into the store and their + /// store path is the result of the coercion (equivalent to the + /// `copyToStore` argument of `EvalState::coerceToString` in C++ Nix). + pub import_paths: bool, } impl<T> From<T> for Value @@ -323,11 +330,22 @@ impl Value { // 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), _) => { - // TODO(tazjin): there are cases where coerce_to_string does not import + ( + Value::Path(p), + CoercionKind { + import_paths: true, .. + }, + ) => { let imported = generators::request_path_import(co, *p).await; Ok(imported.to_string_lossy().into_owned()) } + ( + Value::Path(p), + CoercionKind { + import_paths: false, + .. + }, + ) => Ok(p.to_string_lossy().into_owned()), // Attribute sets can be converted to strings if they either have an // `__toString` attribute which holds a function that receives the @@ -358,19 +376,19 @@ impl Value { } // strong coercions - (Value::Null, CoercionKind::Strong) - | (Value::Bool(false), CoercionKind::Strong) => Ok("".to_owned()), - (Value::Bool(true), CoercionKind::Strong) => Ok("1".to_owned()), + (Value::Null, CoercionKind { strong: true, .. }) + | (Value::Bool(false), CoercionKind { strong: true, .. }) => Ok("".to_owned()), + (Value::Bool(true), CoercionKind { strong: true, .. }) => Ok("1".to_owned()), - (Value::Integer(i), CoercionKind::Strong) => Ok(format!("{i}")), - (Value::Float(f), CoercionKind::Strong) => { + (Value::Integer(i), CoercionKind { strong: true, .. }) => Ok(format!("{i}")), + (Value::Float(f), CoercionKind { strong: true, .. }) => { // contrary to normal Display, coercing a float to a string will // result in unconditional 6 decimal places Ok(format!("{:.6}", f)) } // Lists are coerced by coercing their elements and interspersing spaces - (Value::List(list), CoercionKind::Strong) => { + (Value::List(list), CoercionKind { strong: true, .. }) => { for elem in list.into_iter().rev() { vals.push(elem); } diff --git a/tvix/eval/src/vm/generators.rs b/tvix/eval/src/vm/generators.rs index 6563a82790a6..9686e6542f13 100644 --- a/tvix/eval/src/vm/generators.rs +++ b/tvix/eval/src/vm/generators.rs @@ -144,10 +144,19 @@ impl Display for VMRequest { } VMRequest::StackPush(v) => write!(f, "stack_push({})", v.type_of()), VMRequest::StackPop => write!(f, "stack_pop"), - VMRequest::StringCoerce(v, kind) => match kind { - CoercionKind::Weak => write!(f, "weak_string_coerce({})", v.type_of()), - CoercionKind::Strong => write!(f, "strong_string_coerce({})", v.type_of()), - }, + VMRequest::StringCoerce( + v, + CoercionKind { + strong, + import_paths, + }, + ) => write!( + f, + "{}_{}importing_string_coerce({})", + if *strong { "strong" } else { "weak" }, + if *import_paths { "" } else { "non_" }, + v.type_of() + ), VMRequest::Call(v) => write!(f, "call({})", v), VMRequest::EnterLambda { lambda, .. } => { write!(f, "enter_lambda({:p})", *lambda) diff --git a/tvix/eval/src/vm/mod.rs b/tvix/eval/src/vm/mod.rs index 902121ebc727..0ac5024d6b11 100644 --- a/tvix/eval/src/vm/mod.rs +++ b/tvix/eval/src/vm/mod.rs @@ -780,7 +780,14 @@ impl<'o> VM<'o> { self.push_call_frame(span, frame); self.enqueue_generator("coerce_to_string", gen_span.clone(), |co| { - value.coerce_to_string(co, CoercionKind::Weak, gen_span) + value.coerce_to_string( + co, + CoercionKind { + strong: false, + import_paths: true, + }, + gen_span, + ) }); return Ok(false); @@ -1206,7 +1213,23 @@ async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> { let result = match (a, b) { (Value::Path(p), v) => { let mut path = p.to_string_lossy().into_owned(); - match generators::request_string_coerce(&co, v, CoercionKind::Weak).await { + match generators::request_string_coerce( + &co, + v, + CoercionKind { + strong: false, + + // Concatenating a Path with something else results in a + // Path, so we don't need to import any paths (paths + // imported by Nix always exist as a string, unless + // converted by the user). In C++ Nix they even may not + // contain any string context, the resulting error of such a + // case can not be replicated by us. + import_paths: false, + }, + ) + .await + { Ok(vs) => { path.push_str(vs.as_str()); crate::value::canon_path(PathBuf::from(path)).into() @@ -1215,14 +1238,38 @@ async fn add_values(co: GenCo, a: Value, b: Value) -> Result<Value, ErrorKind> { } } (Value::String(s1), Value::String(s2)) => Value::String(s1.concat(&s2)), - (Value::String(s1), v) => generators::request_string_coerce(&co, v, CoercionKind::Weak) - .await - .map(|s2| Value::String(s1.concat(&s2))) - .into(), + (Value::String(s1), v) => generators::request_string_coerce( + &co, + v, + CoercionKind { + strong: false, + // Behaves the same as string interpolation + import_paths: true, + }, + ) + .await + .map(|s2| Value::String(s1.concat(&s2))) + .into(), (a @ Value::Integer(_), b) | (a @ Value::Float(_), b) => arithmetic_op!(&a, &b, +)?, (a, b) => { - let r1 = generators::request_string_coerce(&co, a, CoercionKind::Weak).await; - let r2 = generators::request_string_coerce(&co, b, CoercionKind::Weak).await; + let r1 = generators::request_string_coerce( + &co, + a, + CoercionKind { + strong: false, + import_paths: false, + }, + ) + .await; + let r2 = generators::request_string_coerce( + &co, + b, + CoercionKind { + strong: false, + import_paths: false, + }, + ) + .await; match (r1, r2) { (Ok(s1), Ok(s2)) => Value::String(s1.concat(&s2)), (Err(c), _) => return Ok(Value::Catchable(c)), diff --git a/tvix/glue/src/builtins/derivation.rs b/tvix/glue/src/builtins/derivation.rs index 6eedbc13e7e3..993e7612aed4 100644 --- a/tvix/glue/src/builtins/derivation.rs +++ b/tvix/glue/src/builtins/derivation.rs @@ -161,7 +161,7 @@ async fn handle_derivation_parameters( "args" => { let args = value.to_list()?; for arg in args { - match strong_coerce_to_string(co, arg).await? { + match strong_importing_coerce_to_string(co, arg).await? { Err(cek) => return Ok(Err(cek)), Ok(s) => drv.arguments.push(s), } @@ -194,12 +194,21 @@ async fn handle_derivation_parameters( Ok(Ok(true)) } -async fn strong_coerce_to_string( +async fn strong_importing_coerce_to_string( co: &GenCo, val: Value, ) -> Result<Result<String, CatchableErrorKind>, ErrorKind> { let val = generators::request_force(co, val).await; - match generators::request_string_coerce(co, val, CoercionKind::Strong).await { + match generators::request_string_coerce( + co, + val, + CoercionKind { + strong: true, + import_paths: true, + }, + ) + .await + { Err(cek) => Ok(Err(cek)), Ok(val_str) => Ok(Ok(val_str.as_str().to_string())), } @@ -268,7 +277,7 @@ pub(crate) mod derivation_builtins { key: &str, ) -> Result<Result<Option<String>, CatchableErrorKind>, ErrorKind> { if let Some(attr) = attrs.select(key) { - match strong_coerce_to_string(co, attr.clone()).await? { + match strong_importing_coerce_to_string(co, attr.clone()).await? { Err(cek) => return Ok(Err(cek)), Ok(str) => return Ok(Ok(Some(str))), } @@ -283,7 +292,7 @@ pub(crate) mod derivation_builtins { continue; } - match strong_coerce_to_string(&co, value.clone()).await? { + match strong_importing_coerce_to_string(&co, value.clone()).await? { Err(cek) => return Ok(Value::Catchable(cek)), Ok(val_str) => { // handle_derivation_parameters tells us whether the |