diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 62f8c901..ad6f312c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -21,6 +21,8 @@ jobs: steps: - uses: actions/checkout@v4 + with: + submodules: true - name: Format run: cargo check - name: Run clippy @@ -39,11 +41,15 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: webiny/action-conventional-commits@v1.3.0 msrv: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: taiki-e/install-action@cargo-hack - run: cargo hack check --rust-version --workspace --all-targets --ignore-private diff --git a/.github/workflows/nix.yml b/.github/workflows/nix.yml index df57a3b0..f1fd174f 100644 --- a/.github/workflows/nix.yml +++ b/.github/workflows/nix.yml @@ -11,6 +11,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: cachix/install-nix-action@v27 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} @@ -26,6 +28,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: cachix/install-nix-action@v27 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} @@ -33,7 +37,7 @@ jobs: with: name: dlr-ft authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} - - run: nix build .#wasm-interpreter --print-build-logs + - run: nix build .?submodules=1#wasm-interpreter --print-build-logs - name: Upload coverage to Codecov uses: codecov/codecov-action@v4 with: @@ -41,7 +45,7 @@ jobs: file: result/lcov-codecov.json env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} - - run: nix build .#report --print-build-logs + - run: nix build .?submodules=1#report --print-build-logs - name: Archive report uses: actions/upload-artifact@v4 with: diff --git a/.github/workflows/pages_coverage_preview.yaml b/.github/workflows/pages_coverage_preview.yaml index ed11443c..c22b0ce0 100644 --- a/.github/workflows/pages_coverage_preview.yaml +++ b/.github/workflows/pages_coverage_preview.yaml @@ -23,6 +23,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true # -=-=-=-= Create report =-=-=-=- - uses: cachix/install-nix-action@v27 if: | diff --git a/.github/workflows/pages_deploy_main.yaml b/.github/workflows/pages_deploy_main.yaml index 2814d336..caf6b253 100644 --- a/.github/workflows/pages_deploy_main.yaml +++ b/.github/workflows/pages_deploy_main.yaml @@ -30,6 +30,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true - uses: cachix/install-nix-action@v27 with: github_access_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/pages_requirement_preview.yaml b/.github/workflows/pages_requirement_preview.yaml index 6fddaa4b..df09ab66 100644 --- a/.github/workflows/pages_requirement_preview.yaml +++ b/.github/workflows/pages_requirement_preview.yaml @@ -24,6 +24,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 + with: + submodules: true # -=-=-=-= Strictdoc =-=-=-=- - name: Install python uses: actions/setup-python@v5.1.0 diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 00000000..993e1db1 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "tests/specification/testsuite"] + path = tests/specification/testsuite + url = https://github.com/WebAssembly/testsuite.git diff --git a/Cargo.lock b/Cargo.lock index 02a96a65..3f58ba2e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -659,6 +659,15 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.212.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "501940df4418b8929eb6d52f1aade1fdd15a5b86c92453cb696e3c906bd3fc33" +dependencies = [ + "leb128", +] + [[package]] name = "wasm-interpreter" version = "0.1.0" @@ -671,6 +680,7 @@ dependencies = [ "log", "test-log", "wasmparser", + "wast 212.0.0", "wat", ] @@ -695,7 +705,20 @@ dependencies = [ "leb128", "memchr", "unicode-width", - "wasm-encoder", + "wasm-encoder 0.200.0", +] + +[[package]] +name = "wast" +version = "212.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4606a05fb0aae5d11dd7d8280a640d88a63ee019360ba9be552da3d294b8d1f5" +dependencies = [ + "bumpalo", + "leb128", + "memchr", + "unicode-width", + "wasm-encoder 0.212.0", ] [[package]] @@ -704,7 +727,7 @@ version = "1.200.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "776cbd10e217f83869beaa3f40e312bb9e91d5eee29bbf6f560db1261b6a4c3d" dependencies = [ - "wast", + "wast 200.0.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index c41468e0..c183fc92 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ env_logger = "0.10.1" wasmparser = "0.119.0" itertools = "0.12.0" wat = "1.0.83" +wast = "212.0.0" criterion = { version = "0.5.1", features = ["html_reports"] } hexf = "0.2.1" diff --git a/pkgs/wasm-interpreter.nix b/pkgs/wasm-interpreter.nix index 7455665a..2fd7ccff 100644 --- a/pkgs/wasm-interpreter.nix +++ b/pkgs/wasm-interpreter.nix @@ -13,7 +13,7 @@ rustPlatform.buildRustPackage rec { src = ./..; # File suffices to include - extensions = [ "lock" "rs" "toml" ]; + extensions = [ "lock" "rs" "toml" "wast" ]; # Files to explicitly include include = [ ]; # Files to explicitly exclude diff --git a/src/execution/mod.rs b/src/execution/mod.rs index 07a2f500..52155e34 100644 --- a/src/execution/mod.rs +++ b/src/execution/mod.rs @@ -96,6 +96,31 @@ where } } + pub fn invoke_named_dynamic( + &mut self, + func_name: &str, + param: Vec, + ret_types: &[ValType], + ) -> Result, RuntimeError> { + // TODO: Optimize this search for better than linear-time. Pre-processing will likely be required + let func_idx = self.exports.iter().find_map(|export| { + if export.name == func_name { + match export.desc { + ExportDesc::FuncIdx(idx) => Some(idx), + _ => None, + } + } else { + None + } + }); + + if let Some(func_idx) = func_idx { + self.invoke_dynamic(func_idx, param, ret_types) + } else { + Err(RuntimeError::FunctionNotFound) + } + } + /// Can only invoke functions with signature `[t1] -> [t2]` as of now. pub fn invoke_func( &mut self, diff --git a/tests/specification/mod.rs b/tests/specification/mod.rs new file mode 100644 index 00000000..1874d3c5 --- /dev/null +++ b/tests/specification/mod.rs @@ -0,0 +1,64 @@ +use std::{ + fs, + path::{Path, PathBuf}, +}; + +mod reports; +mod run; +mod test_errors; + +#[test_log::test] +pub fn spec_tests() { + let paths = get_wast_files(Path::new("./tests/specification/testsuite/")) + .expect("Failed to find testsuite"); + + let mut successful_reports = 0; + let mut failed_reports = 0; + let mut compile_error_reports = 0; + + for test_path in paths { + println!("Report for {}:", test_path.display()); + let report = run::run_spec_test(test_path.to_str().unwrap()); + println!("{}", report); + + match report { + reports::WastTestReport::Asserts(assert_report) => { + if assert_report.has_errors() { + failed_reports += 1; + } else { + successful_reports += 1; + } + } + reports::WastTestReport::CompilationError(_) => { + compile_error_reports += 1; + } + } + } + + println!( + "Tests: {} Passed, {} Failed, {} Compilation Errors", + successful_reports, failed_reports, compile_error_reports + ); +} + +// See: https://stackoverflow.com/a/76820878 +fn get_wast_files(base_path: &Path) -> Result, std::io::Error> { + let mut buf = vec![]; + let entries = fs::read_dir(base_path)?; + + for entry in entries { + let entry = entry?; + let meta = entry.metadata()?; + + if meta.is_dir() { + let mut subdir = get_wast_files(&entry.path())?; + buf.append(&mut subdir); + } + + if meta.is_file() && entry.path().extension().unwrap_or_default() == "wast" { + buf.push(entry.path()); + } + } + + Ok(buf) +} diff --git a/tests/specification/reports.rs b/tests/specification/reports.rs new file mode 100644 index 00000000..e4f0f54b --- /dev/null +++ b/tests/specification/reports.rs @@ -0,0 +1,143 @@ +use std::error::Error; + +pub struct WastSuccess { + line_number: u32, + command: String, +} + +impl WastSuccess { + pub fn new(line_number: u32, command: &str) -> Self { + Self { + line_number, + command: command.to_string(), + } + } +} + +pub struct WastError { + inner: Box, + line_number: Option, + command: String, +} + +impl WastError { + pub fn new(error: Box, line_number: u32, command: &str) -> Self { + Self { + inner: error, + line_number: Some(line_number), + command: command.to_string(), + } + } +} + +pub struct AssertReport { + filename: String, + results: Vec>, +} + +impl AssertReport { + pub fn new(filename: &str) -> Self { + Self { + filename: filename.to_string(), + results: Vec::new(), + } + } + + pub fn push_success(&mut self, success: WastSuccess) { + self.results.push(Ok(success)); + } + + pub fn push_error(&mut self, error: WastError) { + self.results.push(Err(error)); + } + + pub fn compile_report(self) -> WastTestReport { + return WastTestReport::Asserts(self); + } + + pub fn has_errors(&self) -> bool { + self.results.iter().any(|r| r.is_err()) + } +} + +pub struct CompilationError { + inner: Box, + filename: String, + context: String, +} + +impl CompilationError { + pub fn new(error: Box, filename: &str, context: &str) -> Self { + Self { + inner: error, + filename: filename.to_string(), + context: context.to_string(), + } + } + + pub fn compile_report(self) -> WastTestReport { + return WastTestReport::CompilationError(self); + } +} + +pub enum WastTestReport { + Asserts(AssertReport), + CompilationError(CompilationError), +} + +// .------------------------. +// | Display Implementation | +// '------------------------' + +impl std::fmt::Display for WastTestReport { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + WastTestReport::CompilationError(error) => { + writeln!(f, "------ {} ------", error.filename)?; + writeln!(f, "⚠ Compilation Failed ⚠")?; + writeln!(f, "Context: {}", error.context)?; + writeln!(f, "Error: {}", error.inner)?; + writeln!(f, "~~~~~~~~~~~~~~~~")?; + writeln!(f, "")?; + } + WastTestReport::Asserts(assert_report) => { + writeln!(f, "------ {} ------", assert_report.filename)?; + for result in &assert_report.results { + match result { + Ok(success) => { + writeln!( + f, + "✅ {}:{} -> {}", + assert_report.filename, success.line_number, success.command + )?; + } + Err(error) => { + writeln!( + f, + "❌ {}:{} -> {}", + assert_report.filename, + error.line_number.unwrap_or(0), + error.command + )?; + writeln!(f, " Error: {}", error.inner)?; + } + } + } + let passed_asserts = assert_report.results.iter().filter(|r| r.is_ok()).count(); + let failed_asserts = assert_report.results.iter().filter(|r| r.is_err()).count(); + let total_asserts = assert_report.results.len(); + + writeln!(f, "")?; + writeln!( + f, + "Execution finished. Passed: {}, Failed: {}, Total: {}", + passed_asserts, failed_asserts, total_asserts + )?; + writeln!(f, "~~~~~~~~~~~~~~~~")?; + writeln!(f, "")?; + } + } + + Ok(()) + } +} diff --git a/tests/specification/run.rs b/tests/specification/run.rs new file mode 100644 index 00000000..7c90c124 --- /dev/null +++ b/tests/specification/run.rs @@ -0,0 +1,342 @@ +use std::error::Error; +use std::panic::catch_unwind; +use std::panic::AssertUnwindSafe; + +use wasm::Value; +use wasm::{validate, RuntimeInstance}; +use wast::core::WastArgCore; +use wast::core::WastRetCore; +use wast::WastArg; + +use crate::specification::reports::*; +use crate::specification::test_errors::*; + +/// Attempt to unwrap the result of an expression. If the expression is an `Err`, then `return` the +/// error. +/// +/// # Motivation +/// The `Try` trait is not yet stable, so we define our own macro to simulate the `Result` type. +macro_rules! try_to { + ($e:expr) => { + match $e { + Ok(val) => val, + Err(err) => return err, + } + }; +} + +pub fn run_spec_test(filepath: &str) -> WastTestReport { + // -=-= Initialization =-=- + let contents = try_to!( + std::fs::read_to_string(filepath).map_err(|err| CompilationError::new( + Box::new(err), + filepath, + "failed to open wast file" + ) + .compile_report()) + ); + + let buf = + try_to!( + wast::parser::ParseBuffer::new(&contents).map_err(|err| CompilationError::new( + Box::new(err), + filepath, + "failed to create wast buffer" + ) + .compile_report()) + ); + + let wast = + try_to!( + wast::parser::parse::(&buf).map_err(|err| CompilationError::new( + Box::new(err), + filepath, + "failed to parse wast file" + ) + .compile_report()) + ); + + // -=-= Testing & Compilation =-=- + let mut asserts = AssertReport::new(filepath); + + // We need to keep the wasm_bytes in-scope for the lifetime of the interpeter. + // As such, we hoist the bytes into an Option, and assign it once a module directive is found. + #[allow(unused_assignments)] + let mut wasm_bytes: Option> = None; + let mut interpeter = None; + + for directive in wast.directives { + match directive { + wast::WastDirective::Wat(mut quoted) => { + // If we fail to compile or to validate the main module, then we should treat this + // as a fatal (compilation) error. + let encoded = try_to!(quoted.encode().map_err(|err| CompilationError::new( + Box::new(err), + filepath, + "failed to encode main module's wat" + ) + .compile_report())); + + wasm_bytes = Some(encoded); + + let validation_attempt = catch_unwind(|| { + validate(wasm_bytes.as_ref().unwrap()).map_err(|err| { + CompilationError::new( + Box::new(WasmInterpreterError(err)), + filepath, + "main module validation failed", + ) + .compile_report() + }) + }); + + let validation_info = match validation_attempt { + Ok(original_result) => try_to!(original_result), + Err(panic) => { + // TODO: Do we want to exit on panic? State may be left in an inconsistent state, and cascading panics may occur. + let err = if let Ok(msg) = panic.downcast::<&str>() { + Box::new(PanicError::new(&msg)) + } else { + Box::new(PanicError::new("Unknown panic")) + }; + + return CompilationError::new( + err, + filepath, + "main module validation panicked", + ) + .compile_report(); + } + }; + + let instance = try_to!(RuntimeInstance::new(&validation_info).map_err(|err| { + CompilationError::new( + Box::new(WasmInterpreterError(wasm::Error::RuntimeError(err))), + filepath, + "failed to create runtime instance", + ) + .compile_report() + })); + + interpeter = Some(instance); + } + wast::WastDirective::AssertReturn { + span, + exec, + results, + } => { + if interpeter.is_none() { + return CompilationError::new( + Box::new(GenericError::new( + "Attempted to assert before module directive", + )), + filepath, + "no module directive found", + ) + .compile_report(); + } + + let interpeter = interpeter.as_mut().unwrap(); + + let err_or_panic: Result<(), Box> = + match catch_unwind(AssertUnwindSafe(|| { + execute_assert_return(interpeter, exec, results) + })) { + Ok(original_result) => original_result, + Err(inner) => { + // TODO: Do we want to exit on panic? State may be left in an inconsistent state, and cascading panics may occur. + if let Ok(msg) = inner.downcast::<&str>() { + Err(Box::new(PanicError::new(&msg))) + } else { + Err(Box::new(PanicError::new("Unknown panic"))) + } + } + }; + + match err_or_panic { + Ok(_) => { + asserts.push_success(WastSuccess::new( + span.linecol_in(&contents).0 as u32 + 1, + get_command(&contents, span), + )); + } + Err(inner) => { + asserts.push_error(WastError::new( + inner, + span.linecol_in(&contents).0 as u32 + 1, + get_command(&contents, span), + )); + } + } + } + wast::WastDirective::AssertMalformed { + span, + module: _, + message: _, + } + | wast::WastDirective::AssertInvalid { + span, + module: _, + message: _, + } + | wast::WastDirective::Register { + span, + name: _, + module: _, + } + | wast::WastDirective::AssertTrap { + span, + exec: _, + message: _, + } + | wast::WastDirective::AssertExhaustion { + span, + call: _, + message: _, + } + | wast::WastDirective::AssertUnlinkable { + span, + module: _, + message: _, + } + | wast::WastDirective::AssertException { span, exec: _ } => { + asserts.push_error(WastError::new( + Box::new(GenericError::new("Assert directive not yet implemented")), + span.linecol_in(&contents).0 as u32 + 1, + get_command(&contents, span), + )); + } + wast::WastDirective::Wait { span, thread: _ } => { + asserts.push_error(WastError::new( + Box::new(GenericError::new("Wait directive not yet implemented")), + span.linecol_in(&contents).0 as u32 + 1, + get_command(&contents, span), + )); + } + wast::WastDirective::Invoke(invoke) => { + asserts.push_error(WastError::new( + Box::new(GenericError::new("Invoke directive not yet implemented")), + invoke.span.linecol_in(&contents).0 as u32 + 1, + get_command(&contents, invoke.span), + )); + } + wast::WastDirective::Thread(thread) => { + asserts.push_error(WastError::new( + Box::new(GenericError::new("Thread directive not yet implemented")), + thread.span.linecol_in(&contents).0 as u32 + 1, + get_command(&contents, thread.span), + )); + } + } + } + + asserts.compile_report() +} + +fn execute_assert_return( + interpeter: &mut RuntimeInstance, + exec: wast::WastExecute, + results: Vec, +) -> Result<(), Box> { + match exec { + wast::WastExecute::Invoke(invoke_info) => { + let args = invoke_info + .args + .into_iter() + .map(arg_to_value) + .collect::>(); + + let result_vals = results.into_iter().map(result_to_value).collect::>(); + let result_types = result_vals + .iter() + .map(|val| val.to_ty()) + .collect::>(); + + let actual = interpeter + .invoke_named_dynamic(invoke_info.name, args, &result_types) + .map_err(|err| Box::new(WasmInterpreterError(wasm::Error::RuntimeError(err))))?; + + AssertEqError::assert_eq(actual, result_vals)?; + } + wast::WastExecute::Get { + span: _, + module: _, + global: _, + } => todo!("`get` directive inside `assert_return` not yet implemented"), + wast::WastExecute::Wat(_) => { + todo!("`wat` directive inside `assert_return` not yet implemented") + } + } + + Ok(()) +} + +pub fn arg_to_value(arg: WastArg) -> Value { + match arg { + WastArg::Core(core_arg) => match core_arg { + WastArgCore::I32(val) => Value::I32(val as u32), + WastArgCore::I64(val) => Value::I64(val as u64), + WastArgCore::F32(val) => Value::F32(wasm::value::F32(val.bits as f32)), + WastArgCore::F64(val) => Value::F64(wasm::value::F64(val.bits as f64)), + WastArgCore::V128(_) => todo!("`V128` value arguments not yet implemented"), + WastArgCore::RefNull(_) => { + todo!("`RefNull` value arguments not yet implemented") + } + WastArgCore::RefExtern(_) => { + todo!("`RefExtern` value arguments not yet implemented") + } + WastArgCore::RefHost(_) => { + todo!("`RefHost` value arguments not yet implemented") + } + }, + WastArg::Component(_) => todo!("`Component` value arguments not yet implemented"), + } +} + +fn result_to_value(result: wast::WastRet) -> Value { + match result { + wast::WastRet::Core(core_arg) => match core_arg { + WastRetCore::I32(val) => Value::I32(val as u32), + WastRetCore::I64(val) => Value::I64(val as u64), + WastRetCore::F32(val) => match val { + wast::core::NanPattern::CanonicalNan => { + todo!("`F32::CanonicalNan` result not yet implemented") + } + wast::core::NanPattern::ArithmeticNan => { + todo!("`F32::ArithmeticNan` result not yet implemented") + } + wast::core::NanPattern::Value(val) => Value::F32(wasm::value::F32(val.bits as f32)), + }, + WastRetCore::F64(val) => match val { + wast::core::NanPattern::CanonicalNan => { + todo!("`F64::CanonicalNan` result not yet implemented") + } + wast::core::NanPattern::ArithmeticNan => { + todo!("`F64::ArithmeticNan` result not yet implemented") + } + wast::core::NanPattern::Value(val) => Value::F64(wasm::value::F64(val.bits as f64)), + }, + WastRetCore::V128(_) => todo!("`V128` result not yet implemented"), + WastRetCore::RefNull(_) => todo!("`RefNull` result not yet implemented"), + WastRetCore::RefExtern(_) => { + todo!("`RefExtern` result not yet implemented") + } + WastRetCore::RefHost(_) => todo!("`RefHost` result not yet implemented"), + WastRetCore::RefFunc(_) => todo!("`RefFunc` result not yet implemented"), + WastRetCore::RefAny => todo!("`RefAny` result not yet implemented"), + WastRetCore::RefEq => todo!("`RefEq` result not yet implemented"), + WastRetCore::RefArray => todo!("`RefArray` result not yet implemented"), + WastRetCore::RefStruct => todo!("`RefStruct` result not yet implemented"), + WastRetCore::RefI31 => todo!("`RefI31` result not yet implemented"), + WastRetCore::Either(_) => todo!("`Either` result not yet implemented"), + }, + wast::WastRet::Component(_) => todo!("`Component` result not yet implemented"), + } +} + +pub fn get_command(contents: &str, span: wast::token::Span) -> &str { + contents[span.offset() as usize..] + .lines() + .next() + .unwrap_or("") +} diff --git a/tests/specification/test_errors.rs b/tests/specification/test_errors.rs new file mode 100644 index 00000000..55101557 --- /dev/null +++ b/tests/specification/test_errors.rs @@ -0,0 +1,84 @@ +use std::error::Error; + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct AssertEqError { + left: String, + right: String, +} + +impl AssertEqError { + pub fn assert_eq(left: T, right: T) -> Result<(), Self> { + if left != right { + return Err(AssertEqError { + left: format!("{:?}", left), + right: format!("{:?}", right), + }); + } + + Ok(()) + } +} +impl Error for AssertEqError {} +impl std::fmt::Display for AssertEqError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "assert_eq failed: left: {}, right: {}", + self.left, self.right + ) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct PanicError { + message: String, +} + +impl PanicError { + pub fn new(message: &str) -> Self { + PanicError { + message: message.to_string(), + } + } +} + +impl Error for PanicError {} +impl std::fmt::Display for PanicError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "Panic: {}", self.message) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct WasmInterpreterError(pub wasm::Error); + +impl Error for WasmInterpreterError { + fn source(&self) -> Option<&(dyn Error + 'static)> { + match &self.0 { + wasm::Error::MalformedUtf8String(inner) => Some(inner), + _ => None, + } + } +} + +impl std::fmt::Display for WasmInterpreterError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct GenericError(String); + +impl GenericError { + pub fn new(message: &str) -> Self { + GenericError(message.to_string()) + } +} + +impl Error for GenericError {} +impl std::fmt::Display for GenericError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/tests/specification/testsuite b/tests/specification/testsuite new file mode 160000 index 00000000..7c3ec23a --- /dev/null +++ b/tests/specification/testsuite @@ -0,0 +1 @@ +Subproject commit 7c3ec23ab19b37c68976b555f9491752cbda6d5f diff --git a/tests/wasm_spec_testsuite.rs b/tests/wasm_spec_testsuite.rs new file mode 100644 index 00000000..377d7875 --- /dev/null +++ b/tests/wasm_spec_testsuite.rs @@ -0,0 +1,4 @@ +// The reason this file exists is only to expose the `specification` module to the outside world. +// More so, the reason it wasn't added to the `lib.rs` file is because we wanted to separate the +// regular tests from the spec tests. +mod specification;