diff --git a/phases/ephemeral/docs.md b/phases/ephemeral/docs.md index 2cd2d1361..a3c0d0a47 100644 --- a/phases/ephemeral/docs.md +++ b/phases/ephemeral/docs.md @@ -871,12 +871,12 @@ Alignment: 8 - align: 8 - tag_size: 1 ### Variant cases +- `clock` + - `fd_read`: [`event_fd_readwrite`](#event_fd_readwrite) - `fd_write`: [`event_fd_readwrite`](#event_fd_readwrite) -- `clock` - ## `event`: Record An event that occurred. diff --git a/tools/witx/Cargo.toml b/tools/witx/Cargo.toml index 34c074d68..bf9ce16cd 100644 --- a/tools/witx/Cargo.toml +++ b/tools/witx/Cargo.toml @@ -23,3 +23,8 @@ wast = { version = "22.0.0", default-features = false } diff = "0.1.11" pretty_env_logger = "0.4" structopt = "0.3" +rayon = "1.0" + +[[test]] +name = "witxt" +harness = false diff --git a/tools/witx/src/lib.rs b/tools/witx/src/lib.rs index 9c0f53060..2c876aa13 100644 --- a/tools/witx/src/lib.rs +++ b/tools/witx/src/lib.rs @@ -9,7 +9,7 @@ mod io; /// Calculate memory layout of types mod layout; /// Witx syntax parsing from SExprs -mod parser; +pub mod parser; /// Paths to witx documents for various proposal phases pub mod phases; /// Calculate required polyfill between interfaces @@ -28,10 +28,9 @@ pub use coretypes::{AtomType, CoreFuncType, CoreParamSignifies, CoreParamType, T pub use docs::Documentation; pub use io::{Filesystem, MockFs, WitxIo}; pub use layout::{Layout, RecordMemberLayout, SizeAlign}; -pub use parser::DeclSyntax; pub use render::SExpr; pub use representation::{RepEquality, Representable}; -pub use validate::ValidationError; +pub use validate::{DocValidation, ValidationError}; use std::path::{Path, PathBuf}; use thiserror::Error; diff --git a/tools/witx/src/parser.rs b/tools/witx/src/parser.rs index 325eb1d35..19d1a0303 100644 --- a/tools/witx/src/parser.rs +++ b/tools/witx/src/parser.rs @@ -477,7 +477,6 @@ impl<'a> Parse<'a> for UnionSyntax<'a> { None }; let mut fields = Vec::new(); - fields.push(parser.parse()?); while !parser.is_empty() { fields.push(parser.parse()?); } @@ -505,7 +504,9 @@ impl<'a> Parse<'a> for VariantSyntax<'a> { }; let mut cases = Vec::new(); while !parser.is_empty() { - cases.push(parser.parens(|p| p.parse())?); + let comments = parser.parse()?; + let item = parser.parens(|p| p.parse())?; + cases.push(Documented { comments, item }); } Ok(VariantSyntax { tag, cases }) } diff --git a/tools/witx/src/representation.rs b/tools/witx/src/representation.rs index 99f73f5b6..589b64e39 100644 --- a/tools/witx/src/representation.rs +++ b/tools/witx/src/representation.rs @@ -81,12 +81,14 @@ impl Representable for Variant { let other_by_name = by .cases .iter() - .map(|c| (&c.name, c)) + .enumerate() + .map(|(i, c)| (&c.name, (c, i))) .collect::>(); // For each variant in self, must have variant of same name in by: - for v in self.cases.iter() { + for (i, v) in self.cases.iter().enumerate() { let other_ty = match other_by_name.get(&v.name) { - Some(other) => &other.tref, + Some((_, j)) if i != *j => return RepEquality::NotEq, + Some((other, _)) => &other.tref, None => return RepEquality::NotEq, }; match (&v.tref, other_ty) { @@ -157,76 +159,3 @@ impl Representable for Type { } } } - -#[cfg(test)] -mod test { - use super::*; - use crate::io::MockFs; - use crate::toplevel::parse_witx_with; - use crate::Id; - use std::rc::Rc; - - fn def_type(typename: &str, syntax: &str) -> Rc { - use std::path::Path; - let doc = parse_witx_with(&[Path::new("-")], &MockFs::new(&[("-", syntax)])) - .expect("parse witx doc"); - let t = doc.typename(&Id::new(typename)).expect("defined type"); - // Identity should always be true: - assert_eq!(t.representable(&t), RepEquality::Eq, "identity"); - t - } - - #[test] - fn different_typenames() { - let a = def_type("a", "(typename $a (flags (@witx bitflags u32) $b $c))"); - let d = def_type("d", "(typename $d (flags (@witx bitflags u32) $b $c))"); - - assert_eq!(a.representable(&d), RepEquality::Eq); - assert_eq!(d.representable(&a), RepEquality::Eq); - } - - #[test] - fn enum_() { - let base = def_type("a", "(typename $a (enum $b $c))"); - let extra_variant = def_type("a", "(typename $a (enum $b $c $d))"); - - assert_eq!(base.representable(&extra_variant), RepEquality::Superset); - assert_eq!(extra_variant.representable(&base), RepEquality::NotEq); - - let smaller_size = def_type("a", "(typename $a (enum (@witx tag u16) $b $c))"); - assert_eq!(smaller_size.representable(&base), RepEquality::Superset); - assert_eq!( - smaller_size.representable(&extra_variant), - RepEquality::Superset - ); - } - - #[test] - fn union() { - let base = def_type( - "a", - "(typename $tag (enum (@witx tag u8) $b $c)) - (typename $a (union (@witx tag $tag) u32 f32))", - ); - let extra_variant = def_type( - "a", - "(typename $tag (enum (@witx tag u8) $b $c $d)) - (typename $a (union (@witx tag $tag) u32 f32 f64))", - ); - - assert_eq!(base.representable(&extra_variant), RepEquality::Superset); - assert_eq!(extra_variant.representable(&base), RepEquality::NotEq); - - let other_ordering = def_type( - "a", - "(typename $tag (enum (@witx tag u8) $b $c)) - (typename $a (variant (@witx tag $tag) (case $c f32) (case $b u32)))", - ); - assert_eq!(base.representable(&other_ordering), RepEquality::Eq); - assert_eq!(other_ordering.representable(&base), RepEquality::Eq); - assert_eq!( - other_ordering.representable(&extra_variant), - RepEquality::Superset - ); - } -} diff --git a/tools/witx/src/toplevel.rs b/tools/witx/src/toplevel.rs index 993fc8cc4..83e940c7c 100644 --- a/tools/witx/src/toplevel.rs +++ b/tools/witx/src/toplevel.rs @@ -32,7 +32,7 @@ fn _parse_witx_with(paths: &[&Path], io: &dyn WitxIo) -> Result, + entries: HashMap, constant_scopes: HashMap, } @@ -155,6 +155,10 @@ impl DocValidation { path, } } + + pub fn into_document(self, defs: Vec) -> Document { + Document::new(defs, self.entries) + } } impl DocValidationScope<'_> { @@ -439,14 +443,16 @@ impl DocValidationScope<'_> { } } - let mut names = names.map(|names| names.into_iter().collect::>()); + let mut name_set = names + .as_ref() + .map(|names| names.iter().collect::>()); - let cases = syntax + let mut cases = syntax .cases .iter() .map(|case| { let name = Id::new(case.item.name.name()); - if let Some(names) = &mut names { + if let Some(names) = &mut name_set { if !names.remove(&name) { return Err(ValidationError::InvalidUnionField { name: name.as_str().to_string(), @@ -467,6 +473,18 @@ impl DocValidationScope<'_> { }) }) .collect::, _>>()?; + + // If we have an explicit tag with an enum then that's instructing us to + // reorder cases based on the order of the enum itself, so do that here. + if let Some(names) = names { + let name_pos = names + .iter() + .enumerate() + .map(|(i, name)| (name, i)) + .collect::>(); + cases.sort_by_key(|c| name_pos[&&c.name]); + } + Ok(Variant { tag_repr, cases }) } diff --git a/tools/witx/tests/anonymous.rs b/tools/witx/tests/anonymous.rs deleted file mode 100644 index 6cfb2ea7e..000000000 --- a/tools/witx/tests/anonymous.rs +++ /dev/null @@ -1,43 +0,0 @@ -use witx; - -fn is_anonymous_record_err(r: Result) -> bool { - match r { - Err(witx::WitxError::Validation(witx::ValidationError::AnonymousRecord { .. })) => true, - _ => false, - } -} - -#[test] -fn anonymous_types() { - let pointer_to_record = witx::parse("(typename $a (@witx pointer (record (field $b u8))))"); - assert!(is_anonymous_record_err(pointer_to_record)); - - let pointer_to_union = - witx::parse("(typename $tag (enum $b)) (typename $a (@witx pointer (union u8)))"); - assert!(is_anonymous_record_err(pointer_to_union)); - - let pointer_to_enum = witx::parse("(typename $a (@witx pointer (enum $b)))"); - assert!(is_anonymous_record_err(pointer_to_enum)); - - let pointer_to_flags = witx::parse("(typename $a (@witx pointer (flags $b)))"); - assert!(is_anonymous_record_err(pointer_to_flags)); - - let pointer_to_handle = witx::parse("(typename $a (@witx pointer (handle)))"); - assert!(is_anonymous_record_err(pointer_to_handle)); - - let pointer_to_builtin = witx::parse("(typename $a (@witx pointer u8))"); - assert!(pointer_to_builtin.is_ok()); - - let pointer_to_pointer = witx::parse("(typename $a (@witx pointer (@witx const_pointer u8)))"); - assert!(pointer_to_pointer.is_ok()); - - let record_in_record = witx::parse("(typename $a (record (field $b (record (field $c u8)))))"); - assert!(is_anonymous_record_err(record_in_record)); - - let union_in_record = - witx::parse("(typename $tag (enum $c)) (typename $a (record (field $b (union u8))))"); - assert!(is_anonymous_record_err(union_in_record)); - - let pointer_in_record = witx::parse("(typename $a (record (field $b (@witx pointer u8))))"); - assert!(pointer_in_record.is_ok()) -} diff --git a/tools/witx/tests/multimodule.rs b/tools/witx/tests/multimodule.rs deleted file mode 100644 index a6c773363..000000000 --- a/tools/witx/tests/multimodule.rs +++ /dev/null @@ -1,58 +0,0 @@ -use witx::{load, BuiltinType, Id, Type, TypeRef}; - -#[test] -fn validate_multimodule() { - // B uses A, and C uses A. - let doc = load(&[ - "tests/multimodule/type_b.witx", - "tests/multimodule/type_c.witx", - ]) - .unwrap_or_else(|e| panic!("failed to validate: {}", e)); - - //println!("{}", doc); - - // Check that the `a` both modules use is what we expect: - let type_a = doc.typename(&Id::new("a")).expect("type a exists"); - assert_eq!( - *type_a.type_(), - Type::Builtin(BuiltinType::U32 { - lang_ptr_size: false - }) - ); - - // `b` is a struct with a single member of type `a` - let type_b = doc.typename(&Id::new("b")).expect("type b exists"); - match &*type_b.type_() { - Type::Record(record) => { - assert_eq!(record.members.len(), 1); - assert_eq!( - record.members.get(0).unwrap().tref, - TypeRef::Name(type_a.clone()) - ); - } - _ => panic!("b is a struct"), - } - - // `c` is a struct with a two members of type `a` - let type_c = doc.typename(&Id::new("c")).expect("type c exists"); - match &*type_c.type_() { - Type::Record(record) => { - assert_eq!(record.members.len(), 2); - assert_eq!( - record.members.get(0).unwrap().tref, - TypeRef::Name(type_a.clone()) - ); - assert_eq!(record.members.get(1).unwrap().tref, TypeRef::Name(type_a)); - } - _ => panic!("c is a struct"), - } -} - -#[test] -fn multimodule_reject_redefinition() { - assert!(load(&[ - "tests/multimodule/type_a.witx", - "tests/multimodule/redefine_a.witx", - ]) - .is_err()) -} diff --git a/tools/witx/tests/union.rs b/tools/witx/tests/union.rs deleted file mode 100644 index 31ade263a..000000000 --- a/tools/witx/tests/union.rs +++ /dev/null @@ -1,174 +0,0 @@ -// Every worker needs a union! Time to organize -use witx::{Id, Representable}; - -#[test] -fn one_variant_union() { - let d = witx::parse( - "(typename $tag (enum $c)) - (typename $u (union (@witx tag $tag) u8))", - ); - assert!(d.is_ok()); -} - -#[test] -fn two_variant_union() { - let d1 = witx::parse( - "(typename $tag (enum $a $b)) - (typename $u (union (@witx tag $tag) u8 u16))", - ); - assert!(d1.is_ok(), "d1 is ok"); - - // Fields can come in whatever order: - let d2 = witx::parse( - "(typename $tag (enum $a $b)) - (typename $u (variant (@witx tag $tag) (case $b u16) (case $a u8)))", - ); - assert!(d2.is_ok(), "d2 is ok"); - - // These two unions should be represented the same: - let u1 = d1.unwrap().typename(&Id::new("u")).unwrap().type_(); - let u2 = d2.unwrap().typename(&Id::new("u")).unwrap().type_(); - - assert_eq!( - u1.representable(&u2), - witx::RepEquality::Eq, - "u1 can represent u2" - ); - assert_eq!( - u2.representable(&u1), - witx::RepEquality::Eq, - "u2 can represent u1" - ); - - // Tag order doesnt matter for validation - let d3 = witx::parse( - "(typename $tag (enum $b $a)) - (typename $u (union (@witx tag $tag) u16 u8))", - ); - assert!(d3.is_ok(), "d2 is ok"); -} - -#[test] -fn empty_variant_unions() { - let d1 = witx::parse( - "(typename $tag (enum $a $b)) - (typename $u (variant (@witx tag $tag) (case $a) (case $b u16)))", - ); - assert!(d1.is_ok(), "d1 is ok"); - - let d2 = witx::parse( - "(typename $tag (enum $a $b)) - (typename $u (variant (@witx tag $tag) (case $a) (case $b)))", - ); - assert!(d2.is_ok(), "d2 is ok"); -} - -#[test] -fn many_variant_unions() { - let d1 = witx::parse( - "(typename $tag (enum $a $b $c $d $e $f $g $h $i $j $k $l $m)) - (typename $u - (variant (@witx tag $tag) - (case $a u8) - (case $b u16) - (case $c u32) - (case $d u64) - (case $e s8) - (case $f s16) - (case $g s32) - (case $h s64) - (case $i f32) - (case $j f64) - (case $k (@witx usize)) - (case $l (@witx char8)) - (case $m) - ) - )", - ); - assert!(d1.is_ok(), "d1 is ok"); -} - -#[test] -fn no_tag_union() { - let d = witx::parse("(typename $u (union $tag (field $a u8) (field $b u16)))"); - assert!(d.is_err()); -} - -#[test] -fn wrong_kind_tag_union() { - let d = witx::parse( - "(typename $tag string) - (typename $u (union (@witx tag $tag) u8 u16))", - ); - let (expected, got) = wrong_kind_name_err(d).expect("wrong kind of tag"); - assert_eq!(expected, "enum or builtin"); - assert_eq!(got, "list"); -} - -#[test] -fn bad_field_unions() { - let d = witx::parse( - "(typename $tag (enum $c)) - (typename $u (variant (@witx tag $tag) (case $b u8)))", - ); - match validation_err(d) { - witx::ValidationError::InvalidUnionField { name, reason, .. } => { - assert_eq!(name, "b", "bad field name union 1"); - assert_eq!( - reason, "does not correspond to variant in tag `tag`", - "reason union 1" - ); - } - other => panic!("bad error: {}", other), - } - - let d = witx::parse( - "(typename $tag (enum $c)) - (typename $u (variant (@witx tag $tag) (case $c f32) (case $b u8)))", - ); - match validation_err(d) { - witx::ValidationError::UnionSizeMismatch { .. } => {} - other => panic!("bad error: {}", other), - } - - let d = witx::parse( - "(typename $tag (enum $c $d)) - (typename $u (variant (@witx tag $tag) (case $c f32)))", - ); - match validation_err(d) { - witx::ValidationError::UnionSizeMismatch { .. } => {} - other => panic!("bad error: {}", other), - } -} - -fn wrong_kind_name_err( - r: Result, -) -> Option<(&'static str, &'static str)> { - match r { - Err(witx::WitxError::Validation(witx::ValidationError::WrongKindName { - expected, - got, - .. - })) => Some((expected, got)), - Err(e) => { - eprintln!("expected WrongKindName ValidationError, got: {:?}", e); - None - } - Ok(_) => { - eprintln!("expected WrongKindName ValidationError: Ok(witx::Document)"); - None - } - } -} - -fn validation_err(r: Result) -> witx::ValidationError { - match r { - Err(witx::WitxError::Validation(e)) => e, - Err(e) => { - panic!("expected ValidationError, got: {:?}", e) - } - Ok(_) => { - panic!("expected ValidationError, got: Ok(witx::Document)") - } - } -} diff --git a/tools/witx/tests/wasi.rs b/tools/witx/tests/wasi-docs.rs similarity index 58% rename from tools/witx/tests/wasi.rs rename to tools/witx/tests/wasi-docs.rs index 27300a26e..6702edfbd 100644 --- a/tools/witx/tests/wasi.rs +++ b/tools/witx/tests/wasi-docs.rs @@ -2,24 +2,6 @@ use std::fs; use std::path::Path; use witx::{self, Documentation}; -#[test] -fn validate_wasi_snapshot() { - witx::load(&witx::phases::snapshot().unwrap()) - .unwrap_or_else(|e| panic!("failed to parse: {}", e)); -} - -#[test] -fn validate_wasi_ephemeral() { - witx::load(&witx::phases::ephemeral().unwrap()) - .unwrap_or_else(|e| panic!("failed to parse: {}", e)); -} - -#[test] -fn validate_wasi_old_snapshot_0() { - witx::load(&witx::phases::old::snapshot_0().unwrap()) - .unwrap_or_else(|e| panic!("failed to parse: {}", e)); -} - #[test] fn validate_docs() { for phase in &[ @@ -32,52 +14,6 @@ fn validate_docs() { } } -#[test] -fn render_roundtrip() { - let doc = witx::load(&witx::phases::snapshot().unwrap()) - .unwrap_or_else(|e| panic!("failed to parse: {}", e)); - - 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(); - - // I'd just assert_eq, but when it fails the debug print is thousands of lines long and impossible - // to figure out where they are unequal. - if doc != doc2 { - for type_ in doc.typenames() { - let type2 = doc2.typename(&type_.name).expect("doc2 missing datatype"); - assert_eq!(type_, type2); - } - for mod_ in doc.modules() { - let mod2 = doc2.module(&mod_.name).expect("doc2 missing module"); - for import in mod_.imports() { - let import2 = mod2.import(&import.name).expect("mod2 missing import"); - assert_eq!(import, import2); - } - for func in mod_.funcs() { - let func2 = mod2.func(&func.name).expect("mod2 missing func"); - assert_eq!(func, func2); - } - } - } - // This should be equivalent to the above, but just in case some code changes where it isnt: - assert_eq!(doc, doc2); -} - -#[test] -fn document_wasi_snapshot() { - use witx::Documentation; - println!( - "{}", - witx::load(&witx::phases::snapshot().unwrap()) - .unwrap_or_else(|e| panic!("failed to parse: {}", e)) - .to_md() - ); -} - fn dos2unix(s: &str) -> String { let mut t = String::new(); t.reserve(s.len()); diff --git a/tools/witx/tests/witxt.rs b/tools/witx/tests/witxt.rs new file mode 100644 index 000000000..1056947a3 --- /dev/null +++ b/tools/witx/tests/witxt.rs @@ -0,0 +1,409 @@ +//! You can run this test suite with: +//! +//! cargo test --test witxt +//! +//! An argument can be passed as well to filter, based on filename, which test +//! to run +//! +//! cargo test --test witxt foo.witxt + +use anyhow::{anyhow, bail, Context, Result}; +use rayon::prelude::*; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::str; +use std::sync::atomic::{AtomicUsize, Ordering::SeqCst}; +use wast::parser::{self, Parse, ParseBuffer, Parser}; +use witx::{Documentation, Representable}; + +fn main() { + let tests = find_tests(); + let filter = std::env::args().nth(1); + + let tests = tests + .par_iter() + .filter_map(|test| { + if let Some(filter) = &filter { + if let Some(s) = test.to_str() { + if !s.contains(filter) { + return None; + } + } + } + let contents = std::fs::read(test).unwrap(); + Some((test, contents)) + }) + .collect::>(); + + println!("running {} test files\n", tests.len()); + + let ntests = AtomicUsize::new(0); + let errors = tests + .par_iter() + .filter_map(|(test, contents)| { + WitxtRunner { + ntests: &ntests, + documents: HashMap::new(), + } + .run(test, contents) + .err() + }) + .collect::>(); + + if !errors.is_empty() { + for msg in errors.iter() { + eprintln!("{:?}", msg); + } + + panic!("{} tests failed", errors.len()) + } + + println!( + "test result: ok. {} directives passed\n", + ntests.load(SeqCst) + ); +} + +/// Recursively finds all tests in a whitelisted set of directories which we +/// then load up and test in parallel. +fn find_tests() -> Vec { + let mut tests = Vec::new(); + find_tests("tests/witxt".as_ref(), &mut tests); + tests.sort(); + return tests; + + fn find_tests(path: &Path, tests: &mut Vec) { + for f in path.read_dir().unwrap() { + let f = f.unwrap(); + if f.file_type().unwrap().is_dir() { + find_tests(&f.path(), tests); + continue; + } + + match f.path().extension().and_then(|s| s.to_str()) { + Some("witxt") => {} + _ => continue, + } + tests.push(f.path()); + } + } +} + +struct WitxtRunner<'a> { + ntests: &'a AtomicUsize, + documents: HashMap, +} + +impl WitxtRunner<'_> { + fn run(&mut self, test: &Path, contents: &[u8]) -> Result<()> { + let contents = str::from_utf8(contents)?; + macro_rules! adjust { + ($e:expr) => {{ + let mut e = wast::Error::from($e); + e.set_path(test); + e.set_text(contents); + e + }}; + } + let buf = ParseBuffer::new(contents).map_err(|e| adjust!(e))?; + let witxt = parser::parse::(&buf).map_err(|e| adjust!(e))?; + + let errors = witxt + .directives + .into_iter() + .filter_map(|directive| { + let (line, col) = directive.span().linecol_in(contents); + self.test_directive(contents, test, directive) + .with_context(|| { + format!( + "failed directive on {}:{}:{}", + test.display(), + line + 1, + col + 1 + ) + }) + .err() + }) + .collect::>(); + if errors.is_empty() { + return Ok(()); + } + let mut s = format!("{} test failures in {}:", errors.len(), test.display()); + for mut error in errors { + if let Some(err) = error.downcast_mut::() { + err.set_path(test); + err.set_text(contents); + } + s.push_str("\n\n\t--------------------------------\n\n\t"); + s.push_str(&format!("{:?}", error).replace("\n", "\n\t")); + } + bail!("{}", s) + } + + fn test_directive( + &mut self, + contents: &str, + test: &Path, + directive: WitxtDirective, + ) -> Result<()> { + self.bump_ntests(); + match directive { + WitxtDirective::Witx(witx) => { + let doc = witx.document(contents, test)?; + self.assert_roundtrip(&doc) + .context("failed to round-trip the document")?; + self.assert_md(&doc)?; + if let Some(name) = witx.id { + self.documents.insert(name.name().to_string(), doc); + } + } + WitxtDirective::AssertInvalid { witx, message, .. } => { + let err = match witx.document(contents, test) { + Ok(_) => bail!("witx was valid when it shouldn't be"), + Err(e) => format!("{:?}", anyhow::Error::from(e)), + }; + if !err.contains(message) { + bail!("expected error {:?}\nfound error {}", message, err); + } + } + WitxtDirective::AssertRepresentable { repr, t1, t2, .. } => { + let (t1m, t1t) = t1; + let (t2m, t2t) = t2; + let t1d = self + .documents + .get(t1m.name()) + .ok_or_else(|| anyhow!("no document named {:?}", t1m.name()))?; + let t2d = self + .documents + .get(t2m.name()) + .ok_or_else(|| anyhow!("no document named {:?}", t2m.name()))?; + let t1 = t1d + .typename(&witx::Id::new(t1t)) + .ok_or_else(|| anyhow!("no type named {:?}", t1t))?; + let t2 = t2d + .typename(&witx::Id::new(t2t)) + .ok_or_else(|| anyhow!("no type named {:?}", t2t))?; + match (repr, t1.type_().representable(&t2.type_())) { + (RepEquality::Eq, witx::RepEquality::Eq) + | (RepEquality::Superset, witx::RepEquality::Superset) + | (RepEquality::NotEq, witx::RepEquality::NotEq) => {} + (a, b) => { + bail!("expected {:?} representation, got {:?}", a, b); + } + } + } + } + Ok(()) + } + + fn assert_roundtrip(&self, doc: &witx::Document) -> Result<()> { + self.bump_ntests(); + let back_to_sexprs = format!("{}", doc); + let doc2 = witx::parse(&back_to_sexprs)?; + if *doc == doc2 { + return Ok(()); + } + + // Try to get a more specific error message that isn't thousands of + // lines long of debug representations. + for type_ in doc.typenames() { + let type2 = match doc2.typename(&type_.name) { + Some(t) => t, + None => bail!("doc2 missing datatype"), + }; + if type_ != type2 { + bail!("types are not equal\n{:?}\n !=\n{:?}", type_, type2); + } + } + for mod_ in doc.modules() { + let mod2 = match doc2.module(&mod_.name) { + Some(m) => m, + None => bail!("doc2 missing module"), + }; + for import in mod_.imports() { + let import2 = match mod2.import(&import.name) { + Some(i) => i, + None => bail!("mod2 missing import"), + }; + assert_eq!(import, import2); + } + for func in mod_.funcs() { + let func2 = match mod2.func(&func.name) { + Some(f) => f, + None => bail!("mod2 missing func"), + }; + assert_eq!(func, func2); + } + } + bail!("{:?} != {:?}", doc, doc2) + } + + fn assert_md(&self, doc: &witx::Document) -> Result<()> { + self.bump_ntests(); + doc.to_md(); + Ok(()) + } + + fn bump_ntests(&self) { + self.ntests.fetch_add(1, SeqCst); + } +} + +mod kw { + wast::custom_keyword!(assert_invalid); + wast::custom_keyword!(assert_representable); + wast::custom_keyword!(witx); + wast::custom_keyword!(eq); + wast::custom_keyword!(noteq); + wast::custom_keyword!(load); + wast::custom_keyword!(superset); +} + +struct Witxt<'a> { + directives: Vec>, +} + +impl<'a> Parse<'a> for Witxt<'a> { + fn parse(parser: Parser<'a>) -> parser::Result { + let mut directives = Vec::new(); + while !parser.is_empty() { + directives.push(parser.parens(|p| p.parse())?); + } + Ok(Witxt { directives }) + } +} + +enum WitxtDirective<'a> { + Witx(Witx<'a>), + AssertInvalid { + span: wast::Span, + witx: Witx<'a>, + message: &'a str, + }, + AssertRepresentable { + span: wast::Span, + repr: RepEquality, + t1: (wast::Id<'a>, &'a str), + t2: (wast::Id<'a>, &'a str), + }, +} + +impl WitxtDirective<'_> { + fn span(&self) -> wast::Span { + match self { + WitxtDirective::Witx(w) => w.span, + WitxtDirective::AssertInvalid { span, .. } + | WitxtDirective::AssertRepresentable { span, .. } => *span, + } + } +} + +impl<'a> Parse<'a> for WitxtDirective<'a> { + fn parse(parser: Parser<'a>) -> parser::Result { + let mut l = parser.lookahead1(); + if l.peek::() { + Ok(WitxtDirective::Witx(parser.parse()?)) + } else if l.peek::() { + let span = parser.parse::()?.0; + Ok(WitxtDirective::AssertInvalid { + span, + witx: parser.parens(|p| p.parse())?, + message: parser.parse()?, + }) + } else if l.peek::() { + let span = parser.parse::()?.0; + Ok(WitxtDirective::AssertRepresentable { + span, + repr: parser.parse()?, + t1: (parser.parse()?, parser.parse()?), + t2: (parser.parse()?, parser.parse()?), + }) + } else { + Err(l.error()) + } + } +} + +struct Witx<'a> { + span: wast::Span, + id: Option>, + def: WitxDef<'a>, +} + +enum WitxDef<'a> { + Fs(Vec<&'a str>), + Inline(Vec>>), +} + +impl Witx<'_> { + fn document(&self, contents: &str, file: &Path) -> Result { + match &self.def { + WitxDef::Inline(decls) => { + let mut validator = witx::DocValidation::new(); + let mut definitions = Vec::new(); + for decl in decls { + validator + .scope(contents, file) + .validate_decl(&decl.item, &decl.comments, &mut definitions) + .map_err(witx::WitxError::Validation)?; + } + Ok(validator.into_document(definitions)) + } + WitxDef::Fs(paths) => { + let parent = file.parent().unwrap(); + let paths = paths.iter().map(|p| parent.join(p)).collect::>(); + Ok(witx::load(&paths)?) + } + } + } +} + +impl<'a> Parse<'a> for Witx<'a> { + fn parse(parser: Parser<'a>) -> parser::Result { + let span = parser.parse::()?.0; + let id = parser.parse()?; + + let def = if parser.peek2::() { + parser.parens(|p| { + p.parse::()?; + let mut paths = Vec::new(); + while !p.is_empty() { + paths.push(p.parse()?); + } + Ok(WitxDef::Fs(paths)) + })? + } else { + let mut decls = Vec::new(); + while !parser.is_empty() { + decls.push(parser.parens(|p| p.parse())?); + } + WitxDef::Inline(decls) + }; + Ok(Witx { id, span, def }) + } +} + +#[derive(Debug)] +enum RepEquality { + Eq, + NotEq, + Superset, +} + +impl<'a> Parse<'a> for RepEquality { + fn parse(parser: Parser<'a>) -> parser::Result { + let mut l = parser.lookahead1(); + if l.peek::() { + parser.parse::()?; + Ok(RepEquality::Eq) + } else if l.peek::() { + parser.parse::()?; + Ok(RepEquality::NotEq) + } else if l.peek::() { + parser.parse::()?; + Ok(RepEquality::Superset) + } else { + Err(l.error()) + } + } +} diff --git a/tools/witx/tests/witxt/anonymous.witxt b/tools/witx/tests/witxt/anonymous.witxt new file mode 100644 index 000000000..0ed3bc0d2 --- /dev/null +++ b/tools/witx/tests/witxt/anonymous.witxt @@ -0,0 +1,56 @@ +;; Ensure that anonymous structured types are not allowed in type positions at +;; this time, everything has to be named to assist in binding in languages. + +(assert_invalid + (witx + (typename $a (@witx pointer (record (field $b u8)))) + ) + "Anonymous structured types") + +(assert_invalid + (witx + (typename $a (@witx pointer (union))) + ) + "Anonymous structured types") + +(assert_invalid + (witx + (typename $a (@witx pointer (enum $b))) + ) + "Anonymous structured types") + +(assert_invalid + (witx + (typename $a (@witx pointer (flags $b))) + ) + "Anonymous structured types") + +(assert_invalid + (witx + (typename $a (@witx pointer (handle))) + ) + "Anonymous structured types") + +(assert_invalid + (witx + (typename $a (record (field $b (record (field $c u8))))) + ) + "Anonymous structured types") + +(assert_invalid + (witx + (typename $tag (enum $c)) + (typename $a (record (field $b (union)))) + ) + "Anonymous structured types") + + +;; pointers don't count for anonymous indirections +(witx + (typename $a (@witx pointer u8))) + +(witx + (typename $a (@witx pointer (@witx const_pointer u8)))) + +(witx + (typename $a (record (field $b (@witx pointer u8))))) diff --git a/tools/witx/tests/witxt/multimodule.witxt b/tools/witx/tests/witxt/multimodule.witxt new file mode 100644 index 000000000..d8b6559ac --- /dev/null +++ b/tools/witx/tests/witxt/multimodule.witxt @@ -0,0 +1,22 @@ +;; B uses A, and C uses A. +(witx $multi + (load "multimodule/type_b.witx" "multimodule/type_c.witx") +) + +(witx $reference + (typename $a u32) + (typename $b (record (field $member_a $a))) + (typename $c (record (field $first_a $a) (field $second_a $a))) +) + +(assert_representable eq $reference "a" $multi "a") +(assert_representable eq $reference "b" $multi "b") +(assert_representable eq $reference "c" $multi "c") + +(assert_invalid + (witx + (load + "multimodule/type_a.witx" + "multimodule/redefine_a.witx") + ) + "Redefinition of name `a`") diff --git a/tools/witx/tests/multimodule/redefine_a.witx b/tools/witx/tests/witxt/multimodule/redefine_a.witx similarity index 100% rename from tools/witx/tests/multimodule/redefine_a.witx rename to tools/witx/tests/witxt/multimodule/redefine_a.witx diff --git a/tools/witx/tests/multimodule/type_a.witx b/tools/witx/tests/witxt/multimodule/type_a.witx similarity index 100% rename from tools/witx/tests/multimodule/type_a.witx rename to tools/witx/tests/witxt/multimodule/type_a.witx diff --git a/tools/witx/tests/multimodule/type_b.witx b/tools/witx/tests/witxt/multimodule/type_b.witx similarity index 100% rename from tools/witx/tests/multimodule/type_b.witx rename to tools/witx/tests/witxt/multimodule/type_b.witx diff --git a/tools/witx/tests/multimodule/type_c.witx b/tools/witx/tests/witxt/multimodule/type_c.witx similarity index 100% rename from tools/witx/tests/multimodule/type_c.witx rename to tools/witx/tests/witxt/multimodule/type_c.witx diff --git a/tools/witx/tests/witxt/representation.witxt b/tools/witx/tests/witxt/representation.witxt new file mode 100644 index 000000000..37d20a70a --- /dev/null +++ b/tools/witx/tests/witxt/representation.witxt @@ -0,0 +1,60 @@ +;; type names don't matter +(witx $a + (typename $a (flags (@witx bitflags u8) $b $c))) +(witx $b + (typename $b (flags (@witx bitflags u8) $b $c))) + +(assert_representable eq $a "a" $b "b") +(assert_representable eq $b "b" $a "a") + +(; TODO: perhaps add assertions eventually for document-level representability? +;; flags +(witx $a + (typename $a (flags (@witx bitflags u8) $b $c))) +(witx $b + (typename $b (flags (@witx bitflags u8) $b $c $d))) + +(assert_representable noteq $b "b" $a "a") +(assert_representable superset $a "a" $b "b") + +(witx $c + (typename $c (flags (@witx bitflags u8) $b $e))) +(assert_representable noteq $a "a" $c "c") +(assert_representable noteq $c "c" $a "a") + +(witx $d + (typename $d (flags (@witx bitflags u16) $b $c))) +(assert_representable noteq $a "a" $d "d") +(assert_representable superset $d "d" $a "a") +(assert_representable superset $d "d" $b "b") +;) + +;; enums +(witx $a + (typename $a (enum $b $c))) +(witx $b + (typename $b (enum $b $c $d))) +(assert_representable superset $a "a" $b "b") +(assert_representable noteq $b "b" $a "a") + +(witx $c + (typename $c (enum (@witx tag u16) $b $c))) +(assert_representable superset $c "c" $a "a") +(assert_representable superset $c "c" $b "b") + +;; unions +(witx $a + (typename $tag (enum $b $c)) + (typename $a (union (@witx tag $tag) u32 f32))) +(witx $b + (typename $tag (enum $b $c $d)) + (typename $b (union (@witx tag $tag) u32 f32 f64))) +(assert_representable superset $a "a" $b "b") +(assert_representable noteq $b "b" $a "a") + +(witx $c + (typename $tag (enum $b $c)) + (typename $c (variant (@witx tag $tag) (case $c f32) (case $b u32)))) +(assert_representable eq $a "a" $c "c") +(assert_representable eq $c "c" $a "a") +(assert_representable superset $c "c" $b "b") diff --git a/tools/witx/tests/witxt/simple.witxt b/tools/witx/tests/witxt/simple.witxt new file mode 100644 index 000000000..af2403043 --- /dev/null +++ b/tools/witx/tests/witxt/simple.witxt @@ -0,0 +1,12 @@ +(witx) + +(witx + (typename $x u32) +) + +(assert_invalid + (witx + (typename $x u32) + (typename $x u32) + ) + "Redefinition of name `x`") diff --git a/tools/witx/tests/witxt/union.witxt b/tools/witx/tests/witxt/union.witxt new file mode 100644 index 000000000..92be3dcd9 --- /dev/null +++ b/tools/witx/tests/witxt/union.witxt @@ -0,0 +1,97 @@ + +(witx + (typename $u (union u8)) +) +(witx + (typename $tag (enum (@witx tag u8) $c)) + (typename $u (union (@witx tag $tag) u8)) +) + +(witx + (typename $tag (enum $a $b)) + (typename $u (variant (@witx tag $tag) (case $a) (case $b u16))) +) + +(witx + (typename $tag (enum $a $b)) + (typename $u (variant (@witx tag $tag) (case $a) (case $b))) +) + + +(witx + (typename $u + (union + u8 + u16 + u32 + u64 + s8 + s16 + s32 + s64 + f32 + f64 + (@witx usize) + (@witx char8) + ) + ) +) + +(assert_invalid + (witx (typename $u (union (@witx tag $tag) u8 u16))) + "Unknown name `tag`" +) + +(assert_invalid + (witx + (typename $tag string) + (typename $u (union (@witx tag $tag) u8 u16)) + ) + "Wrong kind of name `tag`: expected enum or builtin, got list" +) + +(assert_invalid + (witx + (typename $tag (enum $c)) + (typename $u (variant (@witx tag $tag) (case $b u8))) + ) + "Invalid union field `b`: does not correspond to variant in tag `tag`" +) + +(assert_invalid + (witx + (typename $tag (enum $c)) + (typename $u (union (@witx tag $tag) f32 u8)) + ) + "Union expected 1 variants, found 2" +) + +(assert_invalid + (witx + (typename $tag (enum $c $d)) + (typename $u (union (@witx tag $tag) f32)) + ) + "Union expected 2 variants, found 1" +) + +(witx $d1 + (typename $tag (enum $a $b)) + (typename $u (union (@witx tag $tag) u8 u16)) +) + +(witx $d2 + (typename $tag (enum $a $b)) + (typename $u (variant (@witx tag $tag) (case $b u16) (case $a u8))) +) + +;; These two unions should be represented the same: +(assert_representable eq $d1 "u" $d2 "u") +(assert_representable eq $d2 "u" $d1 "u") + +;; Tag order doesnt matter for validation, but does for rep equality +(witx $d3 + (typename $tag (enum $b $a)) + (typename $u (union (@witx tag $tag) u16 u8)) +) + +(assert_representable noteq $d3 "u" $d1 "u") diff --git a/tools/witx/tests/witxt/wasi.witxt b/tools/witx/tests/witxt/wasi.witxt new file mode 100644 index 000000000..f654a7ee3 --- /dev/null +++ b/tools/witx/tests/witxt/wasi.witxt @@ -0,0 +1,26 @@ +;; Ensure that all current witx definitions in this repository load, parse, +;; roundtrip, and are documentable. + +(witx + (load "../../../../phases/old/snapshot_0/witx/wasi_unstable.witx")) +(witx + (load "../../../../phases/snapshot/witx/wasi_snapshot_preview1.witx")) + +(witx + (load + "../../../../phases/ephemeral/witx/wasi_ephemeral_args.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_clock.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_environ.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_fd.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_path.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_poll.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_proc.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_random.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_sched.witx" + "../../../../phases/ephemeral/witx/wasi_ephemeral_sock.witx" + ) +) +;; should be singularly-loadable as well +(witx + (load + "../../../../phases/ephemeral/witx/wasi_ephemeral_fd.witx"))