@@ -284,3 +284,190 @@ TEST_F(RelaxedSimdTest, dot_s_i16_overflow_pin_sibling_op)
284284 wasm_runtime_deinstantiate (inst);
285285 wasm_runtime_unload (module );
286286}
287+
288+ /*
289+ * Spec-allowed-set test for `i16x8.relaxed_q15mulr_s` at the
290+ * INT16_MIN * INT16_MIN overflow boundary.
291+ *
292+ * (module
293+ * (func (export "q15mulr_int16_min_squared") (result i64)
294+ * v128.const i16x8 -32768 0 0 0 0 0 0 0
295+ * v128.const i16x8 -32768 0 0 0 0 0 0 0
296+ * i16x8.relaxed_q15mulr_s
297+ * i64x2.extract_lane 0))
298+ *
299+ * Q15 multiply-with-rounding: lane = sat_s((a*b + 0x4000) >> 15).
300+ * For a = b = INT16_MIN:
301+ * a*b = (-32768)*(-32768) = 0x40000000
302+ * + 0x4000 = 0x40004000
303+ * >> 15 = 0x8000 = 32768 (overflows i16)
304+ * sat_s = 32767 = 0x7fff (saturate, IEEE/x86 PMULHRSW)
305+ * wrap = (int16)32768 = 0x8000 (truncate, spec-allowed)
306+ *
307+ * The spec's relaxed clause permits either lowering, so the lane-0
308+ * value must be 0x7fff OR 0x8000. Lanes 1..7 are 0 (deterministic).
309+ * Encoded as the low i64 (i64x2.extract_lane 0) the spec-allowed
310+ * set is { 0x0000_0000_0000_7fff, 0x0000_0000_0000_8000 }.
311+ *
312+ * WAMR's hand-rolled lowering picks saturate (0x7fff); this test
313+ * pins the choice via membership rather than exact equality, so a
314+ * future switch to wrap (spec-allowed) does not break the test.
315+ */
316+ static const uint8_t Q15MULR_OVERFLOW_WASM[] = {
317+ 0x00 , 0x61 , 0x73 , 0x6d , 0x01 , 0x00 , 0x00 , 0x00 , 0x01 , 0x05 , 0x01 ,
318+ 0x60 , 0x00 , 0x01 , 0x7e , 0x03 , 0x02 , 0x01 , 0x00 , 0x07 , 0x1d , 0x01 ,
319+ 0x19 , 0x71 , 0x31 , 0x35 , 0x6d , 0x75 , 0x6c , 0x72 , 0x5f , 0x69 , 0x6e ,
320+ 0x74 , 0x31 , 0x36 , 0x5f , 0x6d , 0x69 , 0x6e , 0x5f , 0x73 , 0x71 , 0x75 ,
321+ 0x61 , 0x72 , 0x65 , 0x64 , 0x00 , 0x00 , 0x0a , 0x2e , 0x01 , 0x2c , 0x00 ,
322+ 0xfd , 0x0c , 0x00 , 0x80 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
323+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0xfd , 0x0c , 0x00 , 0x80 ,
324+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
325+ 0x00 , 0x00 , 0x00 , 0xfd , 0x91 , 0x02 , 0xfd , 0x1d , 0x00 , 0x0b
326+ };
327+
328+ TEST_F (RelaxedSimdTest, q15mulr_int16_min_squared_either_sat_or_wrap)
329+ {
330+ char err[128 ] = { 0 };
331+ uint8_t buf[sizeof (Q15MULR_OVERFLOW_WASM)];
332+ memcpy (buf, Q15MULR_OVERFLOW_WASM, sizeof (Q15MULR_OVERFLOW_WASM));
333+
334+ wasm_module_t module = wasm_runtime_load (buf, (uint32_t )sizeof (buf), err,
335+ (uint32_t )sizeof (err));
336+ ASSERT_NE (module , nullptr ) << " load failed: " << err;
337+
338+ wasm_module_inst_t inst = wasm_runtime_instantiate (
339+ module , 32768u , 32768u , err, (uint32_t )sizeof (err));
340+ ASSERT_NE (inst, nullptr ) << " instantiate failed: " << err;
341+
342+ wasm_function_inst_t func =
343+ wasm_runtime_lookup_function (inst, " q15mulr_int16_min_squared" );
344+ ASSERT_NE (func, nullptr ) << " export `q15mulr_int16_min_squared` not found" ;
345+
346+ wasm_exec_env_t env = wasm_runtime_create_exec_env (inst, 32768u );
347+ ASSERT_NE (env, nullptr );
348+
349+ uint32_t argv[2 ] = { 0 , 0 };
350+ bool ok = wasm_runtime_call_wasm (env, func, 0 , argv);
351+ EXPECT_TRUE (ok) << " call_wasm failed: " << wasm_runtime_get_exception (inst);
352+
353+ /* Lanes 1..3 must be 0 (deterministic). Encoded in argv: lanes
354+ * 1..3 occupy bits 16..63 of the 64-bit packed result.
355+ * argv[0] (low i32) = (lane1 << 16) | lane0
356+ * argv[1] (high i32) = (lane3 << 16) | lane2 */
357+ EXPECT_EQ (argv[1 ], 0u ) << " lanes 2,3 must be zero" ;
358+ EXPECT_EQ ((argv[0 ] >> 16 ) & 0xffffu , 0u ) << " lane 1 must be zero" ;
359+
360+ /* Lane 0 = low 16 bits of argv[0]: either 0x7fff (sat) or
361+ * 0x8000 (wrap). Both spec-conformant per the relaxed-SIMD
362+ * implementation-defined clause for q15mulr_s. */
363+ uint32_t lane0 = argv[0 ] & 0xffffu ;
364+ EXPECT_TRUE (lane0 == 0x7fffu || lane0 == 0x8000u )
365+ << " lane 0 = 0x" << std::hex << lane0
366+ << " , expected 0x7fff (saturate) or 0x8000 (wrap)" ;
367+
368+ wasm_runtime_destroy_exec_env (env);
369+ wasm_runtime_deinstantiate (inst);
370+ wasm_runtime_unload (module );
371+ }
372+
373+ /*
374+ * Spec-allowed-set test for `f32x4.relaxed_madd` at the
375+ * (Inf * 0 + c) invalid-multiply boundary.
376+ *
377+ * (module
378+ * (func (export "madd_inf_times_zero_lo") (result i64)
379+ * v128.const f32x4 inf inf inf inf
380+ * v128.const f32x4 0 0 0 0
381+ * v128.const f32x4 1.0 2.0 3.0 4.0
382+ * f32x4.relaxed_madd
383+ * i64x2.extract_lane 0)
384+ * (func (export "madd_inf_times_zero_hi") (result i64) ;; lane 1)
385+ *
386+ * IEEE 754 §7.2: Inf × 0 is an invalid operation and produces NaN
387+ * (regardless of the subsequent add of `c`). Both fused-multiply-
388+ * add (`fma(Inf, 0, c)`) and unfused (`Inf * 0 + c`) lowerings of
389+ * relaxed_madd produce a NaN here — so the choice between them
390+ * doesn't affect the *kind* of result, only its specific bit
391+ * pattern. The relaxed-SIMD spec leaves the NaN bit pattern
392+ * implementation-defined, so the test checks the IEEE-754 NaN
393+ * predicate (exponent all-ones, fraction non-zero) per lane
394+ * rather than an exact bit pattern.
395+ *
396+ * This case is the relevant adversarial input for "do we
397+ * propagate NaN through the FMA path correctly when one of the
398+ * inputs is +Inf and another is +0?" — exactly the kind of
399+ * boundary the spec test set doesn't explicitly cover.
400+ */
401+ static const uint8_t MADD_INF_TIMES_ZERO_WASM[] = {
402+ 0x00 , 0x61 , 0x73 , 0x6d , 0x01 , 0x00 , 0x00 , 0x00 , 0x01 , 0x05 , 0x01 , 0x60 ,
403+ 0x00 , 0x01 , 0x7e , 0x03 , 0x03 , 0x02 , 0x00 , 0x00 , 0x07 , 0x33 , 0x02 , 0x16 ,
404+ 0x6d , 0x61 , 0x64 , 0x64 , 0x5f , 0x69 , 0x6e , 0x66 , 0x5f , 0x74 , 0x69 , 0x6d ,
405+ 0x65 , 0x73 , 0x5f , 0x7a , 0x65 , 0x72 , 0x6f , 0x5f , 0x6c , 0x6f , 0x00 , 0x00 ,
406+ 0x16 , 0x6d , 0x61 , 0x64 , 0x64 , 0x5f , 0x69 , 0x6e , 0x66 , 0x5f , 0x74 , 0x69 ,
407+ 0x6d , 0x65 , 0x73 , 0x5f , 0x7a , 0x65 , 0x72 , 0x6f , 0x5f , 0x68 , 0x69 , 0x00 ,
408+ 0x01 , 0x0a , 0x7f , 0x02 , 0x3e , 0x00 , 0xfd , 0x0c , 0x00 , 0x00 , 0x80 , 0x7f ,
409+ 0x00 , 0x00 , 0x80 , 0x7f , 0x00 , 0x00 , 0x80 , 0x7f , 0x00 , 0x00 , 0x80 , 0x7f ,
410+ 0xfd , 0x0c , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
411+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0xfd , 0x0c , 0x00 , 0x00 , 0x80 , 0x3f ,
412+ 0x00 , 0x00 , 0x00 , 0x40 , 0x00 , 0x00 , 0x40 , 0x40 , 0x00 , 0x00 , 0x80 , 0x40 ,
413+ 0xfd , 0x85 , 0x02 , 0xfd , 0x1d , 0x00 , 0x0b , 0x3e , 0x00 , 0xfd , 0x0c , 0x00 ,
414+ 0x00 , 0x80 , 0x7f , 0x00 , 0x00 , 0x80 , 0x7f , 0x00 , 0x00 , 0x80 , 0x7f , 0x00 ,
415+ 0x00 , 0x80 , 0x7f , 0xfd , 0x0c , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 ,
416+ 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0x00 , 0xfd , 0x0c , 0x00 ,
417+ 0x00 , 0x80 , 0x3f , 0x00 , 0x00 , 0x00 , 0x40 , 0x00 , 0x00 , 0x40 , 0x40 , 0x00 ,
418+ 0x00 , 0x80 , 0x40 , 0xfd , 0x85 , 0x02 , 0xfd , 0x1d , 0x01 , 0x0b
419+ };
420+
421+ /* Helper: true iff the f32 bit pattern is any NaN
422+ * (exponent = 0xff, fraction != 0). */
423+ static bool
424+ f32_bits_are_nan (uint32_t bits)
425+ {
426+ uint32_t exp = (bits >> 23 ) & 0xff ;
427+ uint32_t frac = bits & 0x7fffff ;
428+ return exp == 0xff && frac != 0u ;
429+ }
430+
431+ TEST_F (RelaxedSimdTest, madd_inf_times_zero_propagates_nan)
432+ {
433+ char err[128 ] = { 0 };
434+ uint8_t buf[sizeof (MADD_INF_TIMES_ZERO_WASM)];
435+ memcpy (buf, MADD_INF_TIMES_ZERO_WASM, sizeof (MADD_INF_TIMES_ZERO_WASM));
436+
437+ wasm_module_t module = wasm_runtime_load (buf, (uint32_t )sizeof (buf), err,
438+ (uint32_t )sizeof (err));
439+ ASSERT_NE (module , nullptr ) << " load failed: " << err;
440+
441+ wasm_module_inst_t inst = wasm_runtime_instantiate (
442+ module , 32768u , 32768u , err, (uint32_t )sizeof (err));
443+ ASSERT_NE (inst, nullptr ) << " instantiate failed: " << err;
444+
445+ wasm_exec_env_t env = wasm_runtime_create_exec_env (inst, 32768u );
446+ ASSERT_NE (env, nullptr );
447+
448+ /* Call the lo half (lanes 0,1) then the hi half (lanes 2,3);
449+ * each call returns one i64 packing two f32 lanes:
450+ * argv[0] = lane2k bits, argv[1] = lane2k+1 bits */
451+ for (uint32_t half = 0 ; half < 2 ; half++) {
452+ const char *name =
453+ half == 0 ? " madd_inf_times_zero_lo" : " madd_inf_times_zero_hi" ;
454+ wasm_function_inst_t func = wasm_runtime_lookup_function (inst, name);
455+ ASSERT_NE (func, nullptr ) << " export `" << name << " ` not found" ;
456+
457+ uint32_t argv[2 ] = { 0 , 0 };
458+ bool ok = wasm_runtime_call_wasm (env, func, 0 , argv);
459+ EXPECT_TRUE (ok) << " call_wasm `" << name
460+ << " ` failed: " << wasm_runtime_get_exception (inst);
461+
462+ EXPECT_TRUE (f32_bits_are_nan (argv[0 ]))
463+ << name << " lane " << (2 * half) << " not NaN: bits = 0x"
464+ << std::hex << argv[0 ];
465+ EXPECT_TRUE (f32_bits_are_nan (argv[1 ]))
466+ << name << " lane " << (2 * half + 1 ) << " not NaN: bits = 0x"
467+ << std::hex << argv[1 ];
468+ }
469+
470+ wasm_runtime_destroy_exec_env (env);
471+ wasm_runtime_deinstantiate (inst);
472+ wasm_runtime_unload (module );
473+ }
0 commit comments