about summary refs log tree commit diff
path: root/tvix/eval/src
diff options
context:
space:
mode:
authorGriffin Smith <root@gws.fyi>2022-10-10T04·32-0400
committergrfn <grfn@gws.fyi>2022-10-15T20·35+0000
commit5eb89be68246f1e5a8cd28e48d5cec75921ca97a (patch)
tree73a8d48a4c04e2b41ef100b18560c438e9a0832c /tvix/eval/src
parent277c69cbe5aac853b26d6173e07262f8cc7aff12 (diff)
feat(tvix/eval): Implement builtins.fromJSON r/5135
Using `serde_json` for parsing JSON here, plus an `impl FromJSON for
Value`. The latter is primarily to stay "dependency light" for now -
likely going with an actual serde `Deserialize` impl in the future is
going to be way better as it allows saving significantly on intermediary
allocations.

Change-Id: I152a0448ff7c87cf7ebaac927c38912b99de1c18
Reviewed-on: https://cl.tvl.fyi/c/depot/+/6920
Tested-by: BuildkiteCI
Reviewed-by: tazjin <tazjin@tvl.su>
Diffstat (limited to 'tvix/eval/src')
-rw-r--r--tvix/eval/src/builtins/mod.rs5
-rw-r--r--tvix/eval/src/errors.rs16
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix3
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp1
-rw-r--r--tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix23
-rw-r--r--tvix/eval/src/value/attrs.rs14
-rw-r--r--tvix/eval/src/value/mod.rs48
8 files changed, 107 insertions, 4 deletions
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs
index 1ed59ffdc8ed..c0dce868c6fc 100644
--- a/tvix/eval/src/builtins/mod.rs
+++ b/tvix/eval/src/builtins/mod.rs
@@ -271,6 +271,11 @@ fn pure_builtins() -> Vec<Builtin> {
                 Ok(res)
             },
         ),
