Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
155 changes: 155 additions & 0 deletions loom-core/tests/spec_features.rs
Original file line number Diff line number Diff line change
@@ -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<loom_core::Module>),
}

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);
}
25 changes: 25 additions & 0 deletions tests/fixtures/spec-features/bulk_memory_minimal.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
21 changes: 21 additions & 0 deletions tests/fixtures/spec-features/exception_handling_minimal.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
)
)
23 changes: 23 additions & 0 deletions tests/fixtures/spec-features/multi_memory_minimal.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
20 changes: 20 additions & 0 deletions tests/fixtures/spec-features/ref_types_minimal.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
28 changes: 28 additions & 0 deletions tests/fixtures/spec-features/saturating_trunc.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
24 changes: 24 additions & 0 deletions tests/fixtures/spec-features/sign_extension_ops.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
11 changes: 11 additions & 0 deletions tests/fixtures/spec-features/simd_v128_minimal.wat
Original file line number Diff line number Diff line change
@@ -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
)
)
23 changes: 23 additions & 0 deletions tests/fixtures/spec-features/tail_calls_minimal.wat
Original file line number Diff line number Diff line change
@@ -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))
)
Loading