diff options
author | Vincent Ambo <mail@tazj.in> | 2023-01-15T11·52+0300 |
---|---|---|
committer | tazjin <tazjin@tvl.su> | 2023-01-16T13·43+0000 |
commit | d365b092262013e074f0fe800a1955032eaa2fd9 (patch) | |
tree | d5c32fecf48fdcc2ec9697accd2be69c1c977a1a /tvix/eval | |
parent | 1786b4c835cbce619ddae77ffeebe89b24d50c0e (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/eval')
-rw-r--r-- | tvix/eval/Cargo.toml | 1 | ||||
-rw-r--r-- | tvix/eval/src/builtins/mod.rs | 9 | ||||
-rw-r--r-- | tvix/eval/src/builtins/to_xml.rs | 126 | ||||
-rw-r--r-- | tvix/eval/src/errors.rs | 22 | ||||
-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 |
8 files changed, 158 insertions, 0 deletions
diff --git a/tvix/eval/Cargo.toml b/tvix/eval/Cargo.toml index 75c45c96b08d..6974110290bf 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 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, 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 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<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 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<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 828220890ecd..828220890ecd 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 068c97a6c1b3..068c97a6c1b3 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 634a841eb190..634a841eb190 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 ff1791b30eb5..ff1791b30eb5 100644 --- a/tvix/eval/src/tests/nix_tests/notyetpassing/eval-okay-toxml2.nix +++ b/tvix/eval/src/tests/nix_tests/eval-okay-toxml2.nix |