From a7dba54b7fe8f8d032021bfd2f501c12c88458bc Mon Sep 17 00:00:00 2001 From: Syrus Date: Tue, 14 Apr 2020 11:41:12 -0700 Subject: [PATCH] Improved test generation --- Cargo.lock | 40 +- Cargo.toml | 7 +- Makefile | 8 +- build.rs | 102 +- lib/runtime-core/src/import.rs | 5 + tests/emtest.rs | 2 +- tests/spectest.rs | 1572 +------------------------------ tests/test-generator/Cargo.toml | 8 + tests/test-generator/src/lib.rs | 162 ++++ tests/wast/Cargo.toml | 17 + tests/wast/README.md | 7 + tests/wast/src/errors.rs | 50 + tests/wast/src/lib.rs | 34 + tests/wast/src/spectest.rs | 56 ++ tests/wast/src/wast.rs | 506 ++++++++++ 15 files changed, 1018 insertions(+), 1558 deletions(-) create mode 100644 tests/test-generator/Cargo.toml create mode 100644 tests/test-generator/src/lib.rs create mode 100644 tests/wast/Cargo.toml create mode 100644 tests/wast/README.md create mode 100644 tests/wast/src/errors.rs create mode 100644 tests/wast/src/lib.rs create mode 100644 tests/wast/src/spectest.rs create mode 100644 tests/wast/src/wast.rs diff --git a/Cargo.lock b/Cargo.lock index dc0133cea..21128ecc0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2258,6 +2258,14 @@ dependencies = [ "winapi 0.3.8", ] +[[package]] +name = "test-generator" +version = "0.1.0" +dependencies = [ + "anyhow", + "target-lexicon", +] + [[package]] name = "textwrap" version = "0.11.0" @@ -2269,18 +2277,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee14bf8e6767ab4c687c9e8bc003879e042a96fd67a3ba5934eadb6536bef4db" +checksum = "54b3d3d2ff68104100ab257bb6bb0cb26c901abe4bd4ba15961f3bf867924012" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.11" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a7b51e1fbc44b5a0840be594fbc0f960be09050f2617e61e6aa43bef97cd3ef4" +checksum = "ca972988113b7715266f91250ddb98070d033c62a011fa0fcc57434a649310dd" dependencies = [ "proc-macro2 1.0.9", "quote 1.0.2", @@ -2746,6 +2754,7 @@ dependencies = [ name = "wasmer-bin" version = "0.16.2" dependencies = [ + "anyhow", "atty", "byteorder", "criterion", @@ -2759,6 +2768,7 @@ dependencies = [ "rustc_version", "serde", "structopt", + "test-generator", "typetag", "wabt", "wasmer", @@ -2772,6 +2782,7 @@ dependencies = [ "wasmer-singlepass-backend", "wasmer-wasi", "wasmer-wasi-experimental-io-devices", + "wasmer-wast", ] [[package]] @@ -2844,7 +2855,7 @@ version = "0.16.2" dependencies = [ "nom", "serde", - "wast", + "wast 8.0.0", ] [[package]] @@ -2987,6 +2998,16 @@ dependencies = [ "wasmer-wasi", ] +[[package]] +name = "wasmer-wast" +version = "0.16.2" +dependencies = [ + "anyhow", + "thiserror", + "wasmer", + "wast 9.0.0", +] + [[package]] name = "wasmer-win-exception-handler" version = "0.16.2" @@ -3012,6 +3033,15 @@ dependencies = [ "leb128", ] +[[package]] +name = "wast" +version = "9.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee7b16105405ca2aa2376ba522d8d4b1a11604941dd3bb7df9fd2ece60f8d16a" +dependencies = [ + "leb128", +] + [[package]] name = "web-sys" version = "0.3.36" diff --git a/Cargo.toml b/Cargo.toml index 23add2e44..075edb4d9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -59,18 +59,23 @@ members = [ "examples/parallel", "examples/plugin-for-example", "examples/parallel-guest", + "tests/test-generator", "tests/generate-wasi-tests", "tests/generate-emscripten-tests", + "tests/wast", ] [build-dependencies] -wabt = "0.9.1" +anyhow = "1.0.19" generate-emscripten-tests = { path = "tests/generate-emscripten-tests" } generate-wasi-tests = { path = "tests/generate-wasi-tests" } +test-generator = { path = "tests/test-generator" } glob = "0.3" rustc_version = "0.2" [dev-dependencies] +anyhow = "1.0.19" +wasmer-wast = { path = "tests/wast" } criterion = "0.3" glob = "0.3" libc = "0.2.60" # for `tests/dev-utils`'s Stdout capturing diff --git a/Makefile b/Makefile index 7c9448c25..38435fc61 100644 --- a/Makefile +++ b/Makefile @@ -35,17 +35,17 @@ generate: generate-emtests generate-wasitests # Spectests spectests-singlepass: - WASMER_TEST_SINGLEPASS=1 cargo test test_run_spectests --release --no-default-features --features "wasi backend-singlepass" -- --nocapture --test-threads 1 + WASMER_TEST_SINGLEPASS=1 cargo test singlepass::spec --release --no-default-features --features "wasi backend-singlepass" spectests-cranelift: - WASMER_TEST_CRANELFIT=1 cargo test test_run_spectests --release --no-default-features --features "wasi backend-cranelift" -- --nocapture + WASMER_TEST_CRANELFIT=1 cargo test cranelift::spec --release --no-default-features --features "wasi backend-cranelift" spectests-llvm: - WASMER_TEST_LLVM=1 cargo test test_run_spectests --release --no-default-features --features "wasi backend-llvm wasmer-llvm-backend/test" -- --nocapture + WASMER_TEST_LLVM=1 cargo test llvm::spec --release --no-default-features --features "wasi backend-llvm wasmer-llvm-backend/test" spectests-all: WASMER_TEST_CRANELIFT=1 WASMER_TEST_LLVM=1 WASMER_TEST_SINGLEPASS=1 \ - cargo test test_run_spectests --release --no-default-features --features "wasi backend-cranelift backend-singlepass backend-llvm wasmer-llvm-backend/test" -- --nocapture --test-threads 1 + cargo test spec --release --no-default-features --features "wasi backend-cranelift backend-singlepass backend-llvm wasmer-llvm-backend/test" spectests: spectests-singlepass spectests-cranelift spectests-llvm diff --git a/build.rs b/build.rs index 4f74ecfff..dc598c2b4 100644 --- a/build.rs +++ b/build.rs @@ -4,8 +4,108 @@ use generate_emscripten_tests; use generate_wasi_tests; +use std::env; +use std::fmt::Write; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use test_generator::{ + build_ignores_from_textfile, extract_name, test_directory, test_directory_module, + with_test_module, Test, Testsuite, +}; + +/// Given a Testsuite and a path, process the path in case is a wast +/// file. +fn wast_processor(out: &mut Testsuite, p: PathBuf) -> Option { + let ext = p.extension()?; + // Only look at wast files. + if ext != "wast" { + return None; + } + + // Ignore files starting with `.`, which could be editor temporary files + if p.file_stem()?.to_str()?.starts_with(".") { + return None; + } + + let testname = extract_name(&p); + let body = format!( + "crate::run_wast(r#\"{}\"#, \"{}\")", + p.display(), + out.path.get(0).unwrap() + ); + + Some(Test { + name: testname.to_string(), + body: body.to_string(), + }) +} + +fn main() -> anyhow::Result<()> { + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=test/ignores.txt"); -fn main() { generate_wasi_tests::build(); generate_emscripten_tests::build(); + + let out_dir = PathBuf::from( + env::var_os("OUT_DIR").expect("The OUT_DIR environment variable must be set"), + ); + let ignores = build_ignores_from_textfile("tests/ignores.txt".into())?; + let mut out = Testsuite { + buffer: String::new(), + path: vec![], + ignores: ignores, + }; + + for compiler in &["singlepass", "cranelift", "llvm"] { + writeln!(out.buffer, "#[cfg(feature=\"backend-{}\")]", compiler); + writeln!(out.buffer, "#[cfg(test)]")?; + writeln!(out.buffer, "#[allow(non_snake_case)]")?; + with_test_module(&mut out, compiler, |mut out| { + with_test_module(&mut out, "spec", |out| { + let spec_tests = test_directory(out, "tests/spectests", wast_processor)?; + // Skip running spec_testsuite tests if the submodule isn't checked + // out. + // if spec_tests > 0 { + // test_directory_module( + // out, + // "tests/spec_testsuite/proposals/simd", + // wast_processor, + // )?; + // test_directory_module( + // out, + // "tests/spec_testsuite/proposals/multi-value", + // wast_processor, + // )?; + // test_directory_module( + // out, + // "tests/spec_testsuite/proposals/reference-types", + // wast_processor, + // )?; + // test_directory_module( + // out, + // "tests/spec_testsuite/proposals/bulk-memory-operations", + // wast_processor, + // )?; + // } else { + // println!( + // "cargo:warning=The spec testsuite is disabled. To enable, run `git submodule \ + // update --remote`." + // ); + // } + Ok(()) + })?; + Ok(()) + })?; + } + + // println!("{}", out.buffer); + // std::process::exit(1); + // Write out our auto-generated tests and opportunistically format them with + // `rustfmt` if it's installed. + let output = out_dir.join("generated_tests.rs"); + fs::write(&output, out.buffer)?; + drop(Command::new("rustfmt").arg(&output).status()); + Ok(()) } diff --git a/lib/runtime-core/src/import.rs b/lib/runtime-core/src/import.rs index 284ff1725..a40f0273d 100644 --- a/lib/runtime-core/src/import.rs +++ b/lib/runtime-core/src/import.rs @@ -178,6 +178,11 @@ impl ImportObject { } out } + + /// Returns true if the ImportObject contains namespace with the provided name. + pub fn contains_namespace(&self, name: &str) -> bool { + self.map.lock().unwrap().borrow().contains_key(name) + } } /// Iterator for an `ImportObject`'s exports. diff --git a/tests/emtest.rs b/tests/emtest.rs index 206387d1d..bf9de1a27 100644 --- a/tests/emtest.rs +++ b/tests/emtest.rs @@ -1,2 +1,2 @@ -pub mod utils; mod emtests; +pub mod utils; diff --git a/tests/spectest.rs b/tests/spectest.rs index 32a08c39d..737f0178f 100644 --- a/tests/spectest.rs +++ b/tests/spectest.rs @@ -1,11 +1,4 @@ -#![deny( - bad_style, - dead_code, - unused_imports, - unused_variables, - unused_unsafe, - unreachable_patterns -)] +use std::path::Path; #[cfg(not(any( feature = "backend-llvm", @@ -13,1545 +6,32 @@ feature = "backend-singlepass" )))] compile_error!("No compiler backend detected: please specify at least one compiler backend!"); +use anyhow::bail; +use wasmer::compiler::Backend; +use wasmer_wast::Wast; -#[cfg(test)] -mod tests { +// #[cfg(test)] +// mod spectests { +// mod cranelift { +// #[test] +// fn address() -> Result<(), String> { +// crate::run_wast("tests/spectests/address.wast", "llvm") +// } +// } +// } +include!(concat!(env!("OUT_DIR"), "/generated_tests.rs")); - // TODO fix spec failures - // TODO fix panics and remove panic handlers - // TODO do something with messages _message, message: _, msg: _ - // TODO consider git submodule for spectests? & separate dir for simd/extra tests - // TODO cleanup refactor - // TODO Files could be run with multiple threads - // TODO Allow running WAST &str directly (E.g. for use outside of spectests) - - use std::collections::HashSet; - use std::env; - use std::sync::{Arc, Mutex}; - - struct SpecFailure { - file: String, - line: u64, - kind: String, - message: String, - } - - struct TestReport { - failures: Vec, - passed: u32, - failed: u32, - allowed_failure: u32, - } - - impl TestReport { - pub fn count_passed(&mut self) { - self.passed += 1; - } - - pub fn has_failures(&self) -> bool { - self.failed > 0 - } - - pub fn add_failure( - &mut self, - failure: SpecFailure, - _testkey: &str, - excludes: &mut Vec>, - line: u64, - ) { - // Ensure that each exclude is only used once. - if let Some(_) = excludes - .iter_mut() - .find(|e| { - if let Some(ref e) = e { - e.line_matches(line) && e.exclude_kind == ExcludeKind::Fail - } else { - false - } - }) - .take() - .and_then(|x| x.take()) - { - self.allowed_failure += 1; - return; - } - self.failed += 1; - self.failures.push(failure); - } - } - - fn get_available_compilers() -> &'static [&'static str] { - &[ - #[cfg(feature = "backend-cranelift")] - "clif", - #[cfg(feature = "backend-llvm")] - "llvm", - #[cfg(feature = "backend-singlepass")] - "singlepass", - ] - } - - fn get_compilers_to_test() -> Vec<&'static str> { - let mut out = vec![]; - if let Ok(v) = env::var("WASMER_TEST_CRANELIFT") { - if v == "1" { - out.push("clif"); - } - } - if let Ok(v) = env::var("WASMER_TEST_LLVM") { - if v == "1" { - out.push("llvm"); - } - } - if let Ok(v) = env::var("WASMER_TEST_SINGLEPASS") { - if v == "1" { - out.push("singlepass"); - } - } - - out - } - - #[cfg(unix)] - fn get_target_family() -> &'static str { - "unix" - } - - #[cfg(windows)] - fn get_target_family() -> &'static str { - "windows" - } - - #[cfg(target_os = "android")] - fn get_target_os() -> &'static str { - "android" - } - - #[cfg(target_os = "freebsd")] - fn get_target_os() -> &'static str { - "freebsd" - } - - #[cfg(target_os = "linux")] - fn get_target_os() -> &'static str { - "linux" - } - - #[cfg(target_os = "macos")] - fn get_target_os() -> &'static str { - "macos" - } - - #[cfg(target_os = "windows")] - fn get_target_os() -> &'static str { - "windows" - } - - fn get_target_arch() -> &'static str { - if cfg!(target_arch = "x86_64") { - "x86_64" - } else if cfg!(target_arch = "aarch64") { - "aarch64" - } else if cfg!(target_arch = "x86") { - "x86" - } else if cfg!(target_arch = "mips") { - "mips" - } else if cfg!(target_arch = "powerpc") { - "powerpc" - } else if cfg!(target_arch = "powerpc64") { - "powerpc64" - } else if cfg!(target_arch = "arm") { - "arm" - } else { - panic!("unknown target arch") - } - } - - // clif:skip:data.wast:172:unix:x86 - #[allow(dead_code)] - #[derive(Clone)] - struct Exclude { - backend: Option, - exclude_kind: ExcludeKind, - file: String, - line: Option, - target_family: Option, - target_arch: Option, - } - - impl Exclude { - fn line_matches(&self, value: u64) -> bool { - self.line.is_none() || self.line.unwrap() == value - } - - fn line_exact_match(&self, value: u64) -> bool { - self.line.is_some() && self.line.unwrap() == value - } - - fn matches_backend(&self, value: &str) -> bool { - self.backend.is_none() || self.backend.as_ref().unwrap() == value - } - - fn matches_target_family(&self, value: &str) -> bool { - self.target_family.is_none() || self.target_family.as_ref().unwrap() == value - } - - fn matches_target_arch(&self, value: &str) -> bool { - self.target_arch.is_none() || self.target_arch.as_ref().unwrap() == value - } - - fn from( - backend: &str, - exclude_kind: &str, - file: &str, - line: &str, - target_family: &str, - target_arch: &str, - ) -> Exclude { - let backend: Option = match backend { - "*" => None, - "clif" => Some("clif".to_string()), - "singlepass" => Some("singlepass".to_string()), - "llvm" => Some("llvm".to_string()), - _ => panic!("backend {:?} not recognized", backend), - }; - let exclude_kind = match exclude_kind { - "skip" => ExcludeKind::Skip, - "fail" => ExcludeKind::Fail, - _ => panic!("exclude kind {:?} not recognized", exclude_kind), - }; - let line = match line { - "*" => None, - _ => Some( - line.parse::() - .expect(&format!("expected * or int: {:?}", line)), - ), - }; - let target_family = match target_family { - "*" => None, - _ => Some(target_family.to_string()), - }; - let target_arch = match target_arch { - "*" => None, - _ => Some(target_arch.to_string()), - }; - Exclude { - backend, - exclude_kind, - file: file.to_string(), - line, - target_family, - target_arch, - } - } - } - - fn with_instance( - maybe_instance: Option>>, - named_modules: &HashMap>>, - module: &Option, - f: F, - ) -> Option - where - R: Sized, - F: FnOnce(&Instance) -> R, - { - let ref ins = module - .as_ref() - .and_then(|name| named_modules.get(name).cloned()) - .or(maybe_instance)?; - let guard = ins.lock().unwrap(); - Some(f(guard.borrow())) - } - - use glob::glob; - use std::collections::HashMap; - use std::fs; - use std::panic::AssertUnwindSafe; - use std::path::PathBuf; - use std::str::FromStr; - use wabt::script::{Action, Command, CommandKind, ScriptParser, Value}; - use wasmer::wasm::Export; - use wasmer::{ - compiler::{compile_with_config_with, compiler_for_backend, Backend, CompilerConfig}, - error::CompileError, - func, - import::{ImportObject, LikeNamespace}, - imports, - types::ElementType, - units::Pages, - vm::Ctx, - wasm::{ - self, Features, Global, Instance, Memory, MemoryDescriptor, Table, TableDescriptor, - }, +fn run_wast(wast_path: &str, backend: &str) -> anyhow::Result<()> { + let backend = match backend { + #[cfg(feature = "backend-singlepass")] + "singlepass" => Backend::Singlepass, + #[cfg(feature = "backend-cranelift")] + "cranelift" => Backend::Cranelift, + #[cfg(feature = "backend-llvm")] + "llvm" => Backend::LLVM, + _ => bail!("Backend {} not found", backend), }; - - fn format_panic(e: &dyn std::any::Any) -> String { - if let Some(s) = e.downcast_ref::<&str>() { - format!("{}", s) - } else if let Some(s) = e.downcast_ref::() { - format!("{}", s) - } else { - "(unknown)".into() - } - } - - fn parse_and_run( - path: &PathBuf, - file_excludes: &HashSet, - excludes: &HashMap>, - backend: &'static str, - ) -> Result { - let mut test_report = TestReport { - failures: vec![], - passed: 0, - failed: 0, - allowed_failure: 0, - }; - - let filename = path.file_name().unwrap().to_str().unwrap(); - let source = fs::read(&path).unwrap(); - - // Entire file is excluded by line * and skip - if file_excludes.contains(filename) { - return Ok(test_report); - } - - let mut features = wabt::Features::new(); - features.enable_simd(); - features.enable_threads(); - features.enable_sign_extension(); - features.enable_sat_float_to_int(); - let mut parser: ScriptParser = - ScriptParser::from_source_and_name_with_features(&source, filename, features) - .expect(&format!("Failed to parse script {}", &filename)); - - use std::panic; - let mut instance: Option>> = None; - - let mut named_modules: HashMap>> = HashMap::new(); - - let mut registered_modules: HashMap>> = HashMap::new(); - let mut excludes: Vec<_> = excludes - .get(filename) - .map(|file| file.iter().map(|x| Some(x.clone())).collect()) - .unwrap_or(vec![]); - let excludes = &mut excludes; - let backend_enum = Backend::from_str(if backend == "clif" { - "cranelift" - } else { - backend - }) - .unwrap(); - - while let Some(Command { kind, line }) = - parser.next().map_err(|e| format!("Parse err: {:?}", e))? - { - let test_key = format!("{}:{}:{}", backend, filename, line); - // Use this line to debug which test is running - println!("Running test: {}", test_key); - - // Skip tests that match this line - if excludes - .iter() - .filter_map(|x| x.as_ref()) - .any(|e| e.line_exact_match(line) && e.exclude_kind == ExcludeKind::Skip) - { - continue; - } - - match kind { - CommandKind::Module { module, name } => { - // println!("Module"); - let result = panic::catch_unwind(AssertUnwindSafe(|| { - let spectest_import_object = - get_spectest_import_object(®istered_modules); - let config = CompilerConfig { - features: Features { - simd: true, - threads: true, - }, - nan_canonicalization: true, - enable_verification: true, - ..Default::default() - }; - - let compiler = compiler_for_backend(backend_enum).unwrap(); - let module = - compile_with_config_with(&module.into_vec(), config, &*compiler) - .expect("WASM can't be compiled"); - let i = module - .instantiate(&spectest_import_object) - .expect("WASM can't be instantiated"); - i - })); - match result { - Err(e) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "Module"), - message: format!("caught panic {}", format_panic(&e)), - }, - &test_key, - excludes, - line, - ); - instance = None; - } - Ok(i) => { - let i = Arc::new(Mutex::new(i)); - if name.is_some() { - named_modules.insert(name.unwrap(), Arc::clone(&i)); - } - instance = Some(i); - } - } - } - CommandKind::AssertReturn { action, expected } => { - match action { - Action::Invoke { - module, - field, - args, - } => { - let maybe_call_result = with_instance( - instance.clone(), - &named_modules, - &module, - |instance| { - let params: Vec = - args.iter().cloned().map(convert_value).collect(); - instance.call(&field, ¶ms[..]) - }, - ); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertReturn"), - message: format!("No instance available: {:?}", &module), - }, - &test_key, - excludes, - line, - ); - } else { - let call_result = maybe_call_result.unwrap(); - match call_result { - Err(e) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertReturn"), - message: format!("Call failed {:?}", e), - }, - &test_key, - excludes, - line, - ); - } - Ok(values) => { - for (i, v) in values.iter().enumerate() { - let expected_value = - convert_wabt_value(*expected.get(i).unwrap()); - let v = convert_wasmer_value(v.clone()); - if v != expected_value { - test_report.add_failure(SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertReturn"), - message: format!( - "result {:?} ({:?}) does not match expected {:?} ({:?})", - v, to_hex(v.clone()), expected_value, to_hex(expected_value.clone()) - ), - }, &test_key, excludes, line); - } else { - test_report.count_passed(); - } - } - } - } - } - } - Action::Get { module, field } => { - let maybe_call_result = with_instance( - instance.clone(), - &named_modules, - &module, - |instance| { - instance - .get_export(&field) - .expect(&format!("missing global {:?}", &field)) - }, - ); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertReturn Get"), - message: format!("No instance available {:?}", &module), - }, - &test_key, - excludes, - line, - ); - } else { - let export: Export = maybe_call_result.unwrap(); - match export { - Export::Global(g) => { - let value = g.get(); - let expected_value = - convert_value(*expected.get(0).unwrap()); - if value == expected_value { - test_report.count_passed(); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertReturn Get"), - message: format!( - "Expected Global {:?} got: {:?}", - expected_value, value - ), - }, - &test_key, - excludes, - line, - ); - } - } - _ => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertReturn Get"), - message: format!("Expected Global"), - }, - &test_key, - excludes, - line, - ); - } - } - } - } - } - // println!("in assert return"); - } - CommandKind::AssertReturnCanonicalNan { action } => match action { - Action::Invoke { - module, - field, - args, - } => { - let maybe_call_result = - with_instance(instance.clone(), &named_modules, &module, |instance| { - let params: Vec = - args.iter().cloned().map(convert_value).collect(); - instance.call(&field, ¶ms[..]) - }); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertReturnCanonicalNan"), - message: format!("No instance available {:?}", &module), - }, - &test_key, - excludes, - line, - ); - } else { - let call_result = maybe_call_result.unwrap(); - match call_result { - Err(e) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertReturnCanonicalNan"), - message: format!("Call failed {:?}", e), - }, - &test_key, - excludes, - line, - ); - } - Ok(values) => { - for v in values.iter() { - if is_canonical_nan(v.clone()) { - test_report.count_passed(); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!( - "{:?}", - "AssertReturnCanonicalNan" - ), - message: format!( - "value is not canonical nan {:?} ({:?})", - v, - value_to_hex(v.clone()), - ), - }, - &test_key, - excludes, - line, - ); - } - } - } - } - } - } - _ => panic!("unexpected action in assert return canonical nan"), - }, - CommandKind::AssertReturnArithmeticNan { action } => match action { - Action::Invoke { - module, - field, - args, - } => { - let maybe_call_result = - with_instance(instance.clone(), &named_modules, &module, |instance| { - let params: Vec = - args.iter().cloned().map(convert_value).collect(); - instance.call(&field, ¶ms[..]) - }); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertReturnArithmeticNan"), - message: format!("No instance available"), - }, - &test_key, - excludes, - line, - ); - } else { - let call_result = maybe_call_result.unwrap(); - match call_result { - Err(e) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertReturnArithmeticNan"), - message: format!("Call failed {:?}", e), - }, - &test_key, - excludes, - line, - ); - } - Ok(values) => { - for v in values.iter() { - if is_arithmetic_nan(v.clone()) { - test_report.count_passed(); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!( - "{:?}", - "AssertReturnArithmeticNan" - ), - message: format!( - "value is not arithmetic nan {:?} ({:?})", - v, - value_to_hex(v.clone()), - ), - }, - &test_key, - excludes, - line, - ); - } - } - } - } - } - } - _ => panic!("unexpected action in assert return arithmetic nan"), - }, - CommandKind::AssertTrap { action, message: _ } => match action { - Action::Invoke { - module, - field, - args, - } => { - let maybe_call_result = - with_instance(instance.clone(), &named_modules, &module, |instance| { - #[cfg(unix)] - use wasmer::compiler::{ - pop_code_version, push_code_version, CodeVersion, - }; - - // Manually push code version before calling WebAssembly function, as a hack. - // - // This should eventually be fixed by doing push/pop code version in the function invocation - // logic itself. - - #[cfg(unix)] - let cv_pushed = if let Some(msm) = - instance.module.runnable_module.get_module_state_map() - { - push_code_version(CodeVersion { - baseline: true, - msm: msm, - base: instance - .module - .runnable_module - .get_code() - .unwrap() - .as_ptr() - as usize, - backend: backend.into(), - runnable_module: instance.module.runnable_module.clone(), - }); - true - } else { - false - }; - let params: Vec = - args.iter().cloned().map(convert_value).collect(); - let ret = instance.call(&field, ¶ms[..]); - #[cfg(unix)] - { - if cv_pushed { - pop_code_version().unwrap(); - } - } - ret - }); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertTrap"), - message: format!("No instance available"), - }, - &test_key, - excludes, - line, - ); - } else { - let call_result = maybe_call_result.unwrap(); - use wasmer::error::{CallError, RuntimeError}; - match call_result { - Err(e) => match e { - CallError::Resolve(_) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertTrap"), - message: format!("expected trap, got {:?}", e), - }, - &test_key, - excludes, - line, - ); - } - CallError::Runtime(RuntimeError(e)) => { - use wasmer::error::ExceptionCode; - if let Some(_) = e.downcast_ref::() { - test_report.count_passed(); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertTrap"), - message: format!( - "expected trap, got RuntimeError" - ), - }, - &test_key, - excludes, - line, - ); - } - } - }, - Ok(values) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertTrap"), - message: format!("expected trap, got {:?}", values), - }, - &test_key, - excludes, - line, - ); - } - } - } - } - _ => println!("unexpected action"), - }, - CommandKind::AssertInvalid { module, message: _ } => { - // println!("AssertInvalid"); - let result = panic::catch_unwind(|| { - let config = CompilerConfig { - features: Features { - simd: true, - threads: true, - }, - nan_canonicalization: true, - enable_verification: true, - ..Default::default() - }; - - let compiler = compiler_for_backend(backend_enum).unwrap(); - compile_with_config_with(&module.into_vec(), config, &*compiler) - }); - match result { - Ok(module) => { - if let Err(CompileError::InternalError { msg: _ }) = module { - test_report.count_passed(); - // println!("expected: {:?}", message); - // println!("actual: {:?}", msg); - } else if let Err(CompileError::ValidationError { msg: _ }) = module { - test_report.count_passed(); - // println!("validation expected: {:?}", message); - // println!("validation actual: {:?}", msg); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertInvalid"), - message: "Should be invalid".to_string(), - }, - &test_key, - excludes, - line, - ); - } - } - Err(p) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertInvalid"), - message: format!("caught panic {}", format_panic(&p)), - }, - &test_key, - excludes, - line, - ); - } - } - } - CommandKind::AssertMalformed { module, message: _ } => { - // println!("AssertMalformed"); - - let result = panic::catch_unwind(|| { - let config = CompilerConfig { - features: Features { - simd: true, - threads: true, - }, - nan_canonicalization: true, - enable_verification: true, - ..Default::default() - }; - - let compiler = compiler_for_backend(backend_enum).unwrap(); - compile_with_config_with(&module.into_vec(), config, &*compiler) - }); - - match result { - Ok(module) => { - if let Err(CompileError::InternalError { msg: _ }) = module { - test_report.count_passed(); - // println!("expected: {:?}", message); - // println!("actual: {:?}", msg); - } else if let Err(CompileError::ValidationError { msg: _ }) = module { - test_report.count_passed(); - // println!("validation expected: {:?}", message); - // println!("validation actual: {:?}", msg); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertMalformed"), - message: format!("should be malformed"), - }, - &test_key, - excludes, - line, - ); - } - } - Err(p) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertMalformed"), - message: format!("caught panic {}", format_panic(&p)), - }, - &test_key, - excludes, - line, - ); - } - } - } - CommandKind::AssertUninstantiable { module, message: _ } => { - let spectest_import_object = get_spectest_import_object(®istered_modules); - let config = CompilerConfig { - features: Features { - simd: true, - threads: true, - }, - nan_canonicalization: true, - enable_verification: true, - ..Default::default() - }; - - let compiler = compiler_for_backend(backend_enum).unwrap(); - let module = compile_with_config_with(&module.into_vec(), config, &*compiler) - .expect("WASM can't be compiled"); - let result = panic::catch_unwind(AssertUnwindSafe(|| { - module - .instantiate(&spectest_import_object) - .expect("WASM can't be instantiated"); - })); - match result { - Err(_) => test_report.count_passed(), - Ok(_) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertUninstantiable"), - message: format!( - "instantiate successful, expected uninstantiable" - ), - }, - &test_key, - excludes, - line, - ); - } - }; - } - CommandKind::AssertExhaustion { action, message: _ } => { - match action { - Action::Invoke { - module, - field, - args, - } => { - let maybe_call_result = with_instance( - instance.clone(), - &named_modules, - &module, - |instance| { - let params: Vec = - args.iter().cloned().map(convert_value).collect(); - instance.call(&field, ¶ms[..]) - }, - ); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertExhaustion"), - message: format!("No instance available"), - }, - &test_key, - excludes, - line, - ); - } else { - let call_result = maybe_call_result.unwrap(); - match call_result { - Err(_e) => { - // TODO is specific error required? - test_report.count_passed(); - } - Ok(values) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "AssertExhaustion"), - message: format!( - "Expected call failure, got {:?}", - values - ), - }, - &test_key, - excludes, - line, - ); - } - } - } - } - _ => println!("unexpected action in assert exhaustion"), - } - } - CommandKind::AssertUnlinkable { module, message: _ } => { - let result = panic::catch_unwind(AssertUnwindSafe(|| { - let spectest_import_object = - get_spectest_import_object(®istered_modules); - let config = CompilerConfig { - features: Features { - simd: true, - threads: true, - }, - nan_canonicalization: true, - enable_verification: true, - ..Default::default() - }; - - let compiler = compiler_for_backend(backend_enum).unwrap(); - let module = - compile_with_config_with(&module.into_vec(), config, &*compiler) - .expect("WASM can't be compiled"); - module.instantiate(&spectest_import_object) - })); - match result { - Err(e) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertUnlinkable"), - message: format!("caught panic {}", format_panic(&e)), - }, - &test_key, - excludes, - line, - ); - } - Ok(result) => match result { - Ok(_) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertUnlinkable"), - message: format!( - "instantiate successful, expected unlinkable" - ), - }, - &test_key, - excludes, - line, - ); - } - Err(e) => match e { - wasmer::error::Error::LinkError(_) => { - test_report.count_passed(); - } - _ => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "AssertUnlinkable"), - message: format!("expected link error, got {:?}", e), - }, - &test_key, - excludes, - line, - ); - } - }, - }, - } - } - CommandKind::Register { name, as_name } => { - let instance: Option>> = match name { - Some(ref name) => { - let i = named_modules.get(name); - match i { - Some(ins) => Some(Arc::clone(ins)), - None => None, - } - } - None => match instance { - Some(ref i) => Some(Arc::clone(i)), - None => None, - }, - }; - - if let Some(ins) = instance { - registered_modules.insert(as_name, ins); - } else { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "Register"), - message: format!("No instance available"), - }, - &test_key, - excludes, - line, - ); - } - } - CommandKind::PerformAction(ref action) => match action { - Action::Invoke { - module, - field, - args, - } => { - let maybe_call_result = - with_instance(instance.clone(), &named_modules, &module, |instance| { - let params: Vec = - args.iter().cloned().map(convert_value).collect(); - instance.call(&field, ¶ms[..]) - }); - if maybe_call_result.is_none() { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line: line, - kind: format!("{}", "PerformAction"), - message: format!("No instance available"), - }, - &test_key, - excludes, - line, - ); - } else { - let call_result = maybe_call_result.unwrap(); - match call_result { - Err(e) => { - test_report.add_failure( - SpecFailure { - file: filename.to_string(), - line, - kind: format!("{}", "PerformAction"), - message: format!("Call failed {:?}", e), - }, - &test_key, - excludes, - line, - ); - } - Ok(_values) => { - test_report.count_passed(); - } - } - } - } - Action::Get { module, field } => println!( - "Action Get not implemented {:?} {:?} {:?} {:?}", - module, field, filename, line - ), - }, - } - } - - // Check for unused excludes. - for excl in excludes { - if let Some(ref excl) = *excl { - if excl.exclude_kind == ExcludeKind::Fail { - test_report.failed += 1; - test_report.failures.push(SpecFailure { - file: filename.to_string(), - line: excl.line.unwrap_or(0), - kind: format!("{}", "Exclude"), - message: format!("Excluded failure did not occur"), - }); - } - } - } - Ok(test_report) - } - - fn is_canonical_nan(val: wasm::Value) -> bool { - match val { - wasm::Value::F32(x) => x.is_canonical_nan(), - wasm::Value::F64(x) => x.is_canonical_nan(), - _ => panic!("value is not a float {:?}", val), - } - } - - fn is_arithmetic_nan(val: wasm::Value) -> bool { - match val { - wasm::Value::F32(x) => x.is_quiet_nan(), - wasm::Value::F64(x) => x.is_quiet_nan(), - _ => panic!("value is not a float {:?}", val), - } - } - - fn value_to_hex(val: wasm::Value) -> String { - match val { - wasm::Value::I32(x) => format!("{:#x}", x), - wasm::Value::I64(x) => format!("{:#x}", x), - wasm::Value::F32(x) => format!("{:#x}", x.to_bits()), - wasm::Value::F64(x) => format!("{:#x}", x.to_bits()), - wasm::Value::V128(x) => format!("{:#x}", x), - } - } - - #[derive(Debug, Clone, Eq, PartialEq)] - pub enum SpectestValue { - I32(i32), - I64(i64), - F32(u32), - F64(u64), - V128(u128), - } - - fn convert_wasmer_value(other: wasm::Value) -> SpectestValue { - match other { - wasm::Value::I32(v) => SpectestValue::I32(v), - wasm::Value::I64(v) => SpectestValue::I64(v), - wasm::Value::F32(v) => SpectestValue::F32(v.to_bits()), - wasm::Value::F64(v) => SpectestValue::F64(v.to_bits()), - wasm::Value::V128(v) => SpectestValue::V128(v), - } - } - - fn convert_wabt_value(other: Value) -> SpectestValue { - match other { - Value::I32(v) => SpectestValue::I32(v), - Value::I64(v) => SpectestValue::I64(v), - Value::F32(v) => SpectestValue::F32(v.to_bits()), - Value::F64(v) => SpectestValue::F64(v.to_bits()), - Value::V128(v) => SpectestValue::V128(v), - } - } - - fn convert_value(other: Value) -> wasm::Value { - match other { - Value::I32(v) => wasm::Value::I32(v), - Value::I64(v) => wasm::Value::I64(v), - Value::F32(v) => wasm::Value::F32(v), - Value::F64(v) => wasm::Value::F64(v), - Value::V128(v) => wasm::Value::V128(v), - } - } - - fn to_hex(v: SpectestValue) -> String { - match v { - SpectestValue::I32(v) => format!("{:#x}", v), - SpectestValue::I64(v) => format!("{:#x}", v), - SpectestValue::F32(v) => format!("{:#x}", v), - SpectestValue::F64(v) => format!("{:#x}", v), - SpectestValue::V128(v) => format!("{:#x}", v), - } - } - - fn print(_ctx: &mut Ctx) { - println!(""); - } - - fn print_i32(_ctx: &mut Ctx, val: i32) { - println!("{}", val); - } - - fn print_f32(_ctx: &mut Ctx, val: f32) { - println!("{}", val); - } - - fn print_f64(_ctx: &mut Ctx, val: f64) { - println!("{}", val); - } - - fn print_i32_f32(_ctx: &mut Ctx, val: i32, val2: f32) { - println!("{} {}", val, val2); - } - - fn print_f64_f64(_ctx: &mut Ctx, val: f64, val2: f64) { - println!("{} {}", val, val2); - } - - fn get_spectest_import_object( - registered_modules: &HashMap>>, - ) -> ImportObject { - let memory_desc = MemoryDescriptor::new(Pages(1), Some(Pages(2)), false).unwrap(); - let memory = Memory::new(memory_desc).unwrap(); - - let global_i32 = Global::new(wasm::Value::I32(666)); - let global_f32 = Global::new(wasm::Value::F32(666.0)); - let global_f64 = Global::new(wasm::Value::F64(666.0)); - - let table = Table::new(TableDescriptor { - element: ElementType::Anyfunc, - minimum: 10, - maximum: Some(20), - }) - .unwrap(); - let mut import_object = imports! { - "spectest" => { - "print" => func!(print), - "print_i32" => func!(print_i32), - "print_f32" => func!(print_f32), - "print_f64" => func!(print_f64), - "print_i32_f32" => func!(print_i32_f32), - "print_f64_f64" => func!(print_f64_f64), - "table" => table, - "memory" => memory, - "global_i32" => global_i32, - "global_f32" => global_f32, - "global_f64" => global_f64, - - }, - }; - - for (name, instance) in registered_modules.iter() { - import_object.register(name.clone(), Arc::clone(instance)); - } - import_object - } - - #[derive(Debug, Copy, Clone, PartialEq, Eq)] - enum ExcludeKind { - Skip, - Fail, - } - - use core::borrow::Borrow; - use std::fs::File; - use std::io::{BufRead, BufReader}; - - /// Reads the excludes.txt file into a hash map - fn read_excludes(current_backend: &str) -> (HashMap>, HashSet) { - let mut excludes_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - excludes_path.push("tests"); - excludes_path.push("spectests"); - excludes_path.push("excludes.txt"); - let input = File::open(excludes_path).unwrap(); - let buffered = BufReader::new(input); - let mut result = HashMap::new(); - let mut file_excludes = HashSet::new(); - let current_target_os = get_target_os(); - let current_target_family = get_target_family(); - let current_target_arch = get_target_arch(); - - for line in buffered.lines() { - let mut line = line.unwrap(); - if line.trim().is_empty() || line.starts_with("#") { - // ignore line - } else { - if line.contains("#") { - // Allow end of line comment - let l: Vec<&str> = line.split('#').collect(); - line = l.get(0).unwrap().to_string(); - } - //println!("exclude line {}", line); - // ::: - let split: Vec<&str> = line.trim().split(':').collect(); - - let file = *split.get(2).unwrap(); - let exclude = match split.len() { - 0..=3 => panic!("expected at least 4 exclude conditions"), - 4 => Exclude::from( - *split.get(0).unwrap(), - *split.get(1).unwrap(), - *split.get(2).unwrap(), - *split.get(3).unwrap(), - "*", - "*", - ), - 5 => Exclude::from( - *split.get(0).unwrap(), - *split.get(1).unwrap(), - *split.get(2).unwrap(), - *split.get(3).unwrap(), - *split.get(4).unwrap(), - "*", - ), - 6 => Exclude::from( - *split.get(0).unwrap(), - *split.get(1).unwrap(), - *split.get(2).unwrap(), - *split.get(3).unwrap(), - *split.get(4).unwrap(), - *split.get(5).unwrap(), - ), - _ => panic!("too many exclude conditions {}", split.len()), - }; - - if exclude.matches_backend(current_backend) - && (exclude.matches_target_family(current_target_os) - || exclude.matches_target_family(current_target_family)) - && exclude.matches_target_arch(current_target_arch) - { - // Skip the whole file for line * and skip - if exclude.line.is_none() && exclude.exclude_kind == ExcludeKind::Skip { - file_excludes.insert(file.to_string()); - } - - if !result.contains_key(file) { - result.insert(file.to_string(), vec![]); - } - result.get_mut(file).unwrap().push(exclude); - } - } - } - (result, file_excludes) - } - - #[test] - fn test_run_spectests() { - let mut glob_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")); - glob_path.push("tests"); - glob_path.push("spectests"); - glob_path.push("*.wast"); - - let available_compilers = get_available_compilers() - .iter() - .cloned() - .collect::>(); - let desired_compilers = get_compilers_to_test(); - // default to testing all enabled compilers - let compilers_to_test = if desired_compilers.is_empty() { - available_compilers.iter().cloned().collect::>() - } else { - desired_compilers - .iter() - .cloned() - .filter(|c| available_compilers.contains(c)) - .collect::>() - }; - - // if we've asked to run specific compilers, make sure they're all actually enabled - if !desired_compilers.is_empty() && desired_compilers.len() != compilers_to_test.len() { - panic!("Asked to run spectests with `{:?}` compilers but `{:?}` compilers are enabled (found {} of them: {:?})", desired_compilers, get_compilers_to_test(), compilers_to_test.len(), compilers_to_test); - } - - let glob_str = glob_path.to_str().unwrap(); - for backend in compilers_to_test { - println!("Testing `{}` backend", backend); - let (excludes, file_excludes) = read_excludes(backend); - let mut test_reports = vec![]; - let mut success = true; - for entry in glob(glob_str).expect("Failed to read glob pattern") { - match entry { - Ok(wast_path) => { - let result = parse_and_run(&wast_path, &file_excludes, &excludes, backend); - match result { - Ok(test_report) => { - if test_report.has_failures() { - success = false - } - test_reports.push(test_report); - } - Err(e) => { - success = false; - println!("Unexpected test run error: {:?}", e) - } - } - } - Err(e) => panic!("glob err: {:?}", e), - } - } - - // Print summary - let mut failures = vec![]; - let mut total_passed = 0; - let mut total_failed = 0; - let mut total_allowed_failures = 0; - for mut test_report in test_reports.into_iter() { - total_passed += test_report.passed; - total_failed += test_report.failed; - total_allowed_failures += test_report.allowed_failure; - failures.append(&mut test_report.failures); - } - - println!(""); - println!("{} backend results:", backend); - println!("Failures:"); - for failure in failures.iter() { - // To print excludes for all failures: - println!( - "{}:fail:{}:{} # {} - {}", - backend, failure.file, failure.line, failure.kind, failure.message - ); - } - println!(""); - println!(""); - println!("Spec tests summary report: "); - println!( - "total: {}", - total_passed + total_failed + total_allowed_failures - ); - println!("passed: {}", total_passed); - println!("failed: {}", total_failed); - println!("allowed failures: {}", total_allowed_failures); - println!(""); - println!("Tests {}.", if success { "passed" } else { "failed" }); - println!(""); - assert!(success, "tests passed") - } - } - - /// Bit pattern of an f32 value: - /// 1-bit sign + 8-bit mantissa + 23-bit exponent = 32 bits - /// - /// Bit pattern of an f64 value: - /// 1-bit sign + 11-bit mantissa + 52-bit exponent = 64 bits - /// - /// NOTE: On some old platforms (PA-RISC, some MIPS) quiet NaNs (qNaN) have - /// their mantissa MSB unset and set for signaling NaNs (sNaN). - /// - /// Links: - /// * https://en.wikipedia.org/wiki/Floating-point_arithmetic - /// * https://github.com/WebAssembly/spec/issues/286 - /// * https://en.wikipedia.org/wiki/NaN - /// - pub trait NaNCheck { - fn is_quiet_nan(&self) -> bool; - fn is_canonical_nan(&self) -> bool; - } - - impl NaNCheck for f32 { - /// The MSB of the mantissa must be set for a NaN to be a quiet NaN. - fn is_quiet_nan(&self) -> bool { - let mantissa_msb = 0b1 << 22; - self.is_nan() && (self.to_bits() & mantissa_msb) != 0 - } - - /// For a NaN to be canonical, the MSB of the mantissa must be set and - /// all other mantissa bits must be unset. - fn is_canonical_nan(&self) -> bool { - return self.to_bits() == 0xFFC0_0000 || self.to_bits() == 0x7FC0_0000; - } - } - - impl NaNCheck for f64 { - /// The MSB of the mantissa must be set for a NaN to be a quiet NaN. - fn is_quiet_nan(&self) -> bool { - let mantissa_msb = 0b1 << 51; - self.is_nan() && (self.to_bits() & mantissa_msb) != 0 - } - - /// For a NaN to be canonical, the MSB of the mantissa must be set and - /// all other mantissa bits must be unset. - fn is_canonical_nan(&self) -> bool { - self.to_bits() == 0x7FF8_0000_0000_0000 || self.to_bits() == 0xFFF8_0000_0000_0000 - } - } + let mut wast = Wast::new_with_spectest(backend); + let path = Path::new(wast_path); + wast.run_file(path) } diff --git a/tests/test-generator/Cargo.toml b/tests/test-generator/Cargo.toml new file mode 100644 index 000000000..8714afb9f --- /dev/null +++ b/tests/test-generator/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "test-generator" +version = "0.1.0" +edition = "2018" + +[dependencies] +anyhow = "1.0.19" +target-lexicon = "0.10.0" diff --git a/tests/test-generator/src/lib.rs b/tests/test-generator/src/lib.rs new file mode 100644 index 000000000..3ec3ee79a --- /dev/null +++ b/tests/test-generator/src/lib.rs @@ -0,0 +1,162 @@ +//! Build library to generate a program which runs all the testsuites. +//! +//! By generating a separate `#[test]` test for each file, we allow cargo test +//! to automatically run the files in parallel. +//! +//! > This program is inspired/forked from: +//! > https://github.com/bytecodealliance/wasmtime/blob/master/build.rs + +use anyhow::Context; +use std::collections::HashSet; +use std::fmt::Write; +use std::fs::{DirEntry, File}; +use std::io::{BufRead, BufReader}; +use std::path::{Path, PathBuf}; +use target_lexicon::Triple; + +pub type Ignores = HashSet; +pub struct Testsuite { + pub buffer: String, + pub path: Vec, + pub ignores: Ignores, +} + +impl Testsuite { + fn ignore_current(&self) -> bool { + let full = self.path.join("::"); + self.ignores.contains(&full) + } +} + +#[derive(PartialEq, Eq, PartialOrd, Ord)] +pub struct Test { + pub name: String, + pub body: String, +} + +pub type ProcessorType = fn(&mut Testsuite, PathBuf) -> Option; + +/// Generates an Ignores struct from a text file +pub fn build_ignores_from_textfile(path: PathBuf) -> anyhow::Result { + let mut ignores = HashSet::new(); + let file = File::open(path)?; + let reader = BufReader::new(file); + let host = Triple::host().to_string(); + for line in reader.lines() { + let line = line.unwrap(); + // If the line has a `#` we discard all the content that comes after + let line = if line.contains("#") { + let l: Vec<&str> = line.split('#').collect(); + l.get(0).unwrap().to_string() + } else { + line + }; + // If the lines contains ` on ` it means the test should be ignored + // on that platform + let (line, target) = if line.contains(" on ") { + let l: Vec<&str> = line.split(" on ").collect(); + ( + l.get(0).unwrap().to_string(), + Some(l.get(1).unwrap().to_string()), + ) + } else { + (line, None) + }; + + let line = line.trim().to_string(); + if line.len() == 0 { + continue; + } + + match target { + Some(t) => { + // We skip the ignore if doesn't apply to the current + // host target + if !host.contains(&t) { + continue; + } + } + None => {} + } + ignores.insert(line); + } + Ok(ignores) +} + +pub fn test_directory_module( + out: &mut Testsuite, + path: impl AsRef, + processor: ProcessorType, +) -> anyhow::Result { + let path = path.as_ref(); + let testsuite = &extract_name(path); + with_test_module(out, testsuite, |out| test_directory(out, path, processor)) +} + +fn write_test(out: &mut Testsuite, testname: &str, body: &str) -> anyhow::Result<()> { + writeln!(out.buffer, "#[test]")?; + if out.ignore_current() { + writeln!(out.buffer, "#[ignore]")?; + } + writeln!(out.buffer, "fn r#{}() -> anyhow::Result<()> {{", &testname)?; + writeln!(out.buffer, "{}", body)?; + writeln!(out.buffer, "}}")?; + writeln!(out.buffer)?; + Ok(()) +} + +pub fn test_directory( + out: &mut Testsuite, + path: impl AsRef, + processor: ProcessorType, +) -> anyhow::Result { + let path = path.as_ref(); + let mut dir_entries: Vec<_> = path + .read_dir() + .context(format!("failed to read {:?}", path))? + .map(|r| r.expect("reading testsuite directory entry")) + .filter_map(|dir_entry| processor(out, dir_entry.path())) + .collect(); + + dir_entries.sort(); + + for Test { + name: testname, + body, + } in dir_entries.iter() + { + out.path.push(testname.to_string()); + write_test(out, &testname, &body).unwrap(); + out.path.pop().unwrap(); + } + + Ok(dir_entries.len()) +} + +/// Extract a valid Rust identifier from the stem of a path. +pub fn extract_name(path: impl AsRef) -> String { + path.as_ref() + .file_stem() + .expect("filename should have a stem") + .to_str() + .expect("filename should be representable as a string") + .replace("-", "_") + .replace("/", "_") +} + +pub fn with_test_module( + out: &mut Testsuite, + testsuite: &str, + f: impl FnOnce(&mut Testsuite) -> anyhow::Result, +) -> anyhow::Result { + out.path.push(testsuite.to_string()); + out.buffer.push_str("mod "); + out.buffer.push_str(testsuite); + out.buffer.push_str(" {\n"); + + let result = f(out)?; + + out.buffer.push_str("}\n"); + out.path.pop().unwrap(); + Ok(result) +} diff --git a/tests/wast/Cargo.toml b/tests/wast/Cargo.toml new file mode 100644 index 000000000..f1d446eeb --- /dev/null +++ b/tests/wast/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "wasmer-wast" +version = "0.16.2" +authors = ["Wasmer Engineering Team "] +description = "wast testing support for wasmer" +license = "MIT OR (Apache-2.0 WITH LLVM-exception)" +categories = ["wasm"] +keywords = ["webassembly", "wasm"] +repository = "https://github.com/wasmerio/wasmer" +readme = "README.md" +edition = "2018" + +[dependencies] +anyhow = "1.0.19" +wasmer = { path = "../../lib/api", version = "0.16.2" } +wast = "9.0.0" +thiserror = "1.0.15" diff --git a/tests/wast/README.md b/tests/wast/README.md new file mode 100644 index 000000000..a4f673fee --- /dev/null +++ b/tests/wast/README.md @@ -0,0 +1,7 @@ +This is the `wasmer-wast` crate, which contains an implementation of WebAssembly's +"wast" test scripting language, which is used in the +[WebAssembly spec testsuite], using wasmer for execution. + +[WebAssembly spec testsuite]: https://github.com/WebAssembly/testsuite + +> Note: this project started as a fork of [this crate](https://crates.io/crates/wasmtime-wast). diff --git a/tests/wast/src/errors.rs b/tests/wast/src/errors.rs new file mode 100644 index 000000000..8cb473869 --- /dev/null +++ b/tests/wast/src/errors.rs @@ -0,0 +1,50 @@ +use std::fmt; +use thiserror::Error; + +/// A call Error +#[derive(Error, Debug)] +pub struct CallError { + /// The failing message received when running the directive + pub message: String, +} + +impl fmt::Display for CallError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { + write!(f, "{}", self.message) + } +} + +/// A Directive Error +#[derive(Debug)] +pub struct DirectiveError { + /// The line where the directive is defined + pub line: usize, + /// The column where the directive is defined + pub col: usize, + /// The failing message received when running the directive + pub message: String, +} + +/// A structure holding the list of all executed directives +#[derive(Error, Debug)] +pub struct DirectiveErrors { + /// The filename where the error occured + pub filename: String, + /// The list of errors + pub errors: Vec, +} + +impl fmt::Display for DirectiveErrors { + // This trait requires `fmt` with this exact signature. + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + // Write strictly the first element into the supplied output + // stream: `f`. Returns `fmt::Result` which indicates whether the + // operation succeeded or failed. Note that `write!` uses syntax which + // is very similar to `println!`. + writeln!(f, "Failed directives on {}:", self.filename)?; + for error in self.errors.iter() { + writeln!(f, " • {} ({}:{})", error.message, error.line, error.col)?; + } + Ok(()) + } +} diff --git a/tests/wast/src/lib.rs b/tests/wast/src/lib.rs new file mode 100644 index 000000000..ba5aed9cb --- /dev/null +++ b/tests/wast/src/lib.rs @@ -0,0 +1,34 @@ +//! Implementation of the WAST text format for wasmer. + +#![deny(missing_docs, trivial_numeric_casts, unused_extern_crates)] +#![warn(unused_import_braces)] +#![deny(unstable_features)] +#![cfg_attr(feature = "clippy", plugin(clippy(conf_file = "../../clippy.toml")))] +#![cfg_attr( + feature = "cargo-clippy", + allow(clippy::new_without_default, clippy::new_without_default_derive) +)] +#![cfg_attr( + feature = "cargo-clippy", + warn( + clippy::float_arithmetic, + clippy::mut_mut, + clippy::nonminimal_bool, + clippy::option_map_unwrap_or, + clippy::option_map_unwrap_or_else, + clippy::print_stdout, + clippy::unicode_not_nfc, + clippy::use_self + ) +)] + +mod errors; +mod spectest; +mod wast; + +pub use crate::errors::{DirectiveError, DirectiveErrors}; +pub use crate::spectest::spectest_importobject; +pub use crate::wast::Wast; + +/// Version number of this crate. +pub const VERSION: &str = env!("CARGO_PKG_VERSION"); diff --git a/tests/wast/src/spectest.rs b/tests/wast/src/spectest.rs new file mode 100644 index 000000000..bb2c78c5f --- /dev/null +++ b/tests/wast/src/spectest.rs @@ -0,0 +1,56 @@ +use wasmer::import::ImportObject; +use wasmer::types::ElementType; +use wasmer::units::Pages; +use wasmer::wasm::{Func, Global, Memory, MemoryDescriptor, Table, TableDescriptor, Value}; +use wasmer::*; + +/// Return an instance implementing the "spectest" interface used in the +/// spec testsuite. +pub fn spectest_importobject() -> ImportObject { + let print = Func::new(|| {}); + let print_i32 = Func::new(|val: i32| println!("{}: i32", val)); + let print_i64 = Func::new(|val: i64| println!("{}: i64", val)); + let print_f32 = Func::new(|val: f32| println!("{}: f32", val)); + let print_f64 = Func::new(|val: f64| println!("{}: f64", val)); + let print_i32_f32 = Func::new(|i: i32, f: f32| { + println!("{}: i32", i); + println!("{}: f32", f); + }); + let print_f64_f64 = Func::new(|f1: f64, f2: f64| { + println!("{}: f64", f1); + println!("{}: f64", f2); + }); + + let global_i32 = Global::new(Value::I32(666)); + let global_i64 = Global::new(Value::I64(666)); + let global_f32 = Global::new(Value::F32(f32::from_bits(0x4426_8000))); + let global_f64 = Global::new(Value::F64(f64::from_bits(0x4084_d000_0000_0000))); + + let memory_desc = MemoryDescriptor::new(Pages(1), Some(Pages(2)), false).unwrap(); + let memory = Memory::new(memory_desc).unwrap(); + + let table = Table::new(TableDescriptor { + element: ElementType::Anyfunc, + minimum: 10, + maximum: Some(20), + }) + .unwrap(); + + imports! { + "spectest" => { + "print" => print, + "print_i32" => print_i32, + "print_i64" => print_i64, + "print_f32" => print_f32, + "print_f64" => print_f64, + "print_i32_f32" => print_i32_f32, + "print_f64_f64" => print_f64_f64, + "global_i32" => global_i32, + "global_i64" => global_i64, + "global_f32" => global_f32, + "global_f64" => global_f64, + "table" => table, + "memory" => memory, + }, + } +} diff --git a/tests/wast/src/wast.rs b/tests/wast/src/wast.rs new file mode 100644 index 000000000..1497c5d43 --- /dev/null +++ b/tests/wast/src/wast.rs @@ -0,0 +1,506 @@ +use crate::errors::{CallError, DirectiveError, DirectiveErrors}; +use crate::spectest::spectest_importobject; +use anyhow::{anyhow, bail, Context as _, Result}; +use std::borrow::Borrow; +use std::collections::HashMap; +use std::path::Path; +use std::str; +use std::sync::{Arc, Mutex}; +use wasmer::compiler::{compile_with_config_with, compiler_for_backend, Backend, CompilerConfig}; +use wasmer::import::ImportObject; +use wasmer::wasm::Value; +use wasmer::wasm::{DynFunc, Features, Global}; +use wasmer::*; + +/// The wast test script language allows modules to be defined and actions +/// to be performed on them. +pub struct Wast { + /// Wast files have a concept of a "current" module, which is the most + /// recently defined. + current: Option>>, + /// The Import Object that all wast tests will have + import_object: ImportObject, + /// The instances in the test + instances: HashMap>>, + /// The Wasmer backend used + backend: Backend, + /// A flag indicating if Wast tests should stop as soon as one test fails. + pub fail_fast: bool, +} + +impl Wast { + /// Construct a new instance of `Wast` with a given imports. + pub fn new(import_object: ImportObject, backend: Backend) -> Self { + Self { + current: None, + backend, + import_object, + instances: HashMap::new(), + fail_fast: false, + } + } + + /// Construcet a new instance of `Wast` with the spectests imports. + pub fn new_with_spectest(backend: Backend) -> Self { + let import_object = spectest_importobject(); + Self::new(import_object, backend) + } + + fn get_instance(&self, instance_name: Option<&str>) -> Result>> { + match instance_name { + Some(name) => self + .instances + .get(name) + .as_ref() + .map(|x| Arc::clone(x)) + .ok_or_else(|| anyhow!("failed to find instance named `{}`", name)), + None => self + .current + .as_ref() + .map(|x| Arc::clone(x)) + .ok_or_else(|| anyhow!("no previous instance found")), + } + } + + /// Perform the action portion of a command. + fn perform_execute(&mut self, exec: wast::WastExecute<'_>) -> Result> { + match exec { + wast::WastExecute::Invoke(invoke) => self.perform_invoke(invoke), + wast::WastExecute::Module(mut module) => { + let binary = module.encode()?; + let result = self.instantiate(&binary); + match result { + Ok(_) => Ok(Vec::new()), + Err(e) => Err(e), + } + } + wast::WastExecute::Get { module, global } => self.get(module.map(|s| s.name()), global), + } + } + + fn perform_invoke(&mut self, exec: wast::WastInvoke<'_>) -> Result> { + let values = exec + .args + .iter() + .map(Self::runtime_value) + .collect::>>()?; + self.invoke(exec.module.map(|i| i.name()), exec.name, &values) + } + + fn assert_return( + &self, + result: Result>, + results: &[wast::AssertExpression], + ) -> Result<()> { + let values = result?; + for (v, e) in values.iter().zip(results) { + if val_matches(v, e)? { + continue; + } + bail!("expected {:?}, got {:?}", e, v) + } + Ok(()) + } + + fn assert_trap(&self, result: Result>, expected: &str) -> Result<()> { + let actual = match result { + Ok(values) => bail!("expected trap, got {:?}", values), + Err(t) => format!("{}", t), + }; + if Self::matches_message_assert_trap(expected, &actual) { + return Ok(()); + } + bail!("expected '{}', got '{}'", expected, actual) + } + + fn run_directive(&mut self, directive: wast::WastDirective) -> Result<()> { + use wast::WastDirective::*; + + match directive { + Module(mut module) => { + let binary = module.encode()?; + self.module(module.id.map(|s| s.name()), &binary)?; + } + Register { + span: _, + name, + module, + } => { + self.register(module.map(|s| s.name()), name)?; + } + Invoke(i) => { + self.perform_invoke(i)?; + } + AssertReturn { + span: _, + exec, + results, + } => { + let result = self.perform_execute(exec); + self.assert_return(result, &results)?; + } + AssertTrap { + span: _, + exec, + message, + } => { + let result = self.perform_execute(exec); + self.assert_trap(result, message)?; + } + AssertExhaustion { + span: _, + call, + message, + } => { + let result = self.perform_invoke(call); + self.assert_trap(result, message)?; + } + AssertInvalid { + span: _, + mut module, + message, + } => { + let bytes = module.encode()?; + let err = match self.module(None, &bytes) { + Ok(()) => bail!("expected module to fail to build"), + Err(e) => e, + }; + let error_message = format!("{:?}", err); + if !Self::matches_message_assert_invalid(&message, &error_message) { + bail!( + "assert_invalid: expected \"{}\", got \"{}\"", + message, + error_message + ) + } + } + AssertMalformed { + module, + span: _, + message: _, + } => { + let mut module = match module { + wast::QuoteModule::Module(m) => m, + // This is a `*.wat` parser test which we're not + // interested in. + wast::QuoteModule::Quote(_) => return Ok(()), + }; + let bytes = module.encode()?; + if let Ok(_) = self.module(None, &bytes) { + bail!("expected malformed module to fail to instantiate"); + } + } + AssertUnlinkable { + span: _, + mut module, + message, + } => { + let bytes = module.encode()?; + let err = match self.module(None, &bytes) { + Ok(()) => bail!("expected module to fail to link"), + Err(e) => e, + }; + let error_message = format!("{:?}", err); + if !Self::matches_message_assert_unlinkable(&message, &error_message) { + bail!( + "assert_unlinkable: expected {}, got {}", + message, + error_message + ) + } + } + } + + Ok(()) + } + + /// Run a wast script from a byte buffer. + pub fn run_buffer(&mut self, filename: &str, wast: &[u8]) -> Result<()> { + let wast = str::from_utf8(wast)?; + + let adjust_wast = |mut err: wast::Error| { + err.set_path(filename.as_ref()); + err.set_text(wast); + err + }; + + let buf = wast::parser::ParseBuffer::new(wast).map_err(adjust_wast)?; + let ast = wast::parser::parse::(&buf).map_err(adjust_wast)?; + let mut errors = Vec::with_capacity(ast.directives.len()); + for directive in ast.directives { + let sp = directive.span(); + match self.run_directive(directive) { + Err(e) => { + let (line, col) = sp.linecol_in(wast); + errors.push(DirectiveError { + line: line + 1, + col, + message: format!("{}", e), + }); + if self.fail_fast { + break; + } + } + Ok(_) => {} + } + } + if !errors.is_empty() { + return Err(DirectiveErrors { + filename: filename.to_string(), + errors, + } + .into()); + } + Ok(()) + } + + /// Run a wast script from a file. + pub fn run_file(&mut self, path: &Path) -> Result<()> { + let bytes = + std::fs::read(path).with_context(|| format!("failed to read `{}`", path.display()))?; + self.run_buffer(path.to_str().unwrap(), &bytes) + } +} + +// This is the implementation specific to the Runtime +impl Wast { + /// Define a module and register it. + fn module(&mut self, instance_name: Option<&str>, module: &[u8]) -> Result<()> { + let instance = match self.instantiate(module) { + Ok(i) => i, + Err(e) => bail!("instantiation failed with: {}", e), + }; + let instance = Arc::new(Mutex::new(instance)); + if let Some(name) = instance_name { + self.instances.insert(name.to_string(), instance.clone()); + } + self.current = Some(instance.clone()); + Ok(()) + } + + fn instantiate(&self, module: &[u8]) -> Result { + // let module = Module::new(module)?; + let config = CompilerConfig { + features: Features { + simd: true, + threads: true, + }, + nan_canonicalization: true, + enable_verification: true, + ..Default::default() + }; + let compiler = compiler_for_backend(self.backend).expect("backend not found"); + let module = compile_with_config_with(module, config, &*compiler)?; + + let mut imports = self.import_object.clone_ref(); + + for import in module.imports() { + let module_name = import.namespace; + if imports.contains_namespace(&module_name) { + continue; + } + let instance = self + .instances + .get(&module_name) + .ok_or_else(|| anyhow!("no module named `{}`", module_name))?; + imports.register(module_name, Arc::clone(instance)); + } + + let instance = module + .instantiate(&imports) + .map_err(|e| anyhow!("Instantiate error: {}", e))?; + Ok(instance) + } + + /// Register an instance to make it available for performing actions. + fn register(&mut self, name: Option<&str>, as_name: &str) -> Result<()> { + let instance = self.get_instance(name)?; + self.instances.insert(as_name.to_string(), instance); + Ok(()) + } + + /// Invoke an exported function from an instance. + fn invoke( + &mut self, + instance_name: Option<&str>, + field: &str, + args: &[Value], + ) -> Result> { + let clonable_instance = self.get_instance(instance_name)?; + let instance = clonable_instance.lock().unwrap(); + let func: DynFunc = instance.borrow().exports.get(field)?; + match func.call(args) { + Ok(result) => Ok(result.into()), + Err(e) => Err(CallError { + message: format!("{}", e), + } + .into()), + } + } + + /// Get the value of an exported global from an instance. + fn get(&mut self, instance_name: Option<&str>, field: &str) -> Result> { + let clonable_instance = self.get_instance(instance_name)?; + let instance = clonable_instance.lock().unwrap(); + let global: Global = instance.borrow().exports.get(field)?; + Ok(vec![global.get()]) + } + + /// Translate from a `script::Value` to a `Val`. + fn runtime_value(v: &wast::Expression<'_>) -> Result { + use wast::Instruction::*; + + if v.instrs.len() != 1 { + bail!("too many instructions in {:?}", v); + } + Ok(match &v.instrs[0] { + I32Const(x) => Value::I32(*x), + I64Const(x) => Value::I64(*x), + F32Const(x) => Value::F32(f32::from_bits(x.bits)), + F64Const(x) => Value::F64(f64::from_bits(x.bits)), + V128Const(x) => Value::V128(u128::from_le_bytes(x.to_le_bytes())), + other => bail!("couldn't convert {:?} to a runtime value", other), + }) + } + + // Checks if the `assert_unlinkable` message matches the expected one + fn matches_message_assert_unlinkable(_expected: &str, _actual: &str) -> bool { + // We skip message matching for now + true + // actual.contains(&expected) + } + + // Checks if the `assert_invalid` message matches the expected one + fn matches_message_assert_invalid(_expected: &str, _actual: &str) -> bool { + // We skip message matching for now + true + // actual.contains(expected) + // // Waiting on https://github.com/WebAssembly/bulk-memory-operations/pull/137 + // // to propagate to WebAssembly/testsuite. + // || (expected.contains("unknown table") && actual.contains("unknown elem")) + // // `elem.wast` and `proposals/bulk-memory-operations/elem.wast` disagree + // // on the expected error message for the same error. + // || (expected.contains("out of bounds") && actual.contains("does not fit")) + } + + // Checks if the `assert_trap` message matches the expected one + fn matches_message_assert_trap(_expected: &str, _actual: &str) -> bool { + // We skip message matching for now + true + // actual.contains(expected) + // // `bulk-memory-operations/bulk.wast` checks for a message that + // // specifies which element is uninitialized, but our traps don't + // // shepherd that information out. + // || (expected.contains("uninitialized element 2") && actual.contains("uninitialized element")) + } +} + +fn extract_lane_as_i8(bytes: u128, lane: usize) -> i8 { + (bytes >> (lane * 8)) as i8 +} + +fn extract_lane_as_i16(bytes: u128, lane: usize) -> i16 { + (bytes >> (lane * 16)) as i16 +} + +fn extract_lane_as_i32(bytes: u128, lane: usize) -> i32 { + (bytes >> (lane * 32)) as i32 +} + +fn extract_lane_as_i64(bytes: u128, lane: usize) -> i64 { + (bytes >> (lane * 64)) as i64 +} + +fn val_matches(actual: &Value, expected: &wast::AssertExpression) -> Result { + Ok(match (actual, expected) { + (Value::I32(a), wast::AssertExpression::I32(b)) => a == b, + (Value::I64(a), wast::AssertExpression::I64(b)) => a == b, + // Note that these float comparisons are comparing bits, not float + // values, so we're testing for bit-for-bit equivalence + (Value::F32(a), wast::AssertExpression::F32(b)) => f32_matches(a.to_bits(), &b), + (Value::F64(a), wast::AssertExpression::F64(b)) => f64_matches(a.to_bits(), &b), + (Value::V128(a), wast::AssertExpression::V128(b)) => v128_matches(*a, &b), + // Legacy comparators + (Value::F32(a), wast::AssertExpression::LegacyCanonicalNaN) => a.is_canonical_nan(), + (Value::F32(a), wast::AssertExpression::LegacyArithmeticNaN) => a.is_arithmetic_nan(), + (Value::F64(a), wast::AssertExpression::LegacyCanonicalNaN) => a.is_canonical_nan(), + (Value::F64(a), wast::AssertExpression::LegacyArithmeticNaN) => a.is_arithmetic_nan(), + _ => bail!( + "don't know how to compare {:?} and {:?} yet", + actual, + expected + ), + }) +} + +fn f32_matches(actual: u32, expected: &wast::NanPattern) -> bool { + match expected { + wast::NanPattern::CanonicalNan => f32::from_bits(actual).is_canonical_nan(), + wast::NanPattern::ArithmeticNan => f32::from_bits(actual).is_arithmetic_nan(), + wast::NanPattern::Value(expected_value) => actual == expected_value.bits, + } +} + +fn f64_matches(actual: u64, expected: &wast::NanPattern) -> bool { + match expected { + wast::NanPattern::CanonicalNan => f64::from_bits(actual).is_canonical_nan(), + wast::NanPattern::ArithmeticNan => f64::from_bits(actual).is_arithmetic_nan(), + wast::NanPattern::Value(expected_value) => actual == expected_value.bits, + } +} + +fn v128_matches(actual: u128, expected: &wast::V128Pattern) -> bool { + match expected { + wast::V128Pattern::I8x16(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i8(actual, i)), + wast::V128Pattern::I16x8(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i16(actual, i)), + wast::V128Pattern::I32x4(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i32(actual, i)), + wast::V128Pattern::I64x2(b) => b + .iter() + .enumerate() + .all(|(i, b)| *b == extract_lane_as_i64(actual, i)), + wast::V128Pattern::F32x4(b) => b.iter().enumerate().all(|(i, b)| { + let a = extract_lane_as_i32(actual, i) as u32; + f32_matches(a, b) + }), + wast::V128Pattern::F64x2(b) => b.iter().enumerate().all(|(i, b)| { + let a = extract_lane_as_i64(actual, i) as u64; + f64_matches(a, b) + }), + } +} + +pub trait NaNCheck { + fn is_arithmetic_nan(&self) -> bool; + fn is_canonical_nan(&self) -> bool; +} + +impl NaNCheck for f32 { + fn is_arithmetic_nan(&self) -> bool { + const AF32_NAN: u32 = 0x0040_0000; + (self.to_bits() & AF32_NAN) == AF32_NAN + } + + fn is_canonical_nan(&self) -> bool { + return (self.to_bits() & 0x7fff_ffff) == 0x7fc0_0000; + } +} + +impl NaNCheck for f64 { + fn is_arithmetic_nan(&self) -> bool { + const AF64_NAN: u64 = 0x0008_0000_0000_0000; + (self.to_bits() & AF64_NAN) == AF64_NAN + } + + fn is_canonical_nan(&self) -> bool { + (self.to_bits() & 0x7fff_ffff_ffff_ffff) == 0x7ff8_0000_0000_0000 + } +}