+        Builtin::new("fromJSON", &[true], |args: Vec<Value>, _: &mut VM| {
+            let json_str = args[0].to_str()?;
+            let json: serde_json::Value = serde_json::from_str(&json_str)?;
+            json.try_into()
+        }),
         Builtin::new("genList", &[true, true], |args: Vec<Value>, vm: &mut VM| {
             let len = args[1].as_int()?;
             (0..len)
diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs
index 1c0d71f6188f..33b12daa5d82 100644
--- a/tvix/eval/src/errors.rs
+++ b/tvix/eval/src/errors.rs
@@ -129,6 +129,9 @@ pub enum ErrorKind {
         error: Rc<io::Error>,
     },
 
+    /// Errors converting JSON to a value
+    FromJsonError(String),
+
     /// Tvix internal warning for features triggered by users that are
     /// not actually implemented yet, and without which eval can not
     /// proceed.
@@ -176,6 +179,13 @@ impl ErrorKind {
     }
 }
 
+impl From<serde_json::Error> for ErrorKind {
+    fn from(err: serde_json::Error) -> Self {
+        // Can't just put the `serde_json::Error` in the ErrorKind since it doesn't impl `Clone`
+        Self::FromJsonError(format!("Error parsing JSON: {err}"))
+    }
+}
+
 #[derive(Clone, Debug)]
 pub struct Error {
     pub kind: ErrorKind,
@@ -343,6 +353,10 @@ to a missing value in the attribute set(s) included via `with`."#,
                 write!(f, "{error}")
             }
 
+            ErrorKind::FromJsonError(msg) => {
+                write!(f, "Error converting JSON to a Nix value: {msg}")
+            }
+
             ErrorKind::NotImplemented(feature) => {
                 write!(f, "feature not yet implemented in Tvix: {}", feature)
             }
@@ -621,6 +635,7 @@ impl Error {
             | ErrorKind::ImportParseError { .. }
             | ErrorKind::ImportCompilerError { .. }
             | ErrorKind::IO { .. }
+            | ErrorKind::FromJsonError(_)
             | ErrorKind::NotImplemented(_) => return None,
         };
 
@@ -659,6 +674,7 @@ impl Error {
             ErrorKind::ImportParseError { .. } => "E027",
             ErrorKind::ImportCompilerError { .. } => "E028",
             ErrorKind::IO { .. } => "E029",
+            ErrorKind::FromJsonError { .. } => "E030",
 
             // Placeholder error while Tvix is under construction.
             ErrorKind::NotImplemented(_) => "E999",
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp
new file mode 100644
index 000000000000..add5505a8287
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.exp
@@ -0,0 +1 @@
+"quote \" reverse solidus \\ solidus / backspace  formfeed  newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace  1 char unicode encoded e with accent é 2 char unicode encoded s with caron š 3 char unicode encoded rightwards arrow →"
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix
new file mode 100644
index 000000000000..f00713507732
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson-escapes.nix
@@ -0,0 +1,3 @@
+# This string contains all supported escapes in a JSON string, per json.org
+# \b and \f are not supported by Nix
+builtins.fromJSON ''"quote \" reverse solidus \\ solidus \/ backspace \b formfeed \f newline \n carriage return \r horizontal tab \t 1 char unicode encoded backspace \u0008 1 char unicode encoded e with accent \u00e9 2 char unicode encoded s with caron \u0161 3 char unicode encoded rightwards arrow \u2192"''
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp
new file mode 100644
index 000000000000..4f75c09231b6
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.exp
@@ -0,0 +1 @@
+[ { Image = { Animated = false; Height = 600; IDs = [ 116 943 234 38793 true false null -100 ]; Latitude = 37.7668; Longitude = -122.3959; Thumbnail = { Height = 125; Url = "http://www.example.com/image/481989943"; Width = 100; }; Title = "View from 15th Floor"; Width = 800; }; } { name = "a"; value = "b"; } ]
diff --git a/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix
new file mode 100644
index 000000000000..ccb83fd0bd72
--- /dev/null
+++ b/tvix/eval/src/tests/tvix_tests/eval-okay-fromjson.nix
@@ -0,0 +1,23 @@
+[
+# RFC 7159, section 13.
+  (builtins.fromJSON
+    ''
+    {
+      "Image": {
+          "Width":  800,
+          "Height": 600,
+          "Title":  "View from 15th Floor",
+          "Thumbnail": {
+              "Url":    "http://www.example.com/image/481989943",
+              "Height": 125,
+              "Width":  100
+          },
+          "Animated" : false,
+          "IDs": [116, 943, 234, 38793, true  ,false,null, -100],
+          "Latitude":  37.7668,
+          "Longitude": -122.3959
+      }
+    }
+  '')
+  (builtins.fromJSON ''{"name": "a", "value": "b"}'')
+]
diff --git a/tvix/eval/src/value/attrs.rs b/tvix/eval/src/value/attrs.rs
index 318a8cfa8209..e9d5a239a3cf 100644
--- a/tvix/eval/src/value/attrs.rs
+++ b/tvix/eval/src/value/attrs.rs
@@ -274,6 +274,12 @@ impl NixAttrs {
         NixAttrs(AttrsRep::Map(map))
     }
 
+    /// Construct an optimized "KV"-style attribute set given the value for the
+    /// `"name"` key, and the value for the `"value"` key
+    pub(crate) fn from_kv(name: Value, value: Value) -> Self {
+        NixAttrs(AttrsRep::KV { name, value })
+    }
+
     /// Compare `self` against `other` for equality using Nix equality semantics
     pub fn nix_eq(&self, other: &Self, vm: &mut VM) -> Result<bool, ErrorKind> {
         match (&self.0, &other.0) {
@@ -376,10 +382,10 @@ fn attempt_optimise_kv(slice: &mut [Value]) -> Option<NixAttrs> {
         }
     };
 
-    Some(NixAttrs(AttrsRep::KV {
-        name: slice[name_idx].clone(),
-        value: slice[value_idx].clone(),
-    }))
+    Some(NixAttrs::from_kv(
+        slice[name_idx].clone(),
+        slice[value_idx].clone(),
+    ))
 }
 
 /// Set an attribute on an in-construction attribute set, while
diff --git a/tvix/eval/src/value/mod.rs b/tvix/eval/src/value/mod.rs
index 8672ffc1bb89..175b33bfa2e8 100644
--- a/tvix/eval/src/value/mod.rs
+++ b/tvix/eval/src/value/mod.rs
@@ -390,6 +390,54 @@ impl From<PathBuf> for Value {
     }
 }
 
+impl From<Vec<Value>> for Value {
+    fn from(val: Vec<Value>) -> Self {
+        Self::List(NixList::from(val))
+    }
+}
+
+impl TryFrom<serde_json::Value> for Value {
+    type Error = ErrorKind;
+
+    fn try_from(value: serde_json::Value) -> Result<Self, Self::Error> {
+        // TODO(grfn): Replace with a real serde::Deserialize impl (for perf)
+        match value {
+            serde_json::Value::Null => Ok(Self::Null),
+            serde_json::Value::Bool(b) => Ok(Self::Bool(b)),
+            serde_json::Value::Number(n) => {
+                if let Some(i) = n.as_i64() {
+                    Ok(Self::Integer(i))
+                } else if let Some(f) = n.as_f64() {
+                    Ok(Self::Float(f))
+                } else {
+                    Err(ErrorKind::FromJsonError(format!(
+                        "JSON number not representable as Nix value: {n}"
+                    )))
+                }
+            }
+            serde_json::Value::String(s) => Ok(s.into()),
+            serde_json::Value::Array(a) => Ok(a
+                .into_iter()
+                .map(Value::try_from)
+                .collect::<Result<Vec<_>, _>>()?
+                .into()),
+            serde_json::Value::Object(obj) => {
+                match (obj.len(), obj.get("name"), obj.get("value")) {
+                    (2, Some(name), Some(value)) => Ok(Self::attrs(NixAttrs::from_kv(
+                        name.clone().try_into()?,
+                        value.clone().try_into()?,
+                    ))),
+                    _ => Ok(Self::attrs(NixAttrs::from_map(
+                        obj.into_iter()
+                            .map(|(k, v)| Ok((k.into(), v.try_into()?)))
+                            .collect::<Result<_, ErrorKind>>()?,
+                    ))),
+                }
+            }
+        }
+    }
+}
+
 fn type_error(expected: &'static str, actual: &Value) -> ErrorKind {
     ErrorKind::TypeError {
         expected,