about summary refs log tree commit diff
path: root/tvix/eval
diff options
context:
space:
mode:
authorRyan Lahfa <tvl@lahfa.xyz>2024-03-25T02·36+0100
committerraitobezarius <tvl@lahfa.xyz>2024-04-13T10·11+0000
commit863c4207cc2adbbcbfa539fbfb4765c135801e77 (patch)
tree738314c6c1da04c4b7d78dba40544a0821a10cad /tvix/eval
parent45cf7ae657086993cedaa7c72b813e319e805484 (diff)
feat(tvix/eval): contextful JSON operations r/7899
`toJSON` transform a Nix structure into a JSON string.

For each context in that Nix structure, the JSON string must possess it.

Thus, it is necessary to take the union of all contexts and attach it to
the final structure.

Unfortunately, the return type of `into_json` is a serde's JSON object,
not a string. Therefore, it is not possible to reuse `NixString`
machinery.

Context tests are reinforced as Nix does not test those behaviors.

Fixes b/393.

Change-Id: I5afdbc4e18dd70469192c1aa657d1049ba330149
Signed-off-by: Ryan Lahfa <tvl@lahfa.xyz>
Reviewed-on: https://cl.tvl.fyi/c/depot/+/11266
Reviewed-by: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix/eval')
-rw-r--r--tvix/eval/src/builtins/mod.rs6
-rw-r--r--tvix/eval/src/builtins/to_xml.rs2
-rw-r--r--tvix/eval/src/value/json.rs63
-rw-r--r--tvix/eval/src/value/mod.rs12
-rw-r--r--tvix/eval/src/vm/generators.rs6
5 files changed, 63 insertions, 26 deletions
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs
index cb55894b6c..3d24e495ef 100644
--- a/tvix/eval/src/builtins/mod.rs
+++ b/tvix/eval/src/builtins/mod.rs
@@ -453,11 +453,11 @@ mod pure_builtins {
 
     #[builtin("toJSON")]
     async fn builtin_to_json(co: GenCo, val: Value) -> Result<Value, ErrorKind> {
-        match val.into_json(&co).await? {
+        match val.into_contextful_json(&co).await? {
             Err(cek) => Ok(Value::from(cek)),
-            Ok(json_value) => {
+            Ok((json_value, ctx)) => {
                 let json_str = serde_json::to_string(&json_value)?;
-                Ok(json_str.into())
+                Ok(NixString::new_context_from(ctx, json_str).into())
             }
         }
     }
diff --git a/tvix/eval/src/builtins/to_xml.rs b/tvix/eval/src/builtins/to_xml.rs
index 2f9a11e788..bb12cebfc9 100644
--- a/tvix/eval/src/builtins/to_xml.rs
+++ b/tvix/eval/src/builtins/to_xml.rs
@@ -137,7 +137,7 @@ fn value_variant_to_xml<W: Write>(w: &mut EventWriter<W>, value: &Value) -> Resu
         | Value::Blueprint(_)
         | Value::DeferredUpvalue(_)
         | Value::UnresolvedPath(_)
-        | Value::Json(_)
+        | Value::Json(..)
         | Value::FinaliseRequest(_) => {
             return Err(ErrorKind::TvixBug {
                 msg: "internal value variant encountered in builtins.toXML",
diff --git a/tvix/eval/src/value/json.rs b/tvix/eval/src/value/json.rs
index 7ab1655d90..c48e9c1f4e 100644
--- a/tvix/eval/src/value/json.rs
+++ b/tvix/eval/src/value/json.rs
@@ -6,6 +6,7 @@
 use super::{CoercionKind, Value};
 use crate::errors::{CatchableErrorKind, ErrorKind};
 use crate::generators::{self, GenCo};
+use crate::NixContext;
 
 use bstr::ByteSlice;
 use serde_json::value::to_value;
@@ -13,22 +14,32 @@ use serde_json::Value as Json; // name clash with *our* `Value`
 use serde_json::{Map, Number};
 
 impl Value {
-    pub async fn into_json(
+    /// Transforms the structure into a JSON
+    /// and accumulate all encountered context in the second's element
+    /// of the return type.
+    pub async fn into_contextful_json(
         self,
         co: &GenCo,
-    ) -> Result<Result<Json, CatchableErrorKind>, ErrorKind> {
+    ) -> Result<Result<(Json, NixContext), CatchableErrorKind>, ErrorKind> {
         let self_forced = generators::request_force(co, self).await;
+        let mut context = NixContext::new();
 
         let value = match self_forced {
             Value::Null => Json::Null,
             Value::Bool(b) => Json::Bool(b),
             Value::Integer(i) => Json::Number(Number::from(i)),
             Value::Float(f) => to_value(f)?,
-            Value::String(s) => Json::String(s.to_str()?.to_owned()),
+            Value::String(s) => {
+                context.mimic(&s);
+
+                Json::String(s.to_str()?.to_owned())
+            }
 
             Value::Path(p) => {
                 let imported = generators::request_path_import(co, *p).await;
-                Json::String(imported.to_string_lossy().to_string())
+                let path = imported.to_string_lossy().to_string();
+                context = context.append(crate::NixContextElement::Plain(path.clone()));
+                Json::String(path)
             }
 
             Value::List(l) => {
@@ -36,7 +47,10 @@ impl Value {
 
                 for val in l.into_iter() {
                     match generators::request_to_json(co, val).await {
-                        Ok(v) => out.push(v),
+                        Ok((v, mut ctx)) => {
+                            context = context.join(&mut ctx);
+                            out.push(v)
+                        }
                         Err(cek) => return Ok(Err(cek)),
                     }
                 }
@@ -62,7 +76,14 @@ impl Value {
                         .await?
                     {
                         Value::Catchable(cek) => return Ok(Err(*cek)),
-                        Value::String(s) => return Ok(Ok(Json::String(s.to_str()?.to_owned()))),
+                        Value::String(s) => {
+                            // We need a fresh context here because `__toString` will discard
+                            // everything.
+                            let mut fresh = NixContext::new();
+                            fresh.mimic(&s);
+
+                            return Ok(Ok((Json::String(s.to_str()?.to_owned()), fresh)));
+                        }
                         _ => panic!("Value::coerce_to_string_() returned a non-string!"),
                     }
                 }
@@ -79,7 +100,10 @@ impl Value {
                     out.insert(
                         name.to_str()?.to_owned(),
                         match generators::request_to_json(co, value).await {
-                            Ok(v) => v,
+                            Ok((v, mut ctx)) => {
+                                context = context.join(&mut ctx);
+                                v
+                            }
                             Err(cek) => return Ok(Err(cek)),
                         },
                     );
@@ -97,21 +121,34 @@ impl Value {
             | val @ Value::Blueprint(_)
             | val @ Value::DeferredUpvalue(_)
             | val @ Value::UnresolvedPath(_)
-            | val @ Value::Json(_)
+            | val @ Value::Json(..)
             | val @ Value::FinaliseRequest(_) => {
                 return Err(ErrorKind::NotSerialisableToJson(val.type_of()))
             }
         };
 
-        Ok(Ok(value))
+        Ok(Ok((value, context)))
     }
 
     /// Generator version of the above, which wraps responses in
-    /// Value::Json.
-    pub(crate) async fn into_json_generator(self, co: GenCo) -> Result<Value, ErrorKind> {
-        match self.into_json(&co).await? {
+    /// [`Value::Json`].
+    pub(crate) async fn into_contextful_json_generator(
+        self,
+        co: GenCo,
+    ) -> Result<Value, ErrorKind> {
+        match self.into_contextful_json(&co).await? {
             Err(cek) => Ok(Value::from(cek)),
-            Ok(json) => Ok(Value::Json(Box::new(json))),
+            Ok((json, ctx)) => Ok(Value::Json(Box::new((json, ctx)))),
         }
     }
+
+    /// Transforms the structure into a JSON
+    /// All the accumulated context is ignored, use [`into_contextful_json`]
+    /// to obtain the resulting context of the JSON object.
+    pub async fn into_json(
+        self,
+        co: &GenCo,
+    ) -> Result<Result<Json, CatchableErrorKind>, ErrorKind> {
+        Ok(self.into_contextful_json(co).await?.map(|(json, _)| json))
+    }
 }
diff --git a/tvix/eval/src/value/mod.rs b/tvix/eval/src/value/mod.rs
index e8e27e5968..c171c9a04e 100644
--- a/tvix/eval/src/value/mod.rs
+++ b/tvix/eval/src/value/mod.rs
@@ -78,7 +78,7 @@ pub enum Value {
     #[serde(skip)]
     UnresolvedPath(Box<PathBuf>),
     #[serde(skip)]
-    Json(Box<serde_json::Value>),
+    Json(Box<(serde_json::Value, NixContext)>),
 
     #[serde(skip)]
     FinaliseRequest(bool),
@@ -294,7 +294,7 @@ impl Value {
                 | Value::Blueprint(_)
                 | Value::DeferredUpvalue(_)
                 | Value::UnresolvedPath(_)
-                | Value::Json(_)
+                | Value::Json(..)
                 | Value::FinaliseRequest(_) => panic!(
                     "Tvix bug: internal value left on stack: {}",
                     value.type_of()
@@ -444,7 +444,7 @@ impl Value {
                 | (Value::Blueprint(_), _)
                 | (Value::DeferredUpvalue(_), _)
                 | (Value::UnresolvedPath(_), _)
-                | (Value::Json(_), _)
+                | (Value::Json(..), _)
                 | (Value::FinaliseRequest(_), _) => {
                     panic!("tvix bug: .coerce_to_string() called on internal value")
                 }
@@ -681,7 +681,7 @@ impl Value {
             Value::Blueprint(_) => "internal[blueprint]",
             Value::DeferredUpvalue(_) => "internal[deferred_upvalue]",
             Value::UnresolvedPath(_) => "internal[unresolved_path]",
-            Value::Json(_) => "internal[json]",
+            Value::Json(..) => "internal[json]",
             Value::FinaliseRequest(_) => "internal[finaliser_sentinel]",
             Value::Catchable(_) => "internal[catchable]",
         }
@@ -877,7 +877,7 @@ impl Value {
             | Value::Blueprint(_)
             | Value::DeferredUpvalue(_)
             | Value::UnresolvedPath(_)
-            | Value::Json(_)
+            | Value::Json(..)
             | Value::FinaliseRequest(_) => "an internal Tvix evaluator value".into(),
         }
     }
@@ -991,7 +991,7 @@ impl TotalDisplay for Value {
             Value::Blueprint(_) => f.write_str("internal[blueprint]"),
             Value::DeferredUpvalue(_) => f.write_str("internal[deferred_upvalue]"),
             Value::UnresolvedPath(_) => f.write_str("internal[unresolved_path]"),
-            Value::Json(_) => f.write_str("internal[json]"),
+            Value::Json(..) => f.write_str("internal[json]"),
             Value::FinaliseRequest(_) => f.write_str("internal[finaliser_sentinel]"),
 
             // Delegate thunk display to the type, as it must handle
diff --git a/tvix/eval/src/vm/generators.rs b/tvix/eval/src/vm/generators.rs
index d5b5f1de49..79de688692 100644
--- a/tvix/eval/src/vm/generators.rs
+++ b/tvix/eval/src/vm/generators.rs
@@ -493,7 +493,7 @@ where
                         VMRequest::ToJson(value) => {
                             self.reenqueue_generator(name, span.clone(), generator);
                             self.enqueue_generator("to_json", span, |co| {
-                                value.into_json_generator(co)
+                                value.into_contextful_json_generator(co)
                             });
                             return Ok(false);
                         }
@@ -778,9 +778,9 @@ pub(crate) async fn request_span(co: &GenCo) -> LightSpan {
 pub(crate) async fn request_to_json(
     co: &GenCo,
     value: Value,
-) -> Result<serde_json::Value, CatchableErrorKind> {
+) -> Result<(serde_json::Value, NixContext), CatchableErrorKind> {
     match co.yield_(VMRequest::ToJson(value)).await {
-        VMResponse::Value(Value::Json(json)) => Ok(*json),
+        VMResponse::Value(Value::Json(json_with_ctx)) => Ok(*json_with_ctx),
         VMResponse::Value(Value::Catchable(cek)) => Err(*cek),
         msg => panic!(
             "Tvix bug: VM responded with incorrect generator message: {}",