diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 434ba393d..9ccf24c5c 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -19,11 +19,11 @@ jobs: echo "##[add-path]$HOME/.cargo/bin" if: matrix.os == 'macos-latest' - run: cargo fetch - working-directory: tools/witx + working-directory: tools - run: cargo build - working-directory: tools/witx + working-directory: tools - run: cargo test - working-directory: tools/witx + working-directory: tools rustfmt: name: Rustfmt @@ -33,4 +33,4 @@ jobs: - name: Install Rust run: rustup update stable && rustup default stable && rustup component add rustfmt - run: cargo fmt -- --check - working-directory: tools/witx + working-directory: tools diff --git a/tools/.gitignore b/tools/.gitignore new file mode 100644 index 000000000..fa8d85ac5 --- /dev/null +++ b/tools/.gitignore @@ -0,0 +1,2 @@ +Cargo.lock +target diff --git a/tools/Cargo.toml b/tools/Cargo.toml new file mode 100644 index 000000000..ad508d6d7 --- /dev/null +++ b/tools/Cargo.toml @@ -0,0 +1,5 @@ +[workspace] +members = [ + "witx", + "wasi-spec", +] diff --git a/tools/wasi-spec/.gitignore b/tools/wasi-spec/.gitignore new file mode 100644 index 000000000..a9d37c560 --- /dev/null +++ b/tools/wasi-spec/.gitignore @@ -0,0 +1,2 @@ +target +Cargo.lock diff --git a/tools/wasi-spec/Cargo.toml b/tools/wasi-spec/Cargo.toml new file mode 100644 index 000000000..2d3976b71 --- /dev/null +++ b/tools/wasi-spec/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "wasi-spec" +version = "0.1.0" +description = "Describe WASI interface using witx" +homepage = "https://github.com/WebAssembly/WASI" +repository = "https://github.com/WebAssembly/WASI" +license = "Apache-2.0" +categories = ["wasm"] +authors = ["Pat Hickey "] +edition = "2018" +build = "build.rs" + +[lib] +crate-type=["rlib"] + +[dependencies] +witx = { path = "../witx", version = "0.1.0" } + +[build-dependencies] +witx = { path = "../witx", version = "0.1.0" } diff --git a/tools/wasi-spec/build.rs b/tools/wasi-spec/build.rs new file mode 100644 index 000000000..73e4ba990 --- /dev/null +++ b/tools/wasi-spec/build.rs @@ -0,0 +1,33 @@ +use std::env; +use std::fs::File; +use std::io::Write; +use std::path::Path; +use witx::{load, Document, WitxError}; + +const WASI_VERSIONS: &[(&str, &str)] = &[("unstable", "preview0")]; + +fn load_wasi_spec(phase: &str, version: &str) -> Result { + let path = format!( + "../../phases/{phase}/witx/wasi_{phase}_{version}.witx", + phase = phase, + version = version, + ); + println!("cargo:rerun-if-changed={}", path); + load(Path::new(&path)) +} + +fn serialize_wasi_spec(phase: &str, version: &str, doc: &Document) { + let out_dir = env::var("OUT_DIR").expect("cargo outdir available"); + let witx_path = Path::new(&out_dir).join(format!("wasi_{}_{}.witx", phase, version)); + let mut f = File::create(witx_path).expect("create file"); + f.write_all(format!("{}", doc).as_bytes()) + .expect("write data"); +} + +fn main() { + for (phase, version) in WASI_VERSIONS { + let doc = + load_wasi_spec(phase, version).expect(&format!("load wasi {} {}", phase, version)); + serialize_wasi_spec(phase, version, &doc); + } +} diff --git a/tools/wasi-spec/src/lib.rs b/tools/wasi-spec/src/lib.rs new file mode 100644 index 000000000..1bb5c859e --- /dev/null +++ b/tools/wasi-spec/src/lib.rs @@ -0,0 +1,19 @@ +pub mod unstable { + use witx; + pub fn preview0() -> witx::Document { + witx::parse(include_str!(concat!( + env!("OUT_DIR"), + "/wasi_unstable_preview0.witx" + ))) + .expect("parses") + } + + #[cfg(test)] + #[test] + fn preview0_works() { + let packaged = preview0(); + let canon = witx::load("../../phases/unstable/witx/wasi_unstable_preview0.witx") + .expect("load canonical"); + assert_eq!(packaged, canon); + } +} diff --git a/tools/witx/src/ast.rs b/tools/witx/src/ast.rs index 4df4d1e24..3d2fbb9c2 100644 --- a/tools/witx/src/ast.rs +++ b/tools/witx/src/ast.rs @@ -18,10 +18,71 @@ impl Id { #[derive(Debug, Clone)] pub struct Document { - pub definitions: Vec, - pub entries: HashMap, + definitions: Vec, + entries: HashMap, } +impl Document { + pub(crate) fn new(definitions: Vec, entries: HashMap) -> Self { + Document { + definitions, + entries, + } + } + pub fn datatype(&self, name: &Id) -> Option> { + self.entries.get(name).and_then(|e| match e { + Entry::Datatype(d) => Some(d.upgrade().expect("always possible to upgrade entry")), + _ => None, + }) + } + pub fn datatypes<'a>(&'a self) -> impl Iterator> + 'a { + self.definitions.iter().filter_map(|d| match d { + Definition::Datatype(d) => Some(d.clone()), + _ => None, + }) + } + pub fn module(&self, name: &Id) -> Option> { + self.entries.get(&name).and_then(|e| match e { + Entry::Module(d) => Some(d.upgrade().expect("always possible to upgrade entry")), + _ => None, + }) + } + pub fn modules<'a>(&'a self) -> impl Iterator> + 'a { + self.definitions.iter().filter_map(|d| match d { + Definition::Module(d) => Some(d.clone()), + _ => None, + }) + } +} + +impl PartialEq for Document { + fn eq(&self, rhs: &Document) -> bool { + if self.definitions.len() != rhs.definitions.len() { + return false; + } + for d in self.datatypes() { + if let Some(d_rhs) = rhs.datatype(&d.name) { + if d != d_rhs { + return false; + } + } else { + return false; + } + } + for m in self.modules() { + if let Some(m_rhs) = rhs.module(&m.name) { + if m != m_rhs { + return false; + } + } else { + return false; + } + } + true + } +} +impl Eq for Document {} + #[derive(Debug, Clone)] pub enum Definition { Datatype(Rc), @@ -43,7 +104,7 @@ impl Entry { } } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DatatypeIdent { Builtin(BuiltinType), Array(Box), @@ -52,13 +113,13 @@ pub enum DatatypeIdent { Ident(Rc), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Datatype { pub name: Id, pub variant: DatatypeVariant, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DatatypeVariant { Alias(AliasDatatype), Enum(EnumDatatype), @@ -67,13 +128,13 @@ pub enum DatatypeVariant { Union(UnionDatatype), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct AliasDatatype { pub name: Id, pub to: DatatypeIdent, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum IntRepr { I8, I16, @@ -81,39 +142,39 @@ pub enum IntRepr { I64, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct EnumDatatype { pub name: Id, pub repr: IntRepr, pub variants: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct FlagsDatatype { pub name: Id, pub repr: IntRepr, pub flags: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StructDatatype { pub name: Id, pub members: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StructMember { pub name: Id, pub type_: DatatypeIdent, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct UnionDatatype { pub name: Id, pub variants: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct UnionVariant { pub name: Id, pub type_: DatatypeIdent, @@ -122,10 +183,76 @@ pub struct UnionVariant { #[derive(Debug, Clone)] pub struct Module { pub name: Id, - pub definitions: Vec, - pub entries: HashMap, + definitions: Vec, + entries: HashMap, +} + +impl Module { + pub(crate) fn new( + name: Id, + definitions: Vec, + entries: HashMap, + ) -> Self { + Module { + name, + definitions, + entries, + } + } + pub fn import(&self, name: &Id) -> Option> { + self.entries.get(name).and_then(|e| match e { + ModuleEntry::Import(d) => Some(d.upgrade().expect("always possible to upgrade entry")), + _ => None, + }) + } + pub fn imports<'a>(&'a self) -> impl Iterator> + 'a { + self.definitions.iter().filter_map(|d| match d { + ModuleDefinition::Import(d) => Some(d.clone()), + _ => None, + }) + } + pub fn func(&self, name: &Id) -> Option> { + self.entries.get(name).and_then(|e| match e { + ModuleEntry::Func(d) => Some(d.upgrade().expect("always possible to upgrade entry")), + _ => None, + }) + } + pub fn funcs<'a>(&'a self) -> impl Iterator> + 'a { + self.definitions.iter().filter_map(|d| match d { + ModuleDefinition::Func(d) => Some(d.clone()), + _ => None, + }) + } } +impl PartialEq for Module { + fn eq(&self, rhs: &Module) -> bool { + if self.definitions.len() != rhs.definitions.len() { + return false; + } + for i in self.imports() { + if let Some(i_rhs) = rhs.import(&i.name) { + if i != i_rhs { + return false; + } + } else { + return false; + } + } + for f in self.funcs() { + if let Some(f_rhs) = rhs.func(&f.name) { + if f != f_rhs { + return false; + } + } else { + return false; + } + } + true + } +} +impl Eq for Module {} + #[derive(Debug, Clone)] pub enum ModuleDefinition { Import(Rc), @@ -138,25 +265,25 @@ pub enum ModuleEntry { Func(Weak), } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct ModuleImport { pub name: Id, pub variant: ModuleImportVariant, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ModuleImportVariant { Memory, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceFunc { pub name: Id, pub params: Vec, pub results: Vec, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct InterfaceFuncParam { pub name: Id, pub type_: DatatypeIdent, diff --git a/tools/witx/src/io.rs b/tools/witx/src/io.rs new file mode 100644 index 000000000..2ff2aef62 --- /dev/null +++ b/tools/witx/src/io.rs @@ -0,0 +1,88 @@ +use crate::WitxError; +use std::collections::HashMap; +use std::fs::{read_to_string, File}; +use std::io::{BufRead, BufReader, Error, ErrorKind}; +use std::path::{Path, PathBuf}; + +pub trait WitxIo { + fn fgets(&self, path: &Path) -> Result; + fn fget_line(&self, path: &Path, line_num: usize) -> Result; + fn canonicalize(&self, path: &Path) -> Result; +} + +pub struct Filesystem; + +impl WitxIo for Filesystem { + fn fgets(&self, path: &Path) -> Result { + read_to_string(path).map_err(|e| WitxError::Io(path.to_path_buf(), e)) + } + fn fget_line(&self, path: &Path, line_num: usize) -> Result { + let f = File::open(path).map_err(|e| WitxError::Io(path.into(), e))?; + let buf = BufReader::new(f); + let l = buf + .lines() + .skip(line_num - 1) + .next() + .ok_or_else(|| { + WitxError::Io(path.into(), Error::new(ErrorKind::Other, "Line not found")) + })? + .map_err(|e| WitxError::Io(path.into(), e))?; + + Ok(l) + } + fn canonicalize(&self, path: &Path) -> Result { + path.canonicalize() + .map_err(|e| WitxError::Io(path.to_path_buf(), e)) + } +} + +pub struct MockFs { + map: HashMap, +} + +impl MockFs { + pub fn new(strings: &[(&str, &str)]) -> Self { + MockFs { + map: strings + .iter() + .map(|(k, v)| (k.to_string(), v.to_string())) + .collect(), + } + } +} + +impl WitxIo for MockFs { + fn fgets(&self, path: &Path) -> Result { + if let Some(entry) = self.map.get(path.to_str().unwrap()) { + Ok(entry.to_string()) + } else { + Err(WitxError::Io( + path.to_path_buf(), + Error::new(ErrorKind::Other, "mock fs: file not found"), + )) + } + } + fn fget_line(&self, path: &Path, line: usize) -> Result { + if let Some(entry) = self.map.get(path.to_str().unwrap()) { + entry + .lines() + .skip(line - 1) + .next() + .map(|s| s.to_string()) + .ok_or_else(|| { + WitxError::Io( + path.to_path_buf(), + Error::new(ErrorKind::Other, "mock fs: file not found"), + ) + }) + } else { + Err(WitxError::Io( + path.to_path_buf(), + Error::new(ErrorKind::Other, "mock fs: file not found"), + )) + } + } + fn canonicalize(&self, path: &Path) -> Result { + Ok(PathBuf::from(path)) + } +} diff --git a/tools/witx/src/lib.rs b/tools/witx/src/lib.rs index 7d5040d00..0cac5d6b3 100644 --- a/tools/witx/src/lib.rs +++ b/tools/witx/src/lib.rs @@ -1,9 +1,13 @@ /// Types describing a validated witx document mod ast; +/// Interface for filesystem or mock IO +mod io; /// Lexer text into tokens mod lexer; /// Witx syntax parsing from SExprs mod parser; +/// Render ast to text +mod render; /// SExpr parsing from tokens mod sexpr; /// Resolve toplevel `use` declarations across files @@ -17,13 +21,14 @@ pub use ast::{ ModuleDefinition, ModuleEntry, ModuleImport, ModuleImportVariant, StructDatatype, StructMember, UnionDatatype, UnionVariant, }; +pub use io::{Filesystem, MockFs, WitxIo}; pub use lexer::LexError; pub use parser::{DeclSyntax, ParseError}; +pub use render::{Render, SExpr as RenderSExpr}; pub use sexpr::SExprParseError; pub use validate::ValidationError; use failure::Fail; -use std::io; use std::path::{Path, PathBuf}; pub fn load>(path: P) -> Result { @@ -33,6 +38,14 @@ pub fn load>(path: P) -> Result { validate_document(&parsed_decls).map_err(WitxError::Validation) } +pub fn parse(source: &str) -> Result { + use toplevel::parse_witx_with; + use validate::validate_document; + let mockfs = MockFs::new(&[("-", source)]); + let parsed_decls = parse_witx_with(Path::new("-"), &mockfs)?; + validate_document(&parsed_decls).map_err(WitxError::Validation) +} + /// Location in the source text #[derive(Debug, PartialEq, Eq, Clone)] pub struct Location { @@ -45,8 +58,8 @@ pub struct Location { pub enum WitxError { #[fail(display = "{}", _0)] SExpr(#[cause] SExprParseError), - #[fail(display = "when resolving use declaration for {:?}: {}", _0, _1)] - UseResolution(PathBuf, #[cause] io::Error), + #[fail(display = "with file {:?}: {}", _0, _1)] + Io(PathBuf, #[cause] ::std::io::Error), #[fail(display = "{}", _0)] Parse(#[cause] ParseError), #[fail(display = "{}", _0)] @@ -54,20 +67,24 @@ pub enum WitxError { } impl WitxError { - pub fn report(&self) -> String { + pub fn report_with(&self, witxio: &dyn WitxIo) -> String { use WitxError::*; match self { - SExpr(sexpr) => sexpr.report(), - UseResolution(path, ioerr) => format!("when resolving `use {:?}`: {}", path, ioerr), - Parse(parse) => parse.report(), - Validation(validation) => validation.report(), + SExpr(sexpr) => sexpr.report_with(witxio), + Io(path, ioerr) => format!("with file {:?}: {}", path, ioerr), + Parse(parse) => parse.report_with(witxio), + Validation(validation) => validation.report_with(witxio), } } + pub fn report(&self) -> String { + self.report_with(&Filesystem) + } } + impl Location { - pub fn highlight_source(&self) -> String { + pub fn highlight_source_with(&self, witxio: &dyn WitxIo) -> String { let mut msg = format!("in {:?}:\n", self.path); - if let Ok(src_line) = self.source_line() { + if let Ok(src_line) = witxio.fget_line(&self.path, self.line) { msg += &format!( "{line_num: >5} | {src_line}\n{blank: >5} {caret: >column$}", line_num = self.line, @@ -79,15 +96,7 @@ impl Location { } msg } - pub fn source_line(&self) -> Result { - use std::fs::File; - use std::io::{BufRead, BufReader}; - let f = BufReader::new(File::open(&self.path)?); - let l = f - .lines() - .skip(self.line - 1) - .next() - .unwrap_or_else(|| Err(io::Error::new(io::ErrorKind::Other, "TODO")))?; - Ok(l) + pub fn highlight_source(&self) -> String { + self.highlight_source_with(&Filesystem) } } diff --git a/tools/witx/src/parser.rs b/tools/witx/src/parser.rs index a99df5fa4..1f627b27c 100644 --- a/tools/witx/src/parser.rs +++ b/tools/witx/src/parser.rs @@ -1,3 +1,4 @@ +use crate::io::{Filesystem, WitxIo}; use crate::sexpr::SExpr; use crate::Location; use failure::Fail; @@ -23,8 +24,15 @@ pub struct ParseError { } impl ParseError { + pub fn report_with(&self, witxio: &dyn WitxIo) -> String { + format!( + "{}\n{}", + self.location.highlight_source_with(witxio), + self.message + ) + } pub fn report(&self) -> String { - format!("{}\n{}", self.location.highlight_source(), self.message) + self.report_with(&Filesystem) } } diff --git a/tools/witx/src/render.rs b/tools/witx/src/render.rs new file mode 100644 index 000000000..3a2ece505 --- /dev/null +++ b/tools/witx/src/render.rs @@ -0,0 +1,266 @@ +use crate::ast::*; +use std::fmt; + +impl fmt::Display for Document { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + for d in self.datatypes() { + write!(f, "{}\n", d.to_sexpr())?; + } + for m in self.modules() { + write!(f, "{}\n", m.to_sexpr())?; + } + Ok(()) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SExpr { + Vec(Vec), + Word(String), + Ident(String), + Quote(String), + /// Short for Annotation + Annot(String), +} + +impl fmt::Display for SExpr { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + match self { + SExpr::Vec(vs) => { + write!(f, "(")?; + let mut vss = Vec::new(); + for v in vs { + vss.push(format!("{}", v)); + } + f.write_str(&vss.join(" "))?; + write!(f, ")") + } + SExpr::Word(w) => write!(f, "{}", w), + SExpr::Ident(i) => write!(f, "${}", i), + SExpr::Quote(q) => write!(f, "\"{}\"", q), + SExpr::Annot(a) => write!(f, "@{}", a), + } + } +} + +impl SExpr { + fn word(s: &str) -> SExpr { + SExpr::Word(s.to_string()) + } + fn ident(s: &str) -> SExpr { + SExpr::Ident(s.to_string()) + } + fn quote(s: &str) -> SExpr { + SExpr::Quote(s.to_string()) + } + fn annot(s: &str) -> SExpr { + SExpr::Annot(s.to_string()) + } +} + +pub trait Render { + fn to_sexpr(&self) -> SExpr; +} + +impl Render for Id { + fn to_sexpr(&self) -> SExpr { + SExpr::ident(self.as_str()) + } +} + +impl Render for BuiltinType { + fn to_sexpr(&self) -> SExpr { + match self { + BuiltinType::String => SExpr::word("string"), + BuiltinType::Data => SExpr::word("data"), + BuiltinType::U8 => SExpr::word("u8"), + BuiltinType::U16 => SExpr::word("u16"), + BuiltinType::U32 => SExpr::word("u32"), + BuiltinType::U64 => SExpr::word("u64"), + BuiltinType::S8 => SExpr::word("s8"), + BuiltinType::S16 => SExpr::word("s16"), + BuiltinType::S32 => SExpr::word("s32"), + BuiltinType::S64 => SExpr::word("s64"), + BuiltinType::F32 => SExpr::word("f32"), + BuiltinType::F64 => SExpr::word("f64"), + } + } +} + +impl Render for DatatypeIdent { + fn to_sexpr(&self) -> SExpr { + match self { + DatatypeIdent::Builtin(b) => b.to_sexpr(), + DatatypeIdent::Array(a) => SExpr::Vec(vec![SExpr::word("array"), a.to_sexpr()]), + DatatypeIdent::Pointer(p) => SExpr::Vec(vec![ + SExpr::annot("witx"), + SExpr::word("pointer"), + p.to_sexpr(), + ]), + DatatypeIdent::ConstPointer(p) => SExpr::Vec(vec![ + SExpr::annot("witx"), + SExpr::word("const_pointer"), + p.to_sexpr(), + ]), + DatatypeIdent::Ident(i) => i.name.to_sexpr(), + } + } +} + +impl Render for Datatype { + fn to_sexpr(&self) -> SExpr { + let name = self.name.to_sexpr(); + let body = self.variant.to_sexpr(); + SExpr::Vec(vec![SExpr::word("typename"), name, body]) + } +} + +impl Render for DatatypeVariant { + fn to_sexpr(&self) -> SExpr { + match self { + DatatypeVariant::Alias(a) => a.to_sexpr(), + DatatypeVariant::Enum(a) => a.to_sexpr(), + DatatypeVariant::Flags(a) => a.to_sexpr(), + DatatypeVariant::Struct(a) => a.to_sexpr(), + DatatypeVariant::Union(a) => a.to_sexpr(), + } + } +} + +impl Render for AliasDatatype { + fn to_sexpr(&self) -> SExpr { + self.to.to_sexpr() + } +} + +impl Render for EnumDatatype { + fn to_sexpr(&self) -> SExpr { + let header = vec![SExpr::word("enum"), self.repr.to_sexpr()]; + let variants = self + .variants + .iter() + .map(|v| v.to_sexpr()) + .collect::>(); + SExpr::Vec([header, variants].concat()) + } +} + +impl Render for FlagsDatatype { + fn to_sexpr(&self) -> SExpr { + let header = vec![SExpr::word("flags"), self.repr.to_sexpr()]; + let flags = self + .flags + .iter() + .map(|f| SExpr::Vec(vec![SExpr::word("flag"), f.to_sexpr()])) + .collect::>(); + SExpr::Vec([header, flags].concat()) + } +} + +impl Render for StructDatatype { + fn to_sexpr(&self) -> SExpr { + let header = vec![SExpr::word("struct")]; + let members = self + .members + .iter() + .map(|m| { + SExpr::Vec(vec![ + SExpr::word("field"), + m.name.to_sexpr(), + m.type_.to_sexpr(), + ]) + }) + .collect::>(); + SExpr::Vec([header, members].concat()) + } +} + +impl Render for UnionDatatype { + fn to_sexpr(&self) -> SExpr { + let header = vec![SExpr::word("union")]; + let variants = self + .variants + .iter() + .map(|v| { + SExpr::Vec(vec![ + SExpr::word("field"), + v.name.to_sexpr(), + v.type_.to_sexpr(), + ]) + }) + .collect::>(); + SExpr::Vec([header, variants].concat()) + } +} + +impl Render for IntRepr { + fn to_sexpr(&self) -> SExpr { + match self { + IntRepr::I8 => SExpr::word("u8"), + IntRepr::I16 => SExpr::word("u16"), + IntRepr::I32 => SExpr::word("u32"), + IntRepr::I64 => SExpr::word("u64"), + } + } +} + +impl Render for Module { + fn to_sexpr(&self) -> SExpr { + let header = vec![SExpr::word("module"), self.name.to_sexpr()]; + let definitions = self + .imports() + .map(|i| i.to_sexpr()) + .chain(self.funcs().map(|f| f.to_sexpr())) + .collect::>(); + SExpr::Vec([header, definitions].concat()) + } +} + +impl Render for ModuleImport { + fn to_sexpr(&self) -> SExpr { + let variant = match self.variant { + ModuleImportVariant::Memory => SExpr::Vec(vec![SExpr::word("memory")]), + }; + SExpr::Vec(vec![ + SExpr::word("import"), + SExpr::quote(self.name.as_str()), + variant, + ]) + } +} + +impl Render for InterfaceFunc { + fn to_sexpr(&self) -> SExpr { + let header = vec![ + SExpr::annot("interface"), + SExpr::word("func"), + SExpr::Vec(vec![ + SExpr::word("export"), + SExpr::quote(self.name.as_str()), + ]), + ]; + let params = self + .params + .iter() + .map(|f| { + SExpr::Vec(vec![ + SExpr::word("param"), + f.name.to_sexpr(), + f.type_.to_sexpr(), + ]) + }) + .collect(); + let results = self + .results + .iter() + .map(|f| { + SExpr::Vec(vec![ + SExpr::word("result"), + f.name.to_sexpr(), + f.type_.to_sexpr(), + ]) + }) + .collect(); + SExpr::Vec([header, params, results].concat()) + } +} diff --git a/tools/witx/src/sexpr.rs b/tools/witx/src/sexpr.rs index 6a34cf83b..d87d427e3 100644 --- a/tools/witx/src/sexpr.rs +++ b/tools/witx/src/sexpr.rs @@ -1,3 +1,4 @@ +use crate::io::{Filesystem, WitxIo}; pub use crate::lexer::LexError; use crate::lexer::{Lexer, LocatedError, LocatedToken, Token}; use crate::Location; @@ -41,14 +42,17 @@ pub enum SExprParseError { } impl SExprParseError { - pub fn report(&self) -> String { + pub fn report_with(&self, witxio: &dyn WitxIo) -> String { use SExprParseError::*; match self { - Lex(lex_err, loc) => format!("{}\n{}", loc.highlight_source(), lex_err), - UnexpectedCloseParen(loc) => format!("{}\n{}", loc.highlight_source(), self), + Lex(lex_err, loc) => format!("{}\n{}", loc.highlight_source_with(witxio), lex_err), + UnexpectedCloseParen(loc) => format!("{}\n{}", loc.highlight_source_with(witxio), self), UnexpectedEof(_path) => format!("{}", self), } } + pub fn report(&self) -> String { + self.report_with(&Filesystem) + } } pub struct SExprParser<'a> { diff --git a/tools/witx/src/toplevel.rs b/tools/witx/src/toplevel.rs index cf3a5e377..98325b39c 100644 --- a/tools/witx/src/toplevel.rs +++ b/tools/witx/src/toplevel.rs @@ -1,32 +1,15 @@ +use crate::io::{Filesystem, WitxIo}; use crate::parser::{DeclSyntax, ParseError, TopLevelSyntax}; use crate::sexpr::SExprParser; use crate::WitxError; use std::collections::HashSet; -use std::fs; use std::path::{Path, PathBuf}; -trait WitxIo { - fn fgets(&self, path: &Path) -> Result; - fn canonicalize(&self, path: &Path) -> Result; -} - -struct Filesystem; - -impl WitxIo for Filesystem { - fn fgets(&self, path: &Path) -> Result { - fs::read_to_string(path).map_err(|e| WitxError::UseResolution(path.to_path_buf(), e)) - } - fn canonicalize(&self, path: &Path) -> Result { - path.canonicalize() - .map_err(|e| WitxError::UseResolution(path.to_path_buf(), e)) - } -} - pub fn parse_witx>(i: P) -> Result, WitxError> { parse_witx_with(i, &Filesystem) } -fn parse_witx_with>( +pub fn parse_witx_with>( i: P, witxio: &dyn WitxIo, ) -> Result, WitxError> { @@ -86,44 +69,14 @@ fn resolve_uses( #[cfg(test)] mod test { use super::*; + use crate::io::MockFs; use crate::parser::*; use crate::Location; - use std::collections::HashMap; - - struct MockFs { - map: HashMap<&'static str, &'static str>, - } - - impl MockFs { - pub fn new(strings: Vec<(&'static str, &'static str)>) -> Self { - MockFs { - map: strings.into_iter().collect(), - } - } - } - - impl WitxIo for MockFs { - fn fgets(&self, path: &Path) -> Result { - if let Some(entry) = self.map.get(path.to_str().unwrap()) { - Ok(entry.to_string()) - } else { - use std::io::{Error, ErrorKind}; - Err(WitxError::UseResolution( - path.to_path_buf(), - Error::new(ErrorKind::Other, "mock fs: file not found"), - )) - } - } - fn canonicalize(&self, path: &Path) -> Result { - Ok(PathBuf::from(path)) - } - } #[test] fn empty() { assert_eq!( - parse_witx_with(&Path::new("/a"), &MockFs::new(vec![("/a", ";; empty")])) - .expect("parse"), + parse_witx_with(&Path::new("/a"), &MockFs::new(&[("/a", ";; empty")])).expect("parse"), Vec::new(), ); } @@ -133,7 +86,7 @@ mod test { assert_eq!( parse_witx_with( &Path::new("/a"), - &MockFs::new(vec![("/a", "(use \"b\")"), ("/b", ";; empty")]) + &MockFs::new(&[("/a", "(use \"b\")"), ("/b", ";; empty")]) ) .expect("parse"), Vec::new(), @@ -145,7 +98,7 @@ mod test { assert_eq!( parse_witx_with( &Path::new("/a"), - &MockFs::new(vec![ + &MockFs::new(&[ ("/a", "(use \"b\")"), ("/b", "(use \"c\")\n(typename $b_float f64)"), ("/c", "(typename $c_int u32)") @@ -184,7 +137,7 @@ mod test { assert_eq!( parse_witx_with( &Path::new("/a"), - &MockFs::new(vec![ + &MockFs::new(&[ ("/a", "(use \"b\")\n(use \"c\")"), ("/b", "(use \"d\")"), ("/c", "(use \"d\")"), @@ -208,23 +161,20 @@ mod test { #[test] fn use_not_found() { - match parse_witx_with(&Path::new("/a"), &MockFs::new(vec![("/a", "(use \"b\")")])) + match parse_witx_with(&Path::new("/a"), &MockFs::new(&[("/a", "(use \"b\")")])) .err() .unwrap() { - WitxError::UseResolution(path, _error) => assert_eq!(path, PathBuf::from("/b")), + WitxError::Io(path, _error) => assert_eq!(path, PathBuf::from("/b")), e => panic!("wrong error: {:?}", e), } } #[test] fn use_invalid() { - match parse_witx_with( - &Path::new("/a"), - &MockFs::new(vec![("/a", "(use bbbbbbb)")]), - ) - .err() - .unwrap() + match parse_witx_with(&Path::new("/a"), &MockFs::new(&[("/a", "(use bbbbbbb)")])) + .err() + .unwrap() { WitxError::Parse(e) => { assert_eq!(e.message, "invalid use declaration"); diff --git a/tools/witx/src/validate.rs b/tools/witx/src/validate.rs index 7041fca98..64e4ecca9 100644 --- a/tools/witx/src/validate.rs +++ b/tools/witx/src/validate.rs @@ -1,4 +1,5 @@ use crate::{ + io::{Filesystem, WitxIo}, parser::{ DatatypeIdentSyntax, DeclSyntax, EnumSyntax, FlagsSyntax, IdentSyntax, ImportTypeSyntax, ModuleDeclSyntax, StructSyntax, TypedefSyntax, UnionSyntax, @@ -42,25 +43,30 @@ pub enum ValidationError { } impl ValidationError { - pub fn report(&self) -> String { + pub fn report_with(&self, witxio: &dyn WitxIo) -> String { use ValidationError::*; match self { UnknownName { location, .. } | WrongKindName { location, .. } | Recursive { location, .. } - | InvalidRepr { location, .. } => format!("{}\n{}", location.highlight_source(), &self), + | InvalidRepr { location, .. } => { + format!("{}\n{}", location.highlight_source_with(witxio), &self) + } NameAlreadyExists { at_location, previous_location, .. } => format!( "{}\n{}\nOriginally defined at:\n{}", - at_location.highlight_source(), + at_location.highlight_source_with(witxio), &self, - previous_location.highlight_source(), + previous_location.highlight_source_with(witxio), ), } } + pub fn report(&self) -> String { + self.report_with(&Filesystem) + } } pub fn validate_document(decls: &[DeclSyntax]) -> Result { @@ -70,10 +76,7 @@ pub fn validate_document(decls: &[DeclSyntax]) -> Result, _>>()?; - let rc_module = Rc::new(Module { - name: name.clone(), + let rc_module = Rc::new(Module::new( + name.clone(), definitions, - entries: module_validator.entries, - }); + module_validator.entries, + )); self.entries .insert(name, Entry::Module(Rc::downgrade(&rc_module))); Ok(Definition::Module(rc_module)) diff --git a/tools/witx/tests/wasi_unstable.rs b/tools/witx/tests/wasi_unstable.rs index 6e196cb4a..2722e8d04 100644 --- a/tools/witx/tests/wasi_unstable.rs +++ b/tools/witx/tests/wasi_unstable.rs @@ -21,3 +21,19 @@ fn validate_wasi_ephemeral_preview0() { fn validate_wasi_old_preview0() { witx::load(Path::new("../../phases/old/witx/wasi_unstable.witx")).unwrap(); } + +#[test] +fn render_roundtrip() { + let doc = witx::load(Path::new( + "../../phases/unstable/witx/wasi_unstable_preview0.witx", + )) + .unwrap(); + + let back_to_sexprs = format!("{}", doc); + println!("{}", back_to_sexprs); + let doc2 = witx::parse(&back_to_sexprs) + .map_err(|e| e.report_with(&witx::MockFs::new(&[("-", &back_to_sexprs)]))) + .unwrap(); + + assert_eq!(doc, doc2); +}