From 91ab9f9c6bff8a67df6fd7a1b3f2614005740e0f Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Mon, 26 Oct 2026 18:55:26 -0700 Subject: [PATCH 1/2] [StubUnsupportedJSOps] Stub out trunc_sat ops `wasm2js` and `I64ToI32Lowering` lower 32-bit and 64-bit `trunc_sat` instructions using non-saturating conversions (`~~x`, `~~x >>> 0`, and `lowerTruncFloatToInt`) that wrap rather than saturate on out-of-range inputs. Unlike trapping `trunc` instructions (which trap on out-of-range inputs in the interpreter and are not constant-folded by `Precompute`), `trunc_sat` instructions do not trap in WebAssembly and are folded to saturated constants when optimizing, causing differential mismatches in `Wasm2JS` fuzzing. Stub out all `trunc_sat` unary operations in `StubUnsupportedJSOpsPass::visitUnary`. --- src/passes/RemoveNonJSOps.cpp | 14 ++- test/lit/passes/stub-unsupported-js.wast | 127 +++++++++++++++++++++++ 2 files changed, 139 insertions(+), 2 deletions(-) create mode 100644 test/lit/passes/stub-unsupported-js.wast diff --git a/src/passes/RemoveNonJSOps.cpp b/src/passes/RemoveNonJSOps.cpp index 290eec7c8e5..92c4e3ff4b4 100644 --- a/src/passes/RemoveNonJSOps.cpp +++ b/src/passes/RemoveNonJSOps.cpp @@ -357,8 +357,18 @@ struct StubUnsupportedJSOpsPass void visitUnary(Unary* curr) { switch (curr->op) { case ConvertUInt64ToFloat32: - // See detailed comment in lowerConvertIntToFloat in - // I64ToI32Lowering.cpp. + case TruncSatSFloat32ToInt32: + case TruncSatUFloat32ToInt32: + case TruncSatSFloat64ToInt32: + case TruncSatUFloat64ToInt32: + case TruncSatSFloat32ToInt64: + case TruncSatUFloat32ToInt64: + case TruncSatSFloat64ToInt64: + case TruncSatUFloat64ToInt64: + // f32.convert_i64_u does not have perfect rounding in all cases (see + // lowerConvertIntToFloat in I64ToI32Lowering.cpp), and trunc_sat + // operations are lowered using non-saturating conversions in wasm2js + // and I64ToI32Lowering. stubOut(curr->value, curr->type); break; default: { diff --git a/test/lit/passes/stub-unsupported-js.wast b/test/lit/passes/stub-unsupported-js.wast new file mode 100644 index 00000000000..55aea019f84 --- /dev/null +++ b/test/lit/passes/stub-unsupported-js.wast @@ -0,0 +1,127 @@ +;; NOTE: Assertions have been generated by update_lit_checks.py and should not be edited. + +;; RUN: wasm-opt %s -all --stub-unsupported-js -S -o - | filecheck %s + +(module + ;; CHECK: (func $convert_u_i64_f32 (type $4) (param $x i64) (result f32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: ) + (func $convert_u_i64_f32 (param $x i64) (result f32) + ;; Unsigned i64 to f32 conversions are stubbed out. + (f32.convert_i64_u + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_s_f32_i32 (type $0) (param $x f32) (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_s_f32_i32 (param $x f32) (result i32) + ;; Nontrapping float-to-int conversions are stubbed out. + (i32.trunc_sat_f32_s + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_u_f32_i32 (type $0) (param $x f32) (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_u_f32_i32 (param $x f32) (result i32) + (i32.trunc_sat_f32_u + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_s_f64_i32 (type $1) (param $x f64) (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_s_f64_i32 (param $x f64) (result i32) + (i32.trunc_sat_f64_s + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_u_f64_i32 (type $1) (param $x f64) (result i32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i32.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_u_f64_i32 (param $x f64) (result i32) + (i32.trunc_sat_f64_u + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_s_f32_i64 (type $2) (param $x f32) (result i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_s_f32_i64 (param $x f32) (result i64) + (i64.trunc_sat_f32_s + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_u_f32_i64 (type $2) (param $x f32) (result i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_u_f32_i64 (param $x f32) (result i64) + (i64.trunc_sat_f32_u + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_s_f64_i64 (type $3) (param $x f64) (result i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_s_f64_i64 (param $x f64) (result i64) + (i64.trunc_sat_f64_s + (local.get $x) + ) + ) + + ;; CHECK: (func $trunc_sat_u_f64_i64 (type $3) (param $x f64) (result i64) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (i64.const 0) + ;; CHECK-NEXT: ) + (func $trunc_sat_u_f64_i64 (param $x f64) (result i64) + (i64.trunc_sat_f64_u + (local.get $x) + ) + ) + + ;; CHECK: (func $supported_trunc (type $0) (param $x f32) (result i32) + ;; CHECK-NEXT: (i32.trunc_f32_s + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: ) + (func $supported_trunc (param $x f32) (result i32) + ;; Trapping float-to-int conversions are not stubbed out. + (i32.trunc_f32_s + (local.get $x) + ) + ) +) From efdad5cacb48eacb25bced3081e5d32f197c6379 Mon Sep 17 00:00:00 2001 From: Thomas Lively Date: Tue, 27 Oct 2026 10:39:28 -0700 Subject: [PATCH 2/2] [StubUnsupportedJSOps] Also stub out f32.convert_i64_s `f32.convert_i64_s` is lowered through an intermediate `f64` in `I64ToI32Lowering::lowerConvertIntToFloat` just like `f32.convert_i64_u`, so it suffers from the same double-rounding precision issue. --- src/passes/RemoveNonJSOps.cpp | 9 ++++---- test/lit/passes/stub-unsupported-js.wast | 28 +++++++++++++++++------- 2 files changed, 25 insertions(+), 12 deletions(-) diff --git a/src/passes/RemoveNonJSOps.cpp b/src/passes/RemoveNonJSOps.cpp index 92c4e3ff4b4..7c7d9d41982 100644 --- a/src/passes/RemoveNonJSOps.cpp +++ b/src/passes/RemoveNonJSOps.cpp @@ -356,6 +356,7 @@ struct StubUnsupportedJSOpsPass void visitUnary(Unary* curr) { switch (curr->op) { + case ConvertSInt64ToFloat32: case ConvertUInt64ToFloat32: case TruncSatSFloat32ToInt32: case TruncSatUFloat32ToInt32: @@ -365,10 +366,10 @@ struct StubUnsupportedJSOpsPass case TruncSatUFloat32ToInt64: case TruncSatSFloat64ToInt64: case TruncSatUFloat64ToInt64: - // f32.convert_i64_u does not have perfect rounding in all cases (see - // lowerConvertIntToFloat in I64ToI32Lowering.cpp), and trunc_sat - // operations are lowered using non-saturating conversions in wasm2js - // and I64ToI32Lowering. + // f32.convert_i64_s and f32.convert_i64_u do not have perfect rounding + // in all cases (see lowerConvertIntToFloat in I64ToI32Lowering.cpp), + // and trunc_sat operations are lowered using non-saturating conversions + // in wasm2js and I64ToI32Lowering. stubOut(curr->value, curr->type); break; default: { diff --git a/test/lit/passes/stub-unsupported-js.wast b/test/lit/passes/stub-unsupported-js.wast index 55aea019f84..71376900bfe 100644 --- a/test/lit/passes/stub-unsupported-js.wast +++ b/test/lit/passes/stub-unsupported-js.wast @@ -3,14 +3,26 @@ ;; RUN: wasm-opt %s -all --stub-unsupported-js -S -o - | filecheck %s (module - ;; CHECK: (func $convert_u_i64_f32 (type $4) (param $x i64) (result f32) + ;; CHECK: (func $convert_s_i64_f32 (type $1) (param $x i64) (result f32) + ;; CHECK-NEXT: (drop + ;; CHECK-NEXT: (local.get $x) + ;; CHECK-NEXT: ) + ;; CHECK-NEXT: (f32.const 0) + ;; CHECK-NEXT: ) + (func $convert_s_i64_f32 (param $x i64) (result f32) + ;; Signed and unsigned i64 to f32 conversions are stubbed out. + (f32.convert_i64_s + (local.get $x) + ) + ) + + ;; CHECK: (func $convert_u_i64_f32 (type $1) (param $x i64) (result f32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) ;; CHECK-NEXT: (f32.const 0) ;; CHECK-NEXT: ) (func $convert_u_i64_f32 (param $x i64) (result f32) - ;; Unsigned i64 to f32 conversions are stubbed out. (f32.convert_i64_u (local.get $x) ) @@ -41,7 +53,7 @@ ) ) - ;; CHECK: (func $trunc_sat_s_f64_i32 (type $1) (param $x f64) (result i32) + ;; CHECK: (func $trunc_sat_s_f64_i32 (type $2) (param $x f64) (result i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) @@ -53,7 +65,7 @@ ) ) - ;; CHECK: (func $trunc_sat_u_f64_i32 (type $1) (param $x f64) (result i32) + ;; CHECK: (func $trunc_sat_u_f64_i32 (type $2) (param $x f64) (result i32) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) @@ -65,7 +77,7 @@ ) ) - ;; CHECK: (func $trunc_sat_s_f32_i64 (type $2) (param $x f32) (result i64) + ;; CHECK: (func $trunc_sat_s_f32_i64 (type $3) (param $x f32) (result i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) @@ -77,7 +89,7 @@ ) ) - ;; CHECK: (func $trunc_sat_u_f32_i64 (type $2) (param $x f32) (result i64) + ;; CHECK: (func $trunc_sat_u_f32_i64 (type $3) (param $x f32) (result i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) @@ -89,7 +101,7 @@ ) ) - ;; CHECK: (func $trunc_sat_s_f64_i64 (type $3) (param $x f64) (result i64) + ;; CHECK: (func $trunc_sat_s_f64_i64 (type $4) (param $x f64) (result i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: ) @@ -101,7 +113,7 @@ ) ) - ;; CHECK: (func $trunc_sat_u_f64_i64 (type $3) (param $x f64) (result i64) + ;; CHECK: (func $trunc_sat_u_f64_i64 (type $4) (param $x f64) (result i64) ;; CHECK-NEXT: (drop ;; CHECK-NEXT: (local.get $x) ;; CHECK-NEXT: )