diff --git a/loom-core/tests/spec_features.rs b/loom-core/tests/spec_features.rs new file mode 100644 index 0000000..abe3e29 --- /dev/null +++ b/loom-core/tests/spec_features.rs @@ -0,0 +1,155 @@ +//! Spec-feature coverage tests: post-MVP WebAssembly features. +//! +//! v0.4.0 audit found LOOM's test corpus had zero fixtures using post-MVP +//! features (SIMD, ref types, GC, tail calls, EH, threads, multi-memory). +//! The parser is expected to *reject* unsupported instructions cleanly +//! (return `Err`, never panic). Where LOOM does support the feature, we +//! exercise the full optimize + round-trip path. +//! +//! Per CLAUDE.md: rejection paths matter as much as happy paths. We assert +//! every fixture (a) does not panic the parser, and (b) either succeeds +//! end-to-end or fails with a recognizable diagnostic. + +use loom_core::{encode, optimize, parse}; +use std::panic; + +/// Outcome bucket for a single fixture run. +#[derive(Debug)] +enum Outcome { + /// Parser panicked — should never happen. + Panicked, + /// Parser returned a clean error. + Rejected(String), + /// Parser succeeded; full module is available. + Accepted(Box), +} + +fn classify(wat: &str) -> Outcome { + let result = panic::catch_unwind(panic::AssertUnwindSafe(|| parse::parse_wat(wat))); + match result { + Err(_) => Outcome::Panicked, + Ok(Ok(module)) => Outcome::Accepted(Box::new(module)), + Ok(Err(e)) => Outcome::Rejected(format!("{e:#}")), + } +} + +/// For features LOOM does not support, we require: +/// * no panic +/// * a clean Err (or, if the parser unexpectedly accepts, a successful +/// re-encode — anything except a panic is acceptable; we just want to +/// pin the contract). +fn assert_no_panic(name: &str, wat: &str) { + match classify(wat) { + Outcome::Panicked => panic!("parser panicked on {name}"), + Outcome::Rejected(msg) => { + // Sanity: error string should be non-empty. + assert!( + !msg.is_empty(), + "{name}: rejection produced empty error message" + ); + eprintln!("{name}: rejected cleanly: {msg}"); + } + Outcome::Accepted(module) => { + // Surprise — parser accepted. Make sure encode also doesn't panic. + let encode_result = + panic::catch_unwind(panic::AssertUnwindSafe(|| encode::encode_wasm(&module))); + assert!( + encode_result.is_ok(), + "{name}: parser accepted but encoder panicked" + ); + eprintln!("{name}: parser accepted (likely partial support)"); + } + } +} + +/// For LOOM-supported features, we want full optimize + re-encode round-trip. +fn assert_optimize_roundtrip(name: &str, wat: &str) { + let outcome = classify(wat); + let mut module = match outcome { + Outcome::Panicked => panic!("parser panicked on {name}"), + Outcome::Rejected(msg) => panic!("{name}: expected acceptance, got rejection: {msg}"), + Outcome::Accepted(m) => *m, + }; + + let opt_result = panic::catch_unwind(panic::AssertUnwindSafe(|| { + optimize::optimize_module(&mut module) + })); + assert!(opt_result.is_ok(), "{name}: optimizer panicked"); + opt_result + .unwrap() + .unwrap_or_else(|e| panic!("{name}: optimize_module returned Err: {e:#}")); + + let enc_result = panic::catch_unwind(panic::AssertUnwindSafe(|| encode::encode_wasm(&module))); + assert!(enc_result.is_ok(), "{name}: encoder panicked"); + let bytes = enc_result + .unwrap() + .unwrap_or_else(|e| panic!("{name}: encode_wasm returned Err: {e:#}")); + + // Re-parse the encoded bytes to confirm round-trip stability. + let reparse = panic::catch_unwind(panic::AssertUnwindSafe(|| parse::parse_wasm(&bytes))); + assert!(reparse.is_ok(), "{name}: re-parser panicked"); + reparse + .unwrap() + .unwrap_or_else(|e| panic!("{name}: re-parse failed: {e:#}")); +} + +// --------------------------------------------------------------------------- +// Post-MVP features LOOM does NOT yet support — assert clean rejection only. +// --------------------------------------------------------------------------- + +#[test] +fn spec_feature_simd_v128_does_not_panic() { + let wat = include_str!("../../tests/fixtures/spec-features/simd_v128_minimal.wat"); + assert_no_panic("simd_v128", wat); +} + +#[test] +fn spec_feature_ref_types_does_not_panic() { + let wat = include_str!("../../tests/fixtures/spec-features/ref_types_minimal.wat"); + assert_no_panic("ref_types", wat); +} + +#[test] +fn spec_feature_tail_calls_does_not_panic() { + let wat = include_str!("../../tests/fixtures/spec-features/tail_calls_minimal.wat"); + assert_no_panic("tail_calls", wat); +} + +#[test] +fn spec_feature_exception_handling_does_not_panic() { + let wat = include_str!("../../tests/fixtures/spec-features/exception_handling_minimal.wat"); + assert_no_panic("exception_handling", wat); +} + +// --------------------------------------------------------------------------- +// Post-MVP features LOOM partially supports — accept either rejection or +// successful round-trip; never panic. +// --------------------------------------------------------------------------- + +#[test] +fn spec_feature_bulk_memory_does_not_panic() { + let wat = include_str!("../../tests/fixtures/spec-features/bulk_memory_minimal.wat"); + assert_no_panic("bulk_memory", wat); +} + +#[test] +fn spec_feature_multi_memory_does_not_panic() { + let wat = include_str!("../../tests/fixtures/spec-features/multi_memory_minimal.wat"); + assert_no_panic("multi_memory", wat); +} + +// --------------------------------------------------------------------------- +// Standardized features LOOM fully supports — full optimize + round-trip. +// --------------------------------------------------------------------------- + +#[test] +fn spec_feature_sign_extension_ops_round_trip() { + let wat = include_str!("../../tests/fixtures/spec-features/sign_extension_ops.wat"); + assert_optimize_roundtrip("sign_extension_ops", wat); +} + +#[test] +fn spec_feature_saturating_trunc_round_trip() { + let wat = include_str!("../../tests/fixtures/spec-features/saturating_trunc.wat"); + assert_optimize_roundtrip("saturating_trunc", wat); +} diff --git a/tests/fixtures/spec-features/bulk_memory_minimal.wat b/tests/fixtures/spec-features/bulk_memory_minimal.wat new file mode 100644 index 0000000..786d466 --- /dev/null +++ b/tests/fixtures/spec-features/bulk_memory_minimal.wat @@ -0,0 +1,25 @@ +;; Bulk memory ops minimal fixture (post-MVP wasm feature, partial LOOM support) +;; Exercises memory.copy, memory.fill, memory.init, data.drop. +(module + (memory 1) + (data $d "hello world") + (func (export "bulk_test") + ;; memory.fill: dst=0, val=0, len=4 + i32.const 0 + i32.const 0 + i32.const 4 + memory.fill + ;; memory.copy: dst=8, src=0, len=4 + i32.const 8 + i32.const 0 + i32.const 4 + memory.copy + ;; memory.init from data segment $d: dst=16, src=0, len=11 + i32.const 16 + i32.const 0 + i32.const 11 + memory.init $d + ;; data.drop: drop the data segment + data.drop $d + ) +) diff --git a/tests/fixtures/spec-features/exception_handling_minimal.wat b/tests/fixtures/spec-features/exception_handling_minimal.wat new file mode 100644 index 0000000..ed7cc03 --- /dev/null +++ b/tests/fixtures/spec-features/exception_handling_minimal.wat @@ -0,0 +1,21 @@ +;; Exception handling minimal fixture (post-MVP wasm feature) +;; LOOM does not currently support EH; parser must reject cleanly (no panic). +;; Uses the legacy try/catch encoding plus throw/throw_ref for breadth. +(module + (tag $exn (param i32)) + (func (export "eh_test") (result i32) + (try (result i32) + (do + i32.const 1 + throw $exn + i32.const 0 + ) + (catch $exn + ;; on catch the i32 payload is on the stack + ) + (catch_all + i32.const -1 + ) + ) + ) +) diff --git a/tests/fixtures/spec-features/multi_memory_minimal.wat b/tests/fixtures/spec-features/multi_memory_minimal.wat new file mode 100644 index 0000000..1503086 --- /dev/null +++ b/tests/fixtures/spec-features/multi_memory_minimal.wat @@ -0,0 +1,23 @@ +;; Multi-memory minimal fixture (post-MVP wasm feature, partial LOOM support) +;; Two memories with explicit memarg `mem` index on loads/stores. +(module + (memory $m0 1) + (memory $m1 1) + (func (export "mm_test") (result i32) + ;; store 42 at offset 0 of memory $m1 + i32.const 0 + i32.const 42 + i32.store (memory $m1) + ;; load from memory $m1 + i32.const 0 + i32.load (memory $m1) + ;; store 7 at offset 0 of memory $m0 + i32.const 0 + i32.const 7 + i32.store (memory $m0) + ;; load from memory $m0 and add + i32.const 0 + i32.load (memory $m0) + i32.add + ) +) diff --git a/tests/fixtures/spec-features/ref_types_minimal.wat b/tests/fixtures/spec-features/ref_types_minimal.wat new file mode 100644 index 0000000..c8aec29 --- /dev/null +++ b/tests/fixtures/spec-features/ref_types_minimal.wat @@ -0,0 +1,20 @@ +;; Reference types minimal fixture (post-MVP wasm feature) +;; Exercises ref.null, ref.func, table.get on a funcref table. +(module + (table $t 2 funcref) + (func $callee (result i32) + i32.const 7 + ) + (elem (i32.const 0) $callee) + (func (export "ref_test") (result funcref) + ;; ref.null funcref + ref.null func + drop + ;; table.get + i32.const 0 + table.get $t + drop + ;; ref.func returning a funcref + ref.func $callee + ) +) diff --git a/tests/fixtures/spec-features/saturating_trunc.wat b/tests/fixtures/spec-features/saturating_trunc.wat new file mode 100644 index 0000000..c6b387b --- /dev/null +++ b/tests/fixtures/spec-features/saturating_trunc.wat @@ -0,0 +1,28 @@ +;; Non-trapping float-to-int (saturating truncation), LOOM-supported. +;; Should parse, optimize, and round-trip cleanly. +(module + (func (export "i32_trunc_sat_f32_s") (param f32) (result i32) + local.get 0 + i32.trunc_sat_f32_s + ) + (func (export "i32_trunc_sat_f32_u") (param f32) (result i32) + local.get 0 + i32.trunc_sat_f32_u + ) + (func (export "i32_trunc_sat_f64_s") (param f64) (result i32) + local.get 0 + i32.trunc_sat_f64_s + ) + (func (export "i32_trunc_sat_f64_u") (param f64) (result i32) + local.get 0 + i32.trunc_sat_f64_u + ) + (func (export "i64_trunc_sat_f32_s") (param f32) (result i64) + local.get 0 + i64.trunc_sat_f32_s + ) + (func (export "i64_trunc_sat_f64_u") (param f64) (result i64) + local.get 0 + i64.trunc_sat_f64_u + ) +) diff --git a/tests/fixtures/spec-features/sign_extension_ops.wat b/tests/fixtures/spec-features/sign_extension_ops.wat new file mode 100644 index 0000000..662aebd --- /dev/null +++ b/tests/fixtures/spec-features/sign_extension_ops.wat @@ -0,0 +1,24 @@ +;; Sign-extension operators (wasm 1.1 / standardized feature, LOOM-supported). +;; Should parse, optimize, and round-trip cleanly. +(module + (func (export "ext8_s") (param i32) (result i32) + local.get 0 + i32.extend8_s + ) + (func (export "ext16_s") (param i32) (result i32) + local.get 0 + i32.extend16_s + ) + (func (export "i64_ext8_s") (param i64) (result i64) + local.get 0 + i64.extend8_s + ) + (func (export "i64_ext16_s") (param i64) (result i64) + local.get 0 + i64.extend16_s + ) + (func (export "i64_ext32_s") (param i64) (result i64) + local.get 0 + i64.extend32_s + ) +) diff --git a/tests/fixtures/spec-features/simd_v128_minimal.wat b/tests/fixtures/spec-features/simd_v128_minimal.wat new file mode 100644 index 0000000..baa35e3 --- /dev/null +++ b/tests/fixtures/spec-features/simd_v128_minimal.wat @@ -0,0 +1,11 @@ +;; SIMD/v128 minimal fixture (post-MVP wasm feature) +;; LOOM does not currently support SIMD; parser must reject cleanly (no panic). +(module + (func (export "simd_test") (result v128) + (v128.const i8x16 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15) + (v128.const i8x16 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1 1) + i8x16.add + (v128.const i8x16 1 0 1 0 1 0 1 0 1 0 1 0 1 0 1 0) + v128.bitselect + ) +) diff --git a/tests/fixtures/spec-features/tail_calls_minimal.wat b/tests/fixtures/spec-features/tail_calls_minimal.wat new file mode 100644 index 0000000..528a7c6 --- /dev/null +++ b/tests/fixtures/spec-features/tail_calls_minimal.wat @@ -0,0 +1,23 @@ +;; Tail calls minimal fixture (post-MVP wasm feature) +;; LOOM does not currently support tail calls; parser must reject cleanly. +(module + (type $sig (func (param i32) (result i32))) + (table $t 1 funcref) + (elem (i32.const 0) $direct) + (func $direct (param i32) (result i32) + local.get 0 + i32.const 1 + i32.add + ) + (func $tail_direct (param i32) (result i32) + local.get 0 + return_call $direct + ) + (func $tail_indirect (param i32) (result i32) + local.get 0 + i32.const 0 + return_call_indirect (type $sig) + ) + (export "tail_direct" (func $tail_direct)) + (export "tail_indirect" (func $tail_indirect)) +)