about summary refs log tree commit diff
path: root/tvix
diff options
context:
space:
mode:
authorVincent Ambo <mail@tazj.in>2023-01-15T11·52+0300
committertazjin <tazjin@tvl.su>2023-01-16T13·43+0000
commitd365b092262013e074f0fe800a1955032eaa2fd9 (patch)
treed5c32fecf48fdcc2ec9697accd2be69c1c977a1a /tvix
parent1786b4c835cbce619ddae77ffeebe89b24d50c0e (diff)
feat(tvix/eval): implement builtins.toXML r/5664
Change-Id: I009efc53a8e98f0650ae660c4decd8216e8a06e7
Reviewed-on: https://cl.tvl.fyi/c/depot/+/7835
Reviewed-by: flokli <flokli@flokli.de>
Tested-by: BuildkiteCI
Diffstat (limited to 'tvix')
-rw-r--r--tvix/Cargo.lock7
-rw-r--r--tvix/Cargo.nix16
-rw-r--r--tvix/eval/Cargo.toml1
-rw-r--r--tvix/eval/src/builtins/mod.rs9
-rw-r--r--tvix/eval/src/builtins/to_xml.rs126
-rw-r--r--tvix/eval/src/errors.rs22
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp)0
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix)0
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp)0
-rw-r--r--tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix (renamed from tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix)0
10 files changed, 181 insertions, 0 deletions
diff --git a/tvix/Cargo.lock b/tvix/Cargo.lock
index da7bc0c580..37e1a0f139 100644
--- a/tvix/Cargo.lock
+++ b/tvix/Cargo.lock
@@ -2316,6 +2316,7 @@ dependencies = [
  "test-generator",
  "test-strategy",
  "tvix-eval-builtin-macros",
+ "xml-rs",
 ]
 
 [[package]]
@@ -2603,6 +2604,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5"
 
 [[package]]
+name = "xml-rs"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d2d7d3948613f75c98fd9328cfdcc45acc4d360655289d0a7d4ec931392200a3"
+
+[[package]]
 name = "yansi"
 version = "0.5.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/tvix/Cargo.nix b/tvix/Cargo.nix
index 285e55fb73..5fca533d1c 100644
--- a/tvix/Cargo.nix
+++ b/tvix/Cargo.nix
@@ -6895,6 +6895,10 @@ rec {
             packageId = "tvix-eval-builtin-macros";
             rename = "builtin-macros";
           }
