From d365b092262013e074f0fe800a1955032eaa2fd9 Mon Sep 17 00:00:00 2001 From: Vincent Ambo Date: Sun, 15 Jan 2023 14:52:37 +0300 Subject: feat(tvix/eval): implement builtins.toXML Change-Id: I009efc53a8e98f0650ae660c4decd8216e8a06e7 Reviewed-on: https://cl.tvl.fyi/c/depot/+/7835 Reviewed-by: flokli Tested-by: BuildkiteCI --- tvix/eval/src/builtins/mod.rs | 9 ++ tvix/eval/src/builtins/to_xml.rs | 126 +++++++++++++++++++++ tvix/eval/src/errors.rs | 22 ++++ tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp | 1 + tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix | 3 + tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp | 1 + tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix | 1 + .../nix_tests/notyetpassing/eval-okay-toxml.exp | 1 - .../nix_tests/notyetpassing/eval-okay-toxml.nix | 3 - .../nix_tests/notyetpassing/eval-okay-toxml2.exp | 1 - .../nix_tests/notyetpassing/eval-okay-toxml2.nix | 1 - 11 files changed, 163 insertions(+), 6 deletions(-) create mode 100644 tvix/eval/src/builtins/to_xml.rs create mode 100644 tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp create mode 100644 tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix create mode 100644 tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp create mode 100644 tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix delete mode 100644 tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp delete mode 100644 tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix delete mode 100644 tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp delete mode 100644 tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix (limited to 'tvix/eval/src') diff --git a/tvix/eval/src/builtins/mod.rs b/tvix/eval/src/builtins/mod.rs index b93bbd99f87a..7614353f5bbb 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.deep_force(vm, &mut Default::default())?; + let mut buf: Vec = 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 { // 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 000000000000..f2b98a7e3168 --- /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(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, "\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: &mut EventWriter, + 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: &mut EventWriter, 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 6a463d9f96df..67ef509ba9d7 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), + /// 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 for ErrorKind { } } +impl From for ErrorKind { + fn from(_: FromUtf8Error) -> Self { + Self::NotImplemented("FromUtf8Error not handled: https://b.tvl.fyi/issues/189") + } +} + +impl From 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/eval-okay-toxml.exp b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp new file mode 100644 index 000000000000..828220890ecd --- /dev/null +++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.exp @@ -0,0 +1 @@ +"\n\n \n \n \n \n \n\n" diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix new file mode 100644 index 000000000000..068c97a6c1b3 --- /dev/null +++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml.nix @@ -0,0 +1,3 @@ +# Make sure the expected XML output is produced; in particular, make sure it +# doesn't contain source location information. +builtins.toXML { a = "s"; } diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp new file mode 100644 index 000000000000..634a841eb190 --- /dev/null +++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.exp @@ -0,0 +1 @@ +"\n\n \n \n \n \n \n \n \n \n \n \n \n \n\n" diff --git a/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix new file mode 100644 index 000000000000..ff1791b30eb5 --- /dev/null +++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix @@ -0,0 +1 @@ +builtins.toXML [("a" + "b") 10 (rec {x = "x"; y = x;})] diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp deleted file mode 100644 index 828220890ecd..000000000000 --- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.exp +++ /dev/null @@ -1 +0,0 @@ -"\n\n \n \n \n \n \n\n" diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix deleted file mode 100644 index 068c97a6c1b3..000000000000 --- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml.nix +++ /dev/null @@ -1,3 +0,0 @@ -# Make sure the expected XML output is produced; in particular, make sure it -# doesn't contain source location information. -builtins.toXML { a = "s"; } diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp deleted file mode 100644 index 634a841eb190..000000000000 --- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.exp +++ /dev/null @@ -1 +0,0 @@ -"\n\n \n \n \n \n \n \n \n \n \n \n \n \n\n" diff --git a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix b/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix deleted file mode 100644 index ff1791b30eb5..000000000000 --- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix +++ /dev/null @@ -1 +0,0 @@ -builtins.toXML [("a" + "b") 10 (rec {x = "x"; y = x;})] -- cgit 1.4.1