diff --git a/.isu/issues.json b/.isu/issues.json index 35244ac..722c7e6 100644 --- a/.isu/issues.json +++ b/.isu/issues.json @@ -2734,7 +2734,7 @@ "labels": [], "assigned": [], "author": "pierrelf.com", - "state": "open", + "state": "closed", "created_at": "2026-05-17T17:01:44.710229+00:00" }, { diff --git a/crates/wasm/tests/exec_tests.rs b/crates/wasm/tests/exec_tests.rs index c8b3b83..e1486db 100644 --- a/crates/wasm/tests/exec_tests.rs +++ b/crates/wasm/tests/exec_tests.rs @@ -986,6 +986,112 @@ fn i64_trunc_sat_f64_s_saturates_to_min_max() { assert_eq!(v, i64::MIN); } +#[test] +fn i32_trunc_sat_f64_clamps_and_handles_nan() { + assert_eq!( + run_i32(&[ + Instruction::F64Const(f64::NAN), + Instruction::I32TruncSatF64S, + ]) + .unwrap(), + 0 + ); + assert_eq!( + run_i32(&[ + Instruction::F64Const(f64::INFINITY), + Instruction::I32TruncSatF64S, + ]) + .unwrap(), + i32::MAX + ); + assert_eq!( + run_i32(&[ + Instruction::F64Const(f64::NEG_INFINITY), + Instruction::I32TruncSatF64S, + ]) + .unwrap(), + i32::MIN + ); + // Negative input is clamped to 0 for the unsigned variant. + assert_eq!( + run_i32(&[Instruction::F64Const(-3.5), Instruction::I32TruncSatF64U]).unwrap(), + 0 + ); + // Overflow becomes u32::MAX (i.e., -1 as i32). + assert_eq!( + run_i32(&[Instruction::F64Const(1.0e30), Instruction::I32TruncSatF64U,]).unwrap(), + -1 + ); +} + +#[test] +fn i64_trunc_sat_f32_clamps_and_handles_nan() { + // NaN -> 0 (both signed and unsigned). + assert_eq!( + run_i64(&[ + Instruction::F32Const(f32::NAN), + Instruction::I64TruncSatF32S, + ]) + .unwrap(), + 0 + ); + assert_eq!( + run_i64(&[ + Instruction::F32Const(f32::NAN), + Instruction::I64TruncSatF32U, + ]) + .unwrap(), + 0 + ); + // +inf saturates to i64::MAX / u64::MAX (== -1 as i64). + assert_eq!( + run_i64(&[ + Instruction::F32Const(f32::INFINITY), + Instruction::I64TruncSatF32S, + ]) + .unwrap(), + i64::MAX + ); + assert_eq!( + run_i64(&[ + Instruction::F32Const(f32::INFINITY), + Instruction::I64TruncSatF32U, + ]) + .unwrap(), + -1 + ); + // -inf saturates to i64::MIN / 0. + assert_eq!( + run_i64(&[ + Instruction::F32Const(f32::NEG_INFINITY), + Instruction::I64TruncSatF32S, + ]) + .unwrap(), + i64::MIN + ); + assert_eq!( + run_i64(&[ + Instruction::F32Const(f32::NEG_INFINITY), + Instruction::I64TruncSatF32U, + ]) + .unwrap(), + 0 + ); +} + +#[test] +fn i64_trunc_sat_f64_u_clamps_negatives_and_overflow() { + assert_eq!( + run_i64(&[Instruction::F64Const(-1.0), Instruction::I64TruncSatF64U]).unwrap(), + 0 + ); + // 1e30 > 2^64 -> u64::MAX (-1 as i64). + assert_eq!( + run_i64(&[Instruction::F64Const(1.0e30), Instruction::I64TruncSatF64U,]).unwrap(), + -1 + ); +} + // =========================================================================== // Conversions: round-trips and int → float // =========================================================================== diff --git a/crates/wasm/tests/instantiation_tests.rs b/crates/wasm/tests/instantiation_tests.rs index 01922d3..fddfaa1 100644 --- a/crates/wasm/tests/instantiation_tests.rs +++ b/crates/wasm/tests/instantiation_tests.rs @@ -519,3 +519,70 @@ fn own_global_can_initialize_from_imported_const_global() { }; assert_eq!(store.global(copy).get(), WasmValue::I32(77)); } + +// --------------------------------------------------------------------------- +// Real-world: Rust-compiled cdylib end-to-end. +// --------------------------------------------------------------------------- + +const RUST_FIXTURE: &[u8] = include_bytes!("fixtures/wasm_fixture.wasm"); + +/// Loads the same Rust-compiled fixture exercised by the parser and validator +/// tests, instantiates it without imports, and verifies the three exported +/// functions behave per their Rust sources: +/// #[no_mangle] pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b } +/// #[no_mangle] pub extern "C" fn fib(n: i32) -> i64 { /* iterative */ } +/// #[no_mangle] pub extern "C" fn select_test(cond: i32, a: i32, b: i32) -> i32 +/// { if cond != 0 { a } else { b } } +#[test] +fn rust_compiled_module_instantiates_and_runs() { + let module = parse(RUST_FIXTURE).expect("parses"); + + let mut store = Store::new(); + let instance = instantiate(&mut store, &module, &ImportObject::new()).expect("instantiates"); + + let ExternVal::Func(add_addr) = instance.export("add").unwrap() else { + panic!("add should export a function"); + }; + let ExternVal::Func(fib_addr) = instance.export("fib").unwrap() else { + panic!("fib should export a function"); + }; + let ExternVal::Func(select_addr) = instance.export("select_test").unwrap() else { + panic!("select_test should export a function"); + }; + assert!(matches!( + instance.export("memory"), + Some(ExternVal::Memory(_)) + )); + + let mut engine = we_wasm::runtime::Engine::new(&mut store); + + let out = engine + .invoke(add_addr, vec![WasmValue::I32(40), WasmValue::I32(2)]) + .unwrap(); + assert_eq!(out, vec![WasmValue::I32(42)]); + + // Fibonacci: f(0) = 0, f(1) = 1, f(10) = 55, f(20) = 6765. + let out = engine.invoke(fib_addr, vec![WasmValue::I32(0)]).unwrap(); + assert_eq!(out, vec![WasmValue::I64(0)]); + let out = engine.invoke(fib_addr, vec![WasmValue::I32(1)]).unwrap(); + assert_eq!(out, vec![WasmValue::I64(1)]); + let out = engine.invoke(fib_addr, vec![WasmValue::I32(10)]).unwrap(); + assert_eq!(out, vec![WasmValue::I64(55)]); + let out = engine.invoke(fib_addr, vec![WasmValue::I32(20)]).unwrap(); + assert_eq!(out, vec![WasmValue::I64(6765)]); + + let out = engine + .invoke( + select_addr, + vec![WasmValue::I32(1), WasmValue::I32(11), WasmValue::I32(22)], + ) + .unwrap(); + assert_eq!(out, vec![WasmValue::I32(11)]); + let out = engine + .invoke( + select_addr, + vec![WasmValue::I32(0), WasmValue::I32(11), WasmValue::I32(22)], + ) + .unwrap(); + assert_eq!(out, vec![WasmValue::I32(22)]); +} diff --git a/crates/wasm/tests/validator_tests.rs b/crates/wasm/tests/validator_tests.rs index 4557b5e..7baa271 100644 --- a/crates/wasm/tests/validator_tests.rs +++ b/crates/wasm/tests/validator_tests.rs @@ -668,6 +668,89 @@ fn data_drop_with_valid_index_accepted() { parse_validate(&bytes).unwrap(); } +#[test] +fn memory_init_invalid_data_index_rejected() { + // memory 1; data count 1; memory.init 5 0 references nonexistent data 5. + let type_payload = vec![0x01, 0x60, 0x00, 0x00]; + let func_payload = vec![0x01, 0x00]; + let memory_payload = vec![0x01, 0x00, 0x01]; + let data_count_payload = vec![0x01]; + let data_payload = vec![0x01, 0x01, 0x00]; + // 0xfc 0x08 : memory.init 5 0 + let code_payload = vec![ + 0x01, 0x0c, // count, body size (12 bytes) + 0x00, // locals + 0x41, 0x00, // i32.const 0 (dest) + 0x41, 0x00, // i32.const 0 (src) + 0x41, 0x00, // i32.const 0 (len) + 0xfc, 0x08, 0x05, 0x00, // memory.init 5, mem 0 + 0x0b, + ]; + let bytes = module_with(&[ + section(1, &type_payload), + section(3, &func_payload), + section(5, &memory_payload), + section(12, &data_count_payload), + section(10, &code_payload), + section(11, &data_payload), + ]); + let err = parse_validate(&bytes).unwrap_err(); + assert!(matches!(err, ValidationErrorKind::InvalidDataIndex(5))); +} + +#[test] +fn elem_drop_invalid_index_rejected() { + // funcref table size 1, no element segments, elem.drop 0. + let type_payload = vec![0x01, 0x60, 0x00, 0x00]; + let func_payload = vec![0x01, 0x00]; + // table: 1 entry, funcref, no max, min 1 + let table_payload = vec![0x01, 0x70, 0x00, 0x01]; + let code_payload = vec![ + 0x01, 0x05, // count, body size + 0x00, // + 0xfc, 0x0d, 0x00, // elem.drop 0 + 0x0b, + ]; + let bytes = module_with(&[ + section(1, &type_payload), + section(3, &func_payload), + section(4, &table_payload), + section(10, &code_payload), + ]); + let err = parse_validate(&bytes).unwrap_err(); + assert!(matches!(err, ValidationErrorKind::InvalidElementIndex(0))); +} + +#[test] +fn table_copy_element_type_mismatch_rejected() { + // Two tables: t0 funcref (min 1), t1 externref (min 1). table.copy 0 1. + let type_payload = vec![0x01, 0x60, 0x00, 0x00]; + let func_payload = vec![0x01, 0x00]; + // 2 tables: funcref (0x70), externref (0x6f) + let table_payload = vec![ + 0x02, // + 0x70, 0x00, 0x01, // + 0x6f, 0x00, 0x01, // + ]; + let code_payload = vec![ + 0x01, 0x0c, // count, body size + 0x00, // + 0x41, 0x00, // i32.const 0 (dst) + 0x41, 0x00, // i32.const 0 (src) + 0x41, 0x00, // i32.const 0 (len) + 0xfc, 0x0e, 0x00, 0x01, // table.copy dst=0 src=1 + 0x0b, + ]; + let bytes = module_with(&[ + section(1, &type_payload), + section(3, &func_payload), + section(4, &table_payload), + section(10, &code_payload), + ]); + let err = parse_validate(&bytes).unwrap_err(); + assert!(matches!(err, ValidationErrorKind::ElementTableTypeMismatch)); +} + // --------------------------------------------------------------------------- // Real-world Rust-compiled fixture. // ---------------------------------------------------------------------------