+          {
+            name = "xml-rs";
+            packageId = "xml-rs";
+          }
         ];
         devDependencies = [
           {
@@ -8424,6 +8428,18 @@ rec {
         ];
 
       };
+      "xml-rs" = rec {
+        crateName = "xml-rs";
+        version = "0.8.4";
+        edition = "2015";
+        crateBin = [ ];
+        sha256 = "18q048wk3jafgl59sa2m0qv4vk2sqkfcya4kznc5rxqkhsad7myj";
+        libName = "xml";
+        authors = [
+          "Vladimir Matveev <vmatveev@citrine.cc>"
+        ];
+
+      };
       "yansi" = rec {
         crateName = "yansi";
         version = "0.5.1";
diff --git a/tvix/eval/Cargo.toml b/tvix/eval/Cargo.toml
index 75c45c96b0..6974110290 100644
--- a/tvix/eval/Cargo.toml
+++ b/tvix/eval/Cargo.toml
@@ -25,6 +25,7 @@ serde_json = "1.0"
 smol_str = "0.1"
 tabwriter = "1.2"
 test-strategy = { version = "0.2.1", optional = true }
+xml-rs = "0.8.4"
 
 [dev-dependencies]
 criterion = "0.4"
diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs
index b93bbd99f8..7614353f5b 100644
--- a/tvix/eval/src/builtins/mod.rs
+++ b/tvix/eval/src/builtins/mod.rs
@@ -20,6 +20,7 @@ use crate::{
 
 use self::versions::{VersionPart, VersionPartsIter};
 
+mod to_xml;
 mod versions;
 
 #[cfg(feature = "impure")]
@@ -918,6 +919,14 @@ mod pure_builtins {
             .map(Value::String)
     }
 
+    #[builtin("toXML")]
+    fn builtin_to_xml(vm: &mut VM, value: Value) -> Result<Value, ErrorKind> {
+        value.deep_force(vm, &mut Default::default())?;
+        let mut buf: Vec<u8> = vec![];
+        to_xml::value_to_xml(&mut buf, &value)?;
+        Ok(String::from_utf8(buf)?.into())
+    }
+
     #[builtin("placeholder")]
     fn builtin_placeholder(vm: &mut VM, #[lazy] _: Value) -> Result<Value, ErrorKind> {
         // TODO(amjoseph)
diff --git a/tvix/eval/src/builtins/to_xml.rs b/tvix/eval/src/builtins/to_xml.rs
new file mode 100644
index 0000000000..f2b98a7e31
--- /dev/null
+++ b/tvix/eval/src/builtins/to_xml.rs
@@ -0,0 +1,126 @@
+//! This module implements `builtins.toXML`, which is a serialisation
+//! of value information as well as internal tvix state that several
+//! things in nixpkgs rely on.
+
+use std::{io::Write, rc::Rc};
+use xml::writer::events::XmlEvent;
+use xml::writer::EmitterConfig;
+use xml::writer::EventWriter;
+
+use crate::{ErrorKind, Value};
+
+/// Recursively serialise a value to XML. The value *must* have been
+/// deep-forced before being passed to this function.
+pub(super) fn value_to_xml<W: Write>(mut writer: W, value: &Value) -> Result<(), ErrorKind> {
+    let config = EmitterConfig {
+        perform_indent: true,
+        pad_self_closing: true,
+
+        // Nix uses single-quotes *only* in the document declaration,
+        // so we need to write it manually.
+        write_document_declaration: false,
+        ..Default::default()
+    };
+
+    // Write a literal document declaration, using C++-Nix-style
+    // single quotes.
+    write!(writer, "<?xml version='1.0' encoding='utf-8'?>\n")?;
+
+    let mut writer = EventWriter::new_with_config(writer, config);
+
+    writer.write(XmlEvent::start_element("expr"))?;
+    value_variant_to_xml(&mut writer, value)?;
+    writer.write(XmlEvent::end_element())?;
+
+    // Unwrap the writer to add the final newline that C++ Nix adds.
+    write!(writer.into_inner(), "\n")?;
+
+    Ok(())
+}
+
+fn write_typed_value<W: Write, V: ToString>(
+    w: &mut EventWriter<W>,
+    name: &str,
+    value: V,
+) -> Result<(), ErrorKind> {
+    w.write(XmlEvent::start_element(name).attr("value", &value.to_string()))?;
+    w.write(XmlEvent::end_element())?;
+    Ok(())
+}
+
+fn value_variant_to_xml<W: Write>(w: &mut EventWriter<W>, value: &Value) -> Result<(), ErrorKind> {
+    match value {
+        Value::Thunk(t) => return value_variant_to_xml(w, &t.value()),
+
+        Value::Null => {
+            w.write(XmlEvent::start_element("null"))?;
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Bool(b) => return write_typed_value(w, "bool", b),
+        Value::Integer(i) => return write_typed_value(w, "int", i),
+        Value::Float(f) => return write_typed_value(w, "float", f),
+        Value::String(s) => return write_typed_value(w, "string", s.as_str()),
+        Value::Path(p) => return write_typed_value(w, "path", p.to_string_lossy()),
+
+        Value::List(list) => {
+            w.write(XmlEvent::start_element("list"))?;
+
+            for elem in list.into_iter() {
+                value_variant_to_xml(w, &elem)?;
+            }
+
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Attrs(attrs) => {
+            w.write(XmlEvent::start_element("attrs"))?;
+
+            for elem in attrs.iter() {
+                w.write(XmlEvent::start_element("attr").attr("name", elem.0.as_str()))?;
+                value_variant_to_xml(w, &elem.1)?;
+                w.write(XmlEvent::end_element())?;
+            }
+
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Closure(c) => {
+            w.write(XmlEvent::start_element("function"))?;
+
+            match &c.lambda.formals {
+                Some(formals) => {
+                    if formals.ellipsis {
+                        w.write(XmlEvent::start_element("attrspat").attr("ellipsis", "1"))?;
+                        w.write(XmlEvent::end_element())?;
+                    }
+
+                    for arg in formals.arguments.iter() {
+                        w.write(XmlEvent::start_element("attr").attr("name", arg.0.as_str()))?;
+                        w.write(XmlEvent::end_element())?;
+                    }
+                }
+                None => todo!("we don't persist the arg name ..."),
+            }
+
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::Builtin(_) => {
+            w.write(XmlEvent::start_element("unevaluated"))?;
+            w.write(XmlEvent::end_element())
+        }
+
+        Value::AttrNotFound
+        | Value::Blueprint(_)
+        | Value::DeferredUpvalue(_)
+        | Value::UnresolvedPath(_) => {
+            return Err(ErrorKind::TvixBug {
+                msg: "internal value variant encountered in builtins.toXML",
+                metadata: Some(Rc::new(value.clone())),
+            })
+        }
+    }?;
+
+    Ok(())
+}
diff --git a/tvix/eval/src/errors.rs b/tvix/eval/src/errors.rs
index 6a463d9f96..67ef509ba9 100644
--- a/tvix/eval/src/errors.rs
+++ b/tvix/eval/src/errors.rs
@@ -5,12 +5,14 @@ use std::io;
 use std::path::PathBuf;
 use std::rc::Rc;
 use std::str::Utf8Error;
+use std::string::FromUtf8Error;
 use std::sync::Arc;
 use std::{fmt::Debug, fmt::Display, num::ParseIntError};
 
 use codemap::{File, Span};
 use codemap_diagnostic::{ColorConfig, Diagnostic, Emitter, Level, SpanLabel, SpanStyle};
 use smol_str::SmolStr;
+use xml::writer::Error as XmlError;
 
 use crate::{SourceCode, Value};
 
@@ -138,6 +140,9 @@ pub enum ErrorKind {
         formals_span: Span,
     },
 
+    /// Errors while serialising to XML.
+    Xml(Rc<XmlError>),
+
     /// Variant for code paths that are known bugs in Tvix (usually
     /// issues with the compiler/VM interaction).
     TvixBug {
@@ -164,6 +169,7 @@ impl error::Error for Error {
                 errors.first().map(|e| e as &dyn error::Error)
             }
             ErrorKind::IO { error, .. } => Some(error.as_ref()),
+            ErrorKind::Xml(error) => Some(error.as_ref()),
             _ => None,
         }
     }
@@ -181,6 +187,18 @@ impl From<Utf8Error> for ErrorKind {
     }
 }
 
+impl From<FromUtf8Error> for ErrorKind {
+    fn from(_: FromUtf8Error) -> Self {
+        Self::NotImplemented("FromUtf8Error not handled: https://b.tvl.fyi/issues/189")
+    }
+}
+
+impl From<XmlError> for ErrorKind {
+    fn from(err: XmlError) -> Self {
+        Self::Xml(Rc::new(err))
+    }
+}
+
 /// Implementation used if errors occur while forcing thunks (which
 /// can potentially be threaded through a few contexts, i.e. nested
 /// thunks).
@@ -392,6 +410,8 @@ to a missing value in the attribute set(s) included via `with`."#,
                 )
             }
 
+            ErrorKind::Xml(error) => write!(f, "failed to serialise to XML: {error}"),
+
             ErrorKind::TvixBug { msg, metadata } => {
                 write!(f, "Tvix bug: {}", msg)?;
 
@@ -689,6 +709,7 @@ impl Error {
             | ErrorKind::ImportCompilerError { .. }
             | ErrorKind::IO { .. }
             | ErrorKind::FromJsonError(_)
+            | ErrorKind::Xml(_)
             | ErrorKind::TvixBug { .. }
             | ErrorKind::NotImplemented(_) => return None,
         };
@@ -731,6 +752,7 @@ impl Error {
             ErrorKind::UnexpectedArgument { .. } => "E031",
             ErrorKind::RelativePathResolution(_) => "E032",
             ErrorKind::DivisionByZero => "E033",
+            ErrorKind::Xml(_) => "E034",
 
             // Special error code that is not part of the normal
             // ordering.
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp
index 828220890e..828220890e 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix
index 068c97a6c1..068c97a6c1 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp
index 634a841eb1..634a841eb1 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp
diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix
index ff1791b30e..ff1791b30e 100644
--- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix
+++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix