Bug 1435369: Implement non-trapping float-to-int conversions for WebAssembly r=luke
authorDan Gohman <sunfish@mozilla.com>
Thu, 15 Feb 2018 09:56:00 +0200
changeset 404095 25900f3b9936a16486a36473517e14a6e8a1f4b9
parent 404094 e480e3b7819d5b7494d612839c442985e6d66a88
child 404096 d6316698d32875eed243c3960234b01ad80e1c17
push id33451
push usernbeleuzu@mozilla.com
push dateFri, 16 Feb 2018 09:51:13 +0000
treeherdermozilla-central@9eaebbcc33fd [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersluke
bugs1435369
milestone60.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1435369: Implement non-trapping float-to-int conversions for WebAssembly r=luke
js/src/builtin/TestingFunctions.cpp
js/src/jit-test/lib/wasm-binary.js
js/src/jit-test/tests/wasm/binary.js
js/src/jit-test/tests/wasm/conversion.js
js/src/jit/IonTypes.h
js/src/jit/MIR.cpp
js/src/jit/MIR.h
js/src/jit/MacroAssembler.h
js/src/jit/arm/CodeGenerator-arm.cpp
js/src/jit/arm/MacroAssembler-arm.cpp
js/src/jit/arm/MacroAssembler-arm.h
js/src/jit/arm64/MacroAssembler-arm64.cpp
js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp
js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp
js/src/jit/mips32/MacroAssembler-mips32.cpp
js/src/jit/mips64/MacroAssembler-mips64.cpp
js/src/jit/shared/CodeGenerator-shared.h
js/src/jit/x64/CodeGenerator-x64.cpp
js/src/jit/x64/MacroAssembler-x64.cpp
js/src/jit/x86-shared/CodeGenerator-x86-shared.cpp
js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp
js/src/jit/x86/CodeGenerator-x86.cpp
js/src/jit/x86/MacroAssembler-x86.cpp
js/src/moz.build
js/src/wasm/WasmAST.h
js/src/wasm/WasmBaselineCompile.cpp
js/src/wasm/WasmBinaryConstants.h
js/src/wasm/WasmBinaryIterator.cpp
js/src/wasm/WasmBinaryToAST.cpp
js/src/wasm/WasmBinaryToText.cpp
js/src/wasm/WasmBuiltins.cpp
js/src/wasm/WasmFrameIter.cpp
js/src/wasm/WasmIonCompile.cpp
js/src/wasm/WasmTextToBinary.cpp
js/src/wasm/WasmTypes.h
js/src/wasm/WasmValidate.cpp
js/src/wasm/WasmValidate.h
--- a/js/src/builtin/TestingFunctions.cpp
+++ b/js/src/builtin/TestingFunctions.cpp
@@ -561,16 +561,29 @@ WasmSignExtensionSupported(JSContext* cx
 #else
     bool isSupported = false;
 #endif
     args.rval().setBoolean(isSupported);
     return true;
 }
 
 static bool
+WasmSaturatingTruncationSupported(JSContext* cx, unsigned argc, Value* vp)
+{
+    CallArgs args = CallArgsFromVp(argc, vp);
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+    bool isSupported = true;
+#else
+    bool isSupported = false;
+#endif
+    args.rval().setBoolean(isSupported);
+    return true;
+}
+
+static bool
 WasmCompileMode(JSContext* cx, unsigned argc, Value* vp)
 {
     CallArgs args = CallArgsFromVp(argc, vp);
 
     // We default to ion if nothing is enabled, as does the Wasm compiler.
     JSString* result;
     if (!wasm::HasSupport(cx))
         result = JS_NewStringCopyZ(cx, "disabled");
@@ -5294,16 +5307,21 @@ gc::ZealModeHelpText),
 "  Returns a boolean indicating whether the WebAssembly threads proposal is\n"
 "  supported on the current device."),
 
     JS_FN_HELP("wasmSignExtensionSupported", WasmSignExtensionSupported, 0, 0,
 "wasmSignExtensionSupported()",
 "  Returns a boolean indicating whether the WebAssembly sign extension opcodes are\n"
 "  supported on the current device."),
 
+    JS_FN_HELP("wasmSaturatingTruncationSupported", WasmSaturatingTruncationSupported, 0, 0,
+"wasmSaturatingTruncationSupported()",
+"  Returns a boolean indicating whether the WebAssembly saturating truncates opcodes are\n"
+"  supported on the current device."),
+
     JS_FN_HELP("wasmCompileMode", WasmCompileMode, 0, 0,
 "wasmCompileMode()",
 "  Returns a string indicating the available compile policy: 'baseline', 'ion',\n"
 "  'baseline-or-ion', or 'disabled' (if wasm is not available at all)."),
 
     JS_FN_HELP("wasmTextToBinary", WasmTextToBinary, 1, 0,
 "wasmTextToBinary(str)",
 "  Translates the given text wasm module into its binary encoding."),
--- a/js/src/jit-test/lib/wasm-binary.js
+++ b/js/src/jit-test/lib/wasm-binary.js
@@ -90,17 +90,19 @@ const I64TruncUF32Code = 0xaf;
 const I64TruncSF64Code = 0xb0;
 const I64TruncUF64Code = 0xb1;
 const I64DivSCode      = 0x7f;
 const I64DivUCode      = 0x80;
 const I64RemSCode      = 0x81;
 const I64RemUCode      = 0x82;
 
 const FirstInvalidOpcode = wasmThreadsSupported() ? 0xc5 : 0xc0;
-const LastInvalidOpcode = 0xfd;
+const LastInvalidOpcode = 0xfb;
+const NumericPrefix = 0xfc;
+const SimdPrefix = 0xfd;
 const AtomicPrefix = 0xfe;
 const MozPrefix = 0xff;
 
 // DefinitionKind
 const FunctionCode     = 0x00;
 const TableCode        = 0x01;
 const MemoryCode       = 0x02;
 const GlobalCode       = 0x03;
--- a/js/src/jit-test/tests/wasm/binary.js
+++ b/js/src/jit-test/tests/wasm/binary.js
@@ -459,21 +459,34 @@ function checkIllegalPrefixed(prefix, op
 //  0x10 .. 0x4f are primitive atomic ops
 
 for (let i = 3; i < 0x10; i++)
     checkIllegalPrefixed(AtomicPrefix, i);
 
 for (let i = 0x4f; i < 0x100; i++)
     checkIllegalPrefixed(AtomicPrefix, i);
 
+// Illegal Numeric opcodes
+//
+// Feb 2018 numeric draft:
+//
+//  0x00 .. 0x07 are saturating truncation ops
+
+for (let i = 0x08; i < 256; i++)
+    checkIllegalPrefixed(NumericPrefix, i);
+
+// Illegal SIMD opcodes (all of them, for now)
+for (let i = 0; i < 256; i++)
+    checkIllegalPrefixed(SimdPrefix, i);
+
 // Illegal MozPrefix opcodes (all of them)
 for (let i = 0; i < 256; i++)
     checkIllegalPrefixed(MozPrefix, i);
 
-for (let prefix of [AtomicPrefix, MozPrefix]) {
+for (let prefix of [AtomicPrefix, NumericPrefix, SimdPrefix, MozPrefix]) {
     // Prefix without a subsequent opcode
     let binary = moduleWithSections([v2vSigSection, declSection([0]), bodySection([funcBody({locals:[], body:[prefix]})])]);
     assertErrorMessage(() => wasmEval(binary), CompileError, /unrecognized opcode/);
     assertEq(WebAssembly.validate(binary), false);
 }
 
 // Checking stack trace.
 function runStackTraceTest(moduleName, funcNames, expectedName) {
--- a/js/src/jit-test/tests/wasm/conversion.js
+++ b/js/src/jit-test/tests/wasm/conversion.js
@@ -50,17 +50,17 @@ function testConversion0(resultType, opc
 function testConversion(resultType, opcode, paramType, op, expect) {
   testConversion0(resultType, `${resultType}.${opcode}/${paramType}`, paramType, op, expect);
 }
 
 function testSignExtension(resultType, opcode, paramType, op, expect) {
   testConversion0(resultType, `${resultType}.${opcode}`, paramType, op, expect);
 }
 
-function testTrap(resultType, opcode, paramType, op, expect) {
+function testTrap(resultType, opcode, paramType, op) {
     let func = wasmEvalText(`(module
         (func
             (param ${paramType})
             (result ${resultType})
             (${resultType}.${opcode}/${paramType} (get_local 0))
         )
         (func
             (param ${paramType})
@@ -71,16 +71,18 @@ function testTrap(resultType, opcode, pa
         (export "" 1)
     )`).exports[""];
 
     let expectedError = op === 'nan' ? /invalid conversion to integer/ : /integer overflow/;
 
     assertErrorMessage(() => func(jsify(op)), Error, expectedError);
 }
 
+var p = Math.pow;
+
 testConversion('i32', 'wrap', 'i64', '0x100000028', 40);
 testConversion('i32', 'wrap', 'i64', -10, -10);
 testConversion('i32', 'wrap', 'i64', "0xffffffff7fffffff", 0x7fffffff);
 testConversion('i32', 'wrap', 'i64', "0xffffffff00000000", 0);
 testConversion('i32', 'wrap', 'i64', "0xfffffffeffffffff", -1);
 testConversion('i32', 'wrap', 'i64', "0x1234567801abcdef", 0x01abcdef);
 testConversion('i32', 'wrap', 'i64', "0x8000000000000002", 2);
 
@@ -98,19 +100,19 @@ testConversion('i64', 'extend_u', 'i32',
 testConversion('i64', 'extend_u', 'i32', 0x80000000, "0x0000000080000000");
 
 testConversion('f32', 'convert_s', 'i64', 1, 1.0);
 testConversion('f32', 'convert_s', 'i64', -1, -1.0);
 testConversion('f32', 'convert_s', 'i64', 0, 0.0);
 testConversion('f32', 'convert_s', 'i64', "0x7fffffffffffffff", 9223372036854775807.0);
 testConversion('f32', 'convert_s', 'i64', "0x8000000000000000", -9223372036854775808.0);
 testConversion('f32', 'convert_s', 'i64', "0x11db9e76a2483", 314159275180032.0);
-testConversion('f32', 'convert_s', 'i64', "0x7fffffff", 2147483648.0); // closesth approx.
+testConversion('f32', 'convert_s', 'i64', "0x7fffffff", 2147483648.0); // closest approx.
 testConversion('f32', 'convert_s', 'i64', "0x80000000", 2147483648.0);
-testConversion('f32', 'convert_s', 'i64', "0x80000001", 2147483648.0); // closesth approx.
+testConversion('f32', 'convert_s', 'i64', "0x80000001", 2147483648.0); // closest approx.
 
 // Interesting values at the boundaries.
 testConversion('f32', 'convert_s', 'i64', "0x358a09a000000002", 3857906751034621952);
 testConversion('f32', 'convert_s', 'i64', "0x8000004000000001", -9223371487098961920);
 testConversion('f32', 'convert_s', 'i64', "0xffdfffffdfffffff", -9007200328482816);
 testConversion('f32', 'convert_s', 'i64', "0x0020000020000001", 9007200328482816);
 testConversion('f32', 'convert_s', 'i64', "0x7fffff4000000001", 9223371487098961920);
 
@@ -233,32 +235,87 @@ testTrap('i64', 'trunc_u', 'f32', 184467
 testTrap('i64', 'trunc_u', 'f32', -1);
 testTrap('i64', 'trunc_u', 'f32', "nan");
 testTrap('i64', 'trunc_u', 'f32', "infinity");
 testTrap('i64', 'trunc_u', 'f32', "-infinity");
 
 testConversion('i64', 'reinterpret', 'f64', 40.09999999999968, "0x40440ccccccccca0");
 testConversion('f64', 'reinterpret', 'i64', "0x40440ccccccccca0", 40.09999999999968);
 
+if (wasmSaturatingTruncationSupported()) {
+    var u64max = '0xffffffffffffffff';
+    var s64max = '0x7fffffffffffffff';
+    var s64min = '-0x8000000000000000';
+    var s32max = 2147483647;
+    var s32min = -2147483648;
+
+    testConversion('i32', 'trunc_s:sat', 'f32', NaN, 0);
+    testConversion('i32', 'trunc_s:sat', 'f32', Infinity, s32max);
+    testConversion('i32', 'trunc_s:sat', 'f32', -Infinity, s32min);
+    testConversion('i32', 'trunc_s:sat', 'f32', p(2, 31), s32max);
+    testConversion('i32', 'trunc_s:sat', 'f32', -p(2, 31) - 256, s32min);
+
+    testConversion('i32', 'trunc_s:sat', 'f64', NaN, 0);
+    testConversion('i32', 'trunc_s:sat', 'f64', Infinity, s32max);
+    testConversion('i32', 'trunc_s:sat', 'f64', -Infinity, s32min);
+    testConversion('i32', 'trunc_s:sat', 'f64', p(2, 31), s32max);
+    testConversion('i32', 'trunc_s:sat', 'f64', -p(2, 31) - 1, s32min);
+
+    testConversion('i32', 'trunc_u:sat', 'f32', NaN, 0);
+    testConversion('i32', 'trunc_u:sat', 'f32', Infinity, -1);
+    testConversion('i32', 'trunc_u:sat', 'f32', -Infinity, 0);
+    testConversion('i32', 'trunc_u:sat', 'f32', -1, 0);
+    testConversion('i32', 'trunc_u:sat', 'f32', p(2, 32), -1);
+
+    testConversion('i32', 'trunc_u:sat', 'f64', NaN, 0);
+    testConversion('i32', 'trunc_u:sat', 'f64', Infinity, -1);
+    testConversion('i32', 'trunc_u:sat', 'f64', -Infinity, 0);
+    testConversion('i32', 'trunc_u:sat', 'f64', -1, 0);
+    testConversion('i32', 'trunc_u:sat', 'f64', p(2, 32), -1);
+
+    testConversion('i64', 'trunc_s:sat', 'f64', 9223372036854776000.0, s64max);
+    testConversion('i64', 'trunc_s:sat', 'f64', -9223372036854778000.0, s64min);
+    testConversion('i64', 'trunc_s:sat', 'f64', 'nan', '0');
+    testConversion('i64', 'trunc_s:sat', 'f64', 'infinity', s64max);
+    testConversion('i64', 'trunc_s:sat', 'f64', '-infinity', s64min);
+
+    testConversion('i64', 'trunc_u:sat', 'f64', -1, '0');
+    testConversion('i64', 'trunc_u:sat', 'f64', 18446744073709551616.0, u64max);
+    testConversion('i64', 'trunc_u:sat', 'f64', 'nan', '0');
+    testConversion('i64', 'trunc_u:sat', 'f64', 'infinity', u64max);
+    testConversion('i64', 'trunc_u:sat', 'f64', '-infinity', '0');
+
+    testConversion('i64', 'trunc_s:sat', 'f32', 9223372036854776000.0, s64max);
+    testConversion('i64', 'trunc_s:sat', 'f32', -9223372586610630000.0, s64min);
+    testConversion('i64', 'trunc_s:sat', 'f32', 'nan', '0');
+    testConversion('i64', 'trunc_s:sat', 'f32', 'infinity', s64max);
+    testConversion('i64', 'trunc_s:sat', 'f32', '-infinity', s64min);
+
+    testConversion('i64', 'trunc_u:sat', 'f32', 18446744073709551616.0, u64max);
+    testConversion('i64', 'trunc_u:sat', 'f32', -1, '0');
+    testConversion('i64', 'trunc_u:sat', 'f32', 'nan', '0');
+    testConversion('i64', 'trunc_u:sat', 'f32', 'infinity', u64max);
+    testConversion('i64', 'trunc_u:sat', 'f32', '-infinity', '0');
+}
+
 if (wasmSignExtensionSupported()) {
     testSignExtension('i32', 'extend8_s', 'i32', 0x7F, 0x7F);
     testSignExtension('i32', 'extend8_s', 'i32', 0x80, -0x80);
     testSignExtension('i32', 'extend16_s', 'i32', 0x7FFF, 0x7FFF);
     testSignExtension('i32', 'extend16_s', 'i32', 0x8000, -0x8000);
     testSignExtension('i64', 'extend8_s', 'i64', 0x7F, 0x7F);
     testSignExtension('i64', 'extend8_s', 'i64', 0x80, -0x80);
     testSignExtension('i64', 'extend16_s', 'i64', 0x7FFF, 0x7FFF);
     testSignExtension('i64', 'extend16_s', 'i64', 0x8000, -0x8000);
     testSignExtension('i64', 'extend32_s', 'i64', 0x7FFFFFFF, 0x7FFFFFFF);
     testSignExtension('i64', 'extend32_s', 'i64', "0x80000000", "0xFFFFFFFF80000000");
 }
 
 // i32.trunc_s* : all values in ] -2**31 - 1; 2**31 [ are acceptable.
 // f32:
-var p = Math.pow;
 testConversion('i32', 'trunc_s', 'f32', 40.1, 40);
 testConversion('i32', 'trunc_s', 'f32', p(2, 31) - 128, p(2, 31) - 128); // last f32 value exactly representable < 2**31.
 testConversion('i32', 'trunc_s', 'f32', -p(2, 31), -p(2,31)); // last f32 value exactly representable > -2**31 - 1.
 
 testTrap('i32', 'trunc_s', 'f32', 'nan');
 testTrap('i32', 'trunc_s', 'f32', 'infinity');
 testTrap('i32', 'trunc_s', 'f32', '-infinity');
 testTrap('i32', 'trunc_s', 'f32', p(2, 31));
@@ -288,21 +345,21 @@ testTrap('i32', 'trunc_u', 'f32', '-infi
 testTrap('i32', 'trunc_u', 'f32', -1);
 testTrap('i32', 'trunc_u', 'f32', p(2,32));
 
 // f64:
 testConversion('i32', 'trunc_u', 'f64', 40.1, 40);
 testConversion('i32', 'trunc_u', 'f64', p(2,32) - 0.001, (p(2,32) - 1)|0); // example value near the top.
 testConversion('i32', 'trunc_u', 'f64', -0.99999, 0); // example value near the bottom.
 
-testTrap('i32', 'trunc_u', 'f32', 'nan');
-testTrap('i32', 'trunc_u', 'f32', 'infinity');
-testTrap('i32', 'trunc_u', 'f32', '-infinity');
-testTrap('i32', 'trunc_u', 'f32', -1);
-testTrap('i32', 'trunc_u', 'f32', p(2,32));
+testTrap('i32', 'trunc_u', 'f64', 'nan');
+testTrap('i32', 'trunc_u', 'f64', 'infinity');
+testTrap('i32', 'trunc_u', 'f64', '-infinity');
+testTrap('i32', 'trunc_u', 'f64', -1);
+testTrap('i32', 'trunc_u', 'f64', p(2,32));
 
 // Other opcodes.
 testConversion('i32', 'reinterpret', 'f32', 40.1, 1109419622);
 testConversion('f32', 'reinterpret', 'i32', 40, 5.605193857299268e-44);
 
 testConversion('f32', 'convert_s', 'i32', 40, 40);
 testConversion('f32', 'convert_u', 'i32', 40, 40);
 
--- a/js/src/jit/IonTypes.h
+++ b/js/src/jit/IonTypes.h
@@ -922,12 +922,17 @@ enum class RoundingMode {
     TowardsZero
 };
 
 // If a function contains no calls, we can assume the caller has checked the
 // stack limit up to this maximum frame size. This works because the jit stack
 // limit has a generous buffer before the real end of the native stack.
 static const uint32_t MAX_UNCHECKED_LEAF_FRAME_SIZE = 64;
 
+// Truncating conversion modifiers.
+typedef uint32_t TruncFlags;
+static const TruncFlags TRUNC_UNSIGNED   = TruncFlags(1) << 0;
+static const TruncFlags TRUNC_SATURATING = TruncFlags(1) << 1;
+
 } // namespace jit
 } // namespace js
 
 #endif /* jit_IonTypes_h */
--- a/js/src/jit/MIR.cpp
+++ b/js/src/jit/MIR.cpp
@@ -4397,32 +4397,32 @@ MWasmTruncateToInt32::foldsTo(TempAlloca
     if (input->type() == MIRType::Int32)
         return input;
 
     if (input->type() == MIRType::Double && input->isConstant()) {
         double d = input->toConstant()->toDouble();
         if (IsNaN(d))
             return this;
 
-        if (!isUnsigned_ && d <= double(INT32_MAX) && d >= double(INT32_MIN))
+        if (!isUnsigned() && d <= double(INT32_MAX) && d >= double(INT32_MIN))
             return MConstant::New(alloc, Int32Value(ToInt32(d)));
 
-        if (isUnsigned_ && d <= double(UINT32_MAX) && d >= 0)
+        if (isUnsigned() && d <= double(UINT32_MAX) && d >= 0)
             return MConstant::New(alloc, Int32Value(ToInt32(d)));
     }
 
     if (input->type() == MIRType::Float32 && input->isConstant()) {
         double f = double(input->toConstant()->toFloat32());
         if (IsNaN(f))
             return this;
 
-        if (!isUnsigned_ && f <= double(INT32_MAX) && f >= double(INT32_MIN))
+        if (!isUnsigned() && f <= double(INT32_MAX) && f >= double(INT32_MIN))
             return MConstant::New(alloc, Int32Value(ToInt32(f)));
 
-        if (isUnsigned_ && f <= double(UINT32_MAX) && f >= 0)
+        if (isUnsigned() && f <= double(UINT32_MAX) && f >= 0)
             return MConstant::New(alloc, Int32Value(ToInt32(f)));
     }
 
     return this;
 }
 
 MDefinition*
 MWrapInt64ToInt32::foldsTo(TempAllocator& alloc)
--- a/js/src/jit/MIR.h
+++ b/js/src/jit/MIR.h
@@ -5590,78 +5590,85 @@ class MExtendInt32ToInt64
         return AliasSet::None();
     }
 };
 
 class MWasmTruncateToInt64
   : public MUnaryInstruction,
     public NoTypePolicy::Data
 {
-    bool isUnsigned_;
+    TruncFlags flags_;
     wasm::BytecodeOffset bytecodeOffset_;
 
-    MWasmTruncateToInt64(MDefinition* def, bool isUnsigned, wasm::BytecodeOffset bytecodeOffset)
+    MWasmTruncateToInt64(MDefinition* def, TruncFlags flags, wasm::BytecodeOffset bytecodeOffset)
       : MUnaryInstruction(classOpcode, def),
-        isUnsigned_(isUnsigned),
+        flags_(flags),
         bytecodeOffset_(bytecodeOffset)
     {
         setResultType(MIRType::Int64);
         setGuard(); // neither removable nor movable because of possible side-effects.
     }
 
   public:
     INSTRUCTION_HEADER(WasmTruncateToInt64)
     TRIVIAL_NEW_WRAPPERS
 
-    bool isUnsigned() const { return isUnsigned_; }
+    bool isUnsigned() const { return flags_ & TRUNC_UNSIGNED; }
+    bool isSaturating() const { return flags_ & TRUNC_SATURATING; }
+    TruncFlags flags() const { return flags_; }
     wasm::BytecodeOffset bytecodeOffset() const { return bytecodeOffset_; }
 
     bool congruentTo(const MDefinition* ins) const override {
         return congruentIfOperandsEqual(ins) &&
-               ins->toWasmTruncateToInt64()->isUnsigned() == isUnsigned_;
+               ins->toWasmTruncateToInt64()->flags() == flags_;
     }
     AliasSet getAliasSet() const override {
         return AliasSet::None();
     }
 };
 
 // Truncate a value to an int32, with wasm semantics: this will trap when the
 // value is out of range.
 class MWasmTruncateToInt32
   : public MUnaryInstruction,
     public NoTypePolicy::Data
 {
-    bool isUnsigned_;
+    TruncFlags flags_;
     wasm::BytecodeOffset bytecodeOffset_;
 
-    explicit MWasmTruncateToInt32(MDefinition* def, bool isUnsigned,
+    explicit MWasmTruncateToInt32(MDefinition* def, TruncFlags flags,
                                   wasm::BytecodeOffset bytecodeOffset)
-      : MUnaryInstruction(classOpcode, def),
-        isUnsigned_(isUnsigned), bytecodeOffset_(bytecodeOffset)
+      : MUnaryInstruction(classOpcode, def), flags_(flags), bytecodeOffset_(bytecodeOffset)
     {
         setResultType(MIRType::Int32);
         setGuard(); // neither removable nor movable because of possible side-effects.
     }
 
   public:
     INSTRUCTION_HEADER(WasmTruncateToInt32)
     TRIVIAL_NEW_WRAPPERS
 
     bool isUnsigned() const {
-        return isUnsigned_;
+        return flags_ & TRUNC_UNSIGNED;
+    }
+    bool isSaturating() const {
+        return flags_ & TRUNC_SATURATING;
+    }
+    TruncFlags flags() const {
+        return flags_;
     }
     wasm::BytecodeOffset bytecodeOffset() const {
         return bytecodeOffset_;
     }
 
     MDefinition* foldsTo(TempAllocator& alloc) override;
 
     bool congruentTo(const MDefinition* ins) const override {
         return congruentIfOperandsEqual(ins) &&
-               ins->toWasmTruncateToInt32()->isUnsigned() == isUnsigned_;
+               ins->toWasmTruncateToInt32()->flags() == flags_;
     }
 
     AliasSet getAliasSet() const override {
         return AliasSet::None();
     }
 };
 
 class MInt64ToFloatingPoint
--- a/js/src/jit/MacroAssembler.h
+++ b/js/src/jit/MacroAssembler.h
@@ -1556,47 +1556,51 @@ class MacroAssembler : public MacroAssem
                                Register memoryBase, Register ptr, Register ptrScratch,
                                Register tmp)
         DEFINED_ON(arm);
 
     // wasm specific methods, used in both the wasm baseline compiler and ion.
 
     // The truncate-to-int32 methods do not bind the rejoin label; clients must
     // do so if oolWasmTruncateCheckF64ToI32() can jump to it.
-    void wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry) PER_ARCH;
-    void wasmTruncateDoubleToInt32(FloatRegister input, Register output, Label* oolEntry) PER_SHARED_ARCH;
-    void oolWasmTruncateCheckF64ToI32(FloatRegister input, bool isUnsigned,
+    void wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                    Label* oolEntry) PER_ARCH;
+    void wasmTruncateDoubleToInt32(FloatRegister input, Register output, bool isSaturating,
+                                   Label* oolEntry) PER_SHARED_ARCH;
+    void oolWasmTruncateCheckF64ToI32(FloatRegister input, Register output, TruncFlags flags,
                                       wasm::BytecodeOffset off, Label* rejoin)
         DEFINED_ON(arm, arm64, x86_shared);
 
-    void wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry) PER_ARCH;
-    void wasmTruncateFloat32ToInt32(FloatRegister input, Register output, Label* oolEntry) PER_SHARED_ARCH;
-    void oolWasmTruncateCheckF32ToI32(FloatRegister input, bool isUnsigned,
+    void wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                     Label* oolEntry) PER_ARCH;
+    void wasmTruncateFloat32ToInt32(FloatRegister input, Register output, bool isSaturating,
+                                    Label* oolEntry) PER_SHARED_ARCH;
+    void oolWasmTruncateCheckF32ToI32(FloatRegister input, Register output, TruncFlags flags,
                                       wasm::BytecodeOffset off, Label* rejoin)
         DEFINED_ON(arm, arm64, x86_shared);
 
     // The truncate-to-int64 methods will always bind the `oolRejoin` label
     // after the last emitted instruction.
-    void wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                   Label* oolRejoin, FloatRegister tempDouble)
+    void wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                   Label* oolEntry, Label* oolRejoin, FloatRegister tempDouble)
         DEFINED_ON(arm64, x86, x64);
-    void wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                    Label* oolRejoin, FloatRegister tempDouble)
+    void wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                    Label* oolEntry, Label* oolRejoin, FloatRegister tempDouble)
         DEFINED_ON(arm64, x86, x64);
-    void oolWasmTruncateCheckF64ToI64(FloatRegister input, bool isUnsigned,
+    void oolWasmTruncateCheckF64ToI64(FloatRegister input, Register64 output, TruncFlags flags,
                                       wasm::BytecodeOffset off, Label* rejoin)
         DEFINED_ON(arm, arm64, x86_shared);
 
-    void wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                    Label* oolRejoin, FloatRegister tempDouble)
+    void wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                    Label* oolEntry, Label* oolRejoin, FloatRegister tempDouble)
         DEFINED_ON(arm64, x86, x64);
-    void wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                     Label* oolRejoin, FloatRegister tempDouble)
+    void wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                     Label* oolEntry, Label* oolRejoin, FloatRegister tempDouble)
         DEFINED_ON(arm64, x86, x64);
-    void oolWasmTruncateCheckF32ToI64(FloatRegister input, bool isUnsigned,
+    void oolWasmTruncateCheckF32ToI64(FloatRegister input, Register64 output, TruncFlags flags,
                                       wasm::BytecodeOffset off, Label* rejoin)
         DEFINED_ON(arm, arm64, x86_shared);
 
     // This function takes care of loading the callee's TLS and pinned regs but
     // it is the caller's responsibility to save/restore TLS or pinned regs.
     void wasmCallImport(const wasm::CallSiteDesc& desc, const wasm::CalleeDesc& callee);
 
     // WasmTableCallIndexReg must contain the index of the indirect call.
--- a/js/src/jit/arm/CodeGenerator-arm.cpp
+++ b/js/src/jit/arm/CodeGenerator-arm.cpp
@@ -2557,66 +2557,97 @@ void
 CodeGeneratorARM::visitWasmTruncateToInt32(LWasmTruncateToInt32* lir)
 {
     auto input = ToFloatRegister(lir->input());
     auto output = ToRegister(lir->output());
 
     MWasmTruncateToInt32* mir = lir->mir();
     MIRType fromType = mir->input()->type();
 
-    auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input);
-    addOutOfLineCode(ool, mir);
-    masm.wasmTruncateToInt32(input, output, fromType, mir->isUnsigned(), ool->entry());
-    masm.bind(ool->rejoin());
+    OutOfLineWasmTruncateCheck* ool = nullptr;
+    Label* oolEntry = nullptr;
+    if (!lir->mir()->isSaturating()) {
+        ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input, Register::Invalid());
+        addOutOfLineCode(ool, mir);
+        oolEntry = ool->entry();
+    }
+
+    masm.wasmTruncateToInt32(input, output, fromType, mir->isUnsigned(), mir->isSaturating(),
+                             oolEntry);
+
+    if (!lir->mir()->isSaturating()) {
+        masm.bind(ool->rejoin());
+    }
 }
 
 void
 CodeGeneratorARM::visitWasmTruncateToInt64(LWasmTruncateToInt64* lir)
 {
     FloatRegister input = ToFloatRegister(lir->input());
     FloatRegister inputDouble = input;
     Register64 output = ToOutRegister64(lir);
 
     MWasmTruncateToInt64* mir = lir->mir();
     MIRType fromType = mir->input()->type();
 
-    auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input);
-    addOutOfLineCode(ool, mir);
+    OutOfLineWasmTruncateCheck* ool = nullptr;
+    if (!lir->mir()->isSaturating()) {
+        ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input, Register64::Invalid());
+        addOutOfLineCode(ool, mir);
+    }
 
     ScratchDoubleScope scratchScope(masm);
     if (fromType == MIRType::Float32) {
         inputDouble = ScratchDoubleReg;
         masm.convertFloat32ToDouble(input, inputDouble);
     }
 
     masm.Push(input);
 
     masm.setupWasmABICall();
     masm.passABIArg(inputDouble, MoveOp::DOUBLE);
 
-    if (lir->mir()->isUnsigned())
-        masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToUint64);
-    else
-        masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToInt64);
+    if (lir->mir()->isSaturating()) {
+        if (lir->mir()->isUnsigned())
+            masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::SaturatingTruncateDoubleToUint64);
+        else
+            masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::SaturatingTruncateDoubleToInt64);
+    } else {
+        if (lir->mir()->isUnsigned())
+            masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToUint64);
+        else
+            masm.callWithABI(mir->bytecodeOffset(), wasm::SymbolicAddress::TruncateDoubleToInt64);
+    }
 
     masm.Pop(input);
 
-    ScratchRegisterScope scratch(masm);
-    masm.ma_cmp(output.high, Imm32(0x80000000), scratch);
-    masm.as_cmp(output.low, Imm8(0x00000000), Assembler::Equal);
-    masm.ma_b(ool->entry(), Assembler::Equal);
-
-    masm.bind(ool->rejoin());
+    // TruncateDoubleTo{UI,I}nt64 returns 0x8000000000000000 to indicate
+    // exceptional results, so check for that and produce the appropriate
+    // traps. The Saturating form always returns a normal value and never
+    // needs traps.
+    if (!lir->mir()->isSaturating()) {
+        ScratchRegisterScope scratch(masm);
+        masm.ma_cmp(output.high, Imm32(0x80000000), scratch);
+        masm.as_cmp(output.low, Imm8(0x00000000), Assembler::Equal);
+        masm.ma_b(ool->entry(), Assembler::Equal);
+
+        masm.bind(ool->rejoin());
+    }
 
     MOZ_ASSERT(ReturnReg64 == output);
 }
 
 void
 CodeGeneratorARM::visitOutOfLineWasmTruncateCheck(OutOfLineWasmTruncateCheck* ool)
 {
+    // On ARM, saturating truncation codegen handles saturating itself rather than
+    // relying on out-of-line fixup code.
+    if (ool->isSaturating())
+        return;
+
     masm.outOfLineWasmTruncateToIntCheck(ool->input(), ool->fromType(), ool->toType(),
                                          ool->isUnsigned(), ool->rejoin(),
                                          ool->bytecodeOffset());
 }
 
 void
 CodeGeneratorARM::visitInt64ToFloatingPointCall(LInt64ToFloatingPointCall* lir)
 {
--- a/js/src/jit/arm/MacroAssembler-arm.cpp
+++ b/js/src/jit/arm/MacroAssembler-arm.cpp
@@ -4921,69 +4921,74 @@ MacroAssembler::storeUnboxedValue(const 
 
 CodeOffset
 MacroAssembler::wasmTrapInstruction()
 {
     return CodeOffset(as_illegal_trap().getOffset());
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry)
-{
-    wasmTruncateToInt32(input, output, MIRType::Double, /* isUnsigned= */ true, oolEntry);
-}
-
-void
-MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output, Label* oolEntry)
-{
-    wasmTruncateToInt32(input, output, MIRType::Double, /* isUnsigned= */ false, oolEntry);
-}
-
-void
-MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry)
-{
-    wasmTruncateToInt32(input, output, MIRType::Float32, /* isUnsigned= */ true, oolEntry);
-}
-
-void
-MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output, Label* oolEntry)
-{
-    wasmTruncateToInt32(input, output, MIRType::Float32, /* isUnsigned= */ false, oolEntry);
-}
-
-void
-MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, bool isUnsigned,
+MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output,
+                                           bool isSaturating, Label* oolEntry)
+{
+    wasmTruncateToInt32(input, output, MIRType::Double, /* isUnsigned= */ true, isSaturating,
+                        oolEntry);
+}
+
+void
+MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output,
+                                          bool isSaturating, Label* oolEntry)
+{
+    wasmTruncateToInt32(input, output, MIRType::Double, /* isUnsigned= */ false,isSaturating,
+                         oolEntry);
+}
+
+void
+MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output,
+                                            bool isSaturating, Label* oolEntry)
+{
+    wasmTruncateToInt32(input, output, MIRType::Float32, /* isUnsigned= */ true,isSaturating,
+                         oolEntry);
+}
+
+void
+MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output,
+                                           bool isSaturating, Label* oolEntry)
+{
+    wasmTruncateToInt32(input, output, MIRType::Float32, /* isUnsigned= */ false,isSaturating,
+                         oolEntry);
+}
+
+void
+MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, Register output, TruncFlags flags,
                                              wasm::BytecodeOffset off, Label* rejoin)
 {
-    outOfLineWasmTruncateToIntCheck(input, MIRType::Float32, MIRType::Int32, isUnsigned,
-                                    rejoin, off);
-}
-
-void
-MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, bool isUnsigned,
+    outOfLineWasmTruncateToIntCheck(input, MIRType::Float32, MIRType::Int32, flags, rejoin, off);
+}
+
+void
+MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, Register output, TruncFlags flags,
                                              wasm::BytecodeOffset off, Label* rejoin)
 {
-    outOfLineWasmTruncateToIntCheck(input, MIRType::Double, MIRType::Int32, isUnsigned,
-                                    rejoin, off);
-}
-
-void
-MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, bool isUnsigned,
+    outOfLineWasmTruncateToIntCheck(input, MIRType::Double, MIRType::Int32, flags, rejoin, off);
+}
+
+void
+MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, Register64 output, TruncFlags flags,
                                              wasm::BytecodeOffset off, Label* rejoin)
 {
-    outOfLineWasmTruncateToIntCheck(input, MIRType::Float32, MIRType::Int64, isUnsigned,
-                                    rejoin, off);
-}
-
-void
-MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
-{
-    outOfLineWasmTruncateToIntCheck(input, MIRType::Double, MIRType::Int64, isUnsigned,
-                                    rejoin, off);
+    outOfLineWasmTruncateToIntCheck(input, MIRType::Float32, MIRType::Int64, flags, rejoin, off);
+}
+
+void
+MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, Register64 output, TruncFlags flags,
+                                             wasm::BytecodeOffset off,
+                                             Label* rejoin)
+{
+    outOfLineWasmTruncateToIntCheck(input, MIRType::Double, MIRType::Int64, flags, rejoin, off);
 }
 
 void
 MacroAssembler::wasmLoad(const wasm::MemoryAccessDesc& access, Register memoryBase, Register ptr,
                          Register ptrScratch, AnyRegister output)
 {
     wasmLoadImpl(access, memoryBase, ptr, ptrScratch, output, Register64::Invalid());
 }
@@ -5790,20 +5795,20 @@ MacroAssembler::convertUInt64ToDouble(Re
     convertUInt32ToDouble(src.low, scratchDouble);
     addDouble(scratchDouble, dest);
 }
 
 //}}} check_macroassembler_style
 
 void
 MacroAssemblerARM::wasmTruncateToInt32(FloatRegister input, Register output, MIRType fromType,
-                                       bool isUnsigned, Label* oolEntry)
+                                       bool isUnsigned, bool isSaturating, Label* oolEntry)
 {
     // vcvt* converts NaN into 0, so check for NaNs here.
-    {
+    if (!isSaturating) {
         if (fromType == MIRType::Double)
             asMasm().compareDouble(input, input);
         else if (fromType == MIRType::Float32)
             asMasm().compareFloat(input, input);
         else
             MOZ_CRASH("unexpected type in visitWasmTruncateToInt32");
 
         ma_b(oolEntry, Assembler::VFP_Unordered);
@@ -5821,44 +5826,55 @@ MacroAssemblerARM::wasmTruncateToInt32(F
             ma_vcvt_F64_U32(input, scratch);
         else if (fromType == MIRType::Float32)
             ma_vcvt_F32_U32(input, scratch);
         else
             MOZ_CRASH("unexpected type in visitWasmTruncateToInt32");
 
         ma_vxfer(scratch, output);
 
-        // int32_t(UINT32_MAX) == -1.
-        ma_cmp(output, Imm32(-1), scratchReg);
-        as_cmp(output, Imm8(0), Assembler::NotEqual);
-        ma_b(oolEntry, Assembler::Equal);
+        if (!isSaturating) {
+            // int32_t(UINT32_MAX) == -1.
+            ma_cmp(output, Imm32(-1), scratchReg);
+            as_cmp(output, Imm8(0), Assembler::NotEqual);
+            ma_b(oolEntry, Assembler::Equal);
+        }
 
         return;
     }
 
     scratch = scratchScope.sintOverlay();
 
     if (fromType == MIRType::Double)
         ma_vcvt_F64_I32(input, scratch);
     else if (fromType == MIRType::Float32)
         ma_vcvt_F32_I32(input, scratch);
     else
         MOZ_CRASH("unexpected type in visitWasmTruncateToInt32");
 
     ma_vxfer(scratch, output);
-    ma_cmp(output, Imm32(INT32_MAX), scratchReg);
-    ma_cmp(output, Imm32(INT32_MIN), scratchReg, Assembler::NotEqual);
-    ma_b(oolEntry, Assembler::Equal);
+
+    if (!isSaturating) {
+        ma_cmp(output, Imm32(INT32_MAX), scratchReg);
+        ma_cmp(output, Imm32(INT32_MIN), scratchReg, Assembler::NotEqual);
+        ma_b(oolEntry, Assembler::Equal);
+    }
 }
 
 void
 MacroAssemblerARM::outOfLineWasmTruncateToIntCheck(FloatRegister input, MIRType fromType,
-                                                   MIRType toType, bool isUnsigned, Label* rejoin,
-                                                   wasm::BytecodeOffset trapOffset)
-{
+                                                   MIRType toType, TruncFlags flags,
+                                                   Label* rejoin, wasm::BytecodeOffset trapOffset)
+{
+    // On ARM, saturating truncation codegen handles saturating itself rather
+    // than relying on out-of-line fixup code.
+    if (flags & TRUNC_SATURATING)
+        return;
+
+    bool isUnsigned = flags & TRUNC_UNSIGNED;
     ScratchDoubleScope scratchScope(asMasm());
     FloatRegister scratch;
 
     // Eagerly take care of NaNs.
     Label inputIsNaN;
     if (fromType == MIRType::Double)
         asMasm().branchDouble(Assembler::DoubleUnordered, input, input, &inputIsNaN);
     else if (fromType == MIRType::Float32)
--- a/js/src/jit/arm/MacroAssembler-arm.h
+++ b/js/src/jit/arm/MacroAssembler-arm.h
@@ -94,20 +94,20 @@ class MacroAssemblerARM : public Assembl
     void convertFloat32ToInt32(FloatRegister src, Register dest, Label* fail,
                                bool negativeZeroCheck = true);
 
     void convertFloat32ToDouble(FloatRegister src, FloatRegister dest);
     void convertInt32ToFloat32(Register src, FloatRegister dest);
     void convertInt32ToFloat32(const Address& src, FloatRegister dest);
 
     void wasmTruncateToInt32(FloatRegister input, Register output, MIRType fromType,
-                             bool isUnsigned, Label* oolEntry);
+                             bool isUnsigned, bool isSaturating, Label* oolEntry);
     void outOfLineWasmTruncateToIntCheck(FloatRegister input, MIRType fromType,
-                                         MIRType toType, bool isUnsigned, Label* rejoin,
-                                         wasm::BytecodeOffset trapOffset);
+                                         MIRType toType, TruncFlags flags,
+                                         Label* rejoin, wasm::BytecodeOffset trapOffset);
 
     // Somewhat direct wrappers for the low-level assembler funcitons
     // bitops. Attempt to encode a virtual alu instruction using two real
     // instructions.
   private:
     bool alu_dbl(Register src1, Imm32 imm, Register dest, ALUOp op,
                  SBit s, Condition c);
 
--- a/js/src/jit/arm64/MacroAssembler-arm64.cpp
+++ b/js/src/jit/arm64/MacroAssembler-arm64.cpp
@@ -908,91 +908,101 @@ MacroAssembler::comment(const char* msg)
 
 CodeOffset
 MacroAssembler::wasmTrapInstruction()
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output,
+                                           bool isSaturating, Label* oolEntry)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output,
+                                          bool isSaturating, Label* oolEntry)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output,
+                                            bool isSaturating, Label* oolEntry)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output,
+                                           bool isSaturating, Label* oolEntry)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output,
+                                          bool isSaturating, Label* oolEntry,
                                           Label* oolRejoin, FloatRegister tempDouble)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output,
+                                           bool isSaturating, Label* oolEntry,
                                            Label* oolRejoin, FloatRegister tempDouble)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output,
+                                           bool isSaturating, Label* oolEntry,
                                            Label* oolRejoin, FloatRegister tempDouble)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output,
+                                            bool isSaturating, Label* oolEntry,
                                             Label* oolRejoin, FloatRegister tempDouble)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, bool isUnsigned,
+MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, Register output, TruncFlags flags,
                                              wasm::BytecodeOffset off, Label* rejoin)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, bool isUnsigned,
+MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, Register output, TruncFlags flags,
                                              wasm::BytecodeOffset off, Label* rejoin)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
+MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, Register64 output,
+                                             TruncFlags flags, wasm::BytecodeOffset off,
+                                             Label* rejoin)
 {
     MOZ_CRASH("NYI");
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
+MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, Register64 output,
+                                             TruncFlags flags, wasm::BytecodeOffset off,
+                                             Label* rejoin)
 {
     MOZ_CRASH("NYI");
 }
 
 // ========================================================================
 // Convert floating point.
 
 bool
--- a/js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp
+++ b/js/src/jit/mips-shared/CodeGenerator-mips-shared.cpp
@@ -1481,19 +1481,19 @@ CodeGeneratorMIPSShared::visitWasmTrunca
         else if (fromType == MIRType::Float32)
             masm.wasmTruncateFloat32ToUInt32(input, output, oolEntry);
         else
             MOZ_CRASH("unexpected type");
         return;
     }
 
     if (fromType == MIRType::Double)
-        masm.wasmTruncateDoubleToInt32(input, output, oolEntry);
+        masm.wasmTruncateDoubleToInt32(input, output, mir->isSaturating(), oolEntry);
     else if (fromType == MIRType::Float32)
-        masm.wasmTruncateFloat32ToInt32(input, output, oolEntry);
+        masm.wasmTruncateFloat32ToInt32(input, output, mir->isSaturating(), oolEntry);
     else
         MOZ_CRASH("unexpected type");
 
     masm.bind(ool->rejoin());
 }
 
 void
 CodeGeneratorMIPSShared::visitOutOfLineWasmTruncateCheck(OutOfLineWasmTruncateCheck* ool)
--- a/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp
+++ b/js/src/jit/mips-shared/MacroAssembler-mips-shared.cpp
@@ -1615,28 +1615,30 @@ CodeOffset
 MacroAssembler::wasmTrapInstruction()
 {
     CodeOffset offset(currentOffset());
     as_teq(zero, zero, WASM_TRAP);
     return offset;
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output, bool isSaturating,
+                                          Label* oolEntry)
 {
     as_truncwd(ScratchFloat32Reg, input);
     as_cfc1(ScratchRegister, Assembler::FCSR);
     moveFromFloat32(ScratchFloat32Reg, output);
     ma_ext(ScratchRegister, ScratchRegister, 6, 1);
     ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual);
 }
 
 
 void
-MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output, bool isSaturating,
+                                           Label* oolEntry)
 {
     as_truncws(ScratchFloat32Reg, input);
     as_cfc1(ScratchRegister, Assembler::FCSR);
     moveFromFloat32(ScratchFloat32Reg, output);
     ma_ext(ScratchRegister, ScratchRegister, 6, 1);
     ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual);
 }
 
--- a/js/src/jit/mips32/MacroAssembler-mips32.cpp
+++ b/js/src/jit/mips32/MacroAssembler-mips32.cpp
@@ -2347,18 +2347,20 @@ template void
 MacroAssembler::storeUnboxedValue(const ConstantOrRegister& value, MIRType valueType,
                                   const Address& dest, MIRType slotType);
 template void
 MacroAssembler::storeUnboxedValue(const ConstantOrRegister& value, MIRType valueType,
                                   const BaseIndex& dest, MIRType slotType);
 
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                           Label* oolEntry)
 {
+    MOZ_ASSERT(!isSaturating, "NYI");
 
     loadConstantDouble(double(-1.0), ScratchDoubleReg);
     branchDouble(Assembler::DoubleLessThanOrEqual, input, ScratchDoubleReg, oolEntry);
 
     loadConstantDouble(double(UINT32_MAX) + 1.0, ScratchDoubleReg);
     branchDouble(Assembler::DoubleGreaterThanOrEqualOrUnordered, input, ScratchDoubleReg, oolEntry);
     Label done, simple;
     loadConstantDouble(double(0x80000000UL), ScratchDoubleReg);
@@ -2371,18 +2373,21 @@ MacroAssembler::wasmTruncateDoubleToUInt
     ma_b(&done);
     bind(&simple);
     as_truncwd(ScratchDoubleReg, input);
     moveFromFloat32(ScratchDoubleReg, output);
     bind(&done);
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                            Label* oolEntry)
 {
+    MOZ_ASSERT(!isSaturating, "NYI");
+
     loadConstantFloat32(double(-1.0), ScratchDoubleReg);
     branchFloat(Assembler::DoubleLessThanOrEqualOrUnordered, input, ScratchDoubleReg, oolEntry);
 
     loadConstantFloat32(double(UINT32_MAX) + 1.0, ScratchDoubleReg);
     branchFloat(Assembler::DoubleGreaterThanOrEqualOrUnordered, input, ScratchDoubleReg, oolEntry);
     Label done, simple;
     loadConstantFloat32(double(0x80000000UL), ScratchDoubleReg);
     branchFloat(Assembler::DoubleLessThan, input, ScratchDoubleReg, &simple);
--- a/js/src/jit/mips64/MacroAssembler-mips64.cpp
+++ b/js/src/jit/mips64/MacroAssembler-mips64.cpp
@@ -2433,32 +2433,38 @@ template void
 MacroAssembler::storeUnboxedValue(const ConstantOrRegister& value, MIRType valueType,
                                   const Address& dest, MIRType slotType);
 template void
 MacroAssembler::storeUnboxedValue(const ConstantOrRegister& value, MIRType valueType,
                                   const BaseIndex& dest, MIRType slotType);
 
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                           Label* oolEntry)
 {
+    MOZ_ASSERT(!isSaturating, "NYI");
+
     as_truncld(ScratchDoubleReg, input);
     moveFromDoubleHi(ScratchDoubleReg, output);
     as_cfc1(ScratchRegister, Assembler::FCSR);
     ma_ext(ScratchRegister, ScratchRegister, 6, 1);
     ma_or(ScratchRegister, output);
     moveFromFloat32(ScratchDoubleReg, output);
     ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual);
 
 
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                            Label* oolEntry)
 {
+    MOZ_ASSERT(!isSaturating, "NYI");
+
     as_truncls(ScratchDoubleReg, input);
     moveFromDoubleHi(ScratchDoubleReg, output);
     as_cfc1(ScratchRegister, Assembler::FCSR);
     ma_ext(ScratchRegister, ScratchRegister, 6, 1);
     ma_or(ScratchRegister, output);
     moveFromFloat32(ScratchDoubleReg, output);
     ma_b(ScratchRegister, Imm32(0), oolEntry, Assembler::NotEqual);
 
--- a/js/src/jit/shared/CodeGenerator-shared.h
+++ b/js/src/jit/shared/CodeGenerator-shared.h
@@ -849,37 +849,45 @@ CodeGeneratorShared::visitOutOfLineCallV
     masm.jump(ool->rejoin());
 }
 
 class OutOfLineWasmTruncateCheck : public OutOfLineCodeBase<CodeGeneratorShared>
 {
     MIRType fromType_;
     MIRType toType_;
     FloatRegister input_;
-    bool isUnsigned_;
+    Register output_;
+    Register64 output64_;
+    TruncFlags flags_;
     wasm::BytecodeOffset bytecodeOffset_;
 
   public:
-    OutOfLineWasmTruncateCheck(MWasmTruncateToInt32* mir, FloatRegister input)
-      : fromType_(mir->input()->type()), toType_(MIRType::Int32), input_(input),
-        isUnsigned_(mir->isUnsigned()), bytecodeOffset_(mir->bytecodeOffset())
+    OutOfLineWasmTruncateCheck(MWasmTruncateToInt32* mir, FloatRegister input, Register output)
+      : fromType_(mir->input()->type()), toType_(MIRType::Int32), input_(input), output_(output),
+        output64_(Register64::Invalid()), flags_(mir->flags()),
+        bytecodeOffset_(mir->bytecodeOffset())
     { }
 
-    OutOfLineWasmTruncateCheck(MWasmTruncateToInt64* mir, FloatRegister input)
+    OutOfLineWasmTruncateCheck(MWasmTruncateToInt64* mir, FloatRegister input, Register64 output)
       : fromType_(mir->input()->type()), toType_(MIRType::Int64), input_(input),
-        isUnsigned_(mir->isUnsigned()), bytecodeOffset_(mir->bytecodeOffset())
+        output_(Register::Invalid()), output64_(output), flags_(mir->flags()),
+        bytecodeOffset_(mir->bytecodeOffset())
     { }
 
     void accept(CodeGeneratorShared* codegen) override {
         codegen->visitOutOfLineWasmTruncateCheck(this);
     }
 
     FloatRegister input() const { return input_; }
+    Register output() const { return output_; }
+    Register64 output64() const { return output64_; }
     MIRType toType() const { return toType_; }
     MIRType fromType() const { return fromType_; }
-    bool isUnsigned() const { return isUnsigned_; }
+    bool isUnsigned() const { return flags_ & TRUNC_UNSIGNED; }
+    bool isSaturating() const { return flags_ & TRUNC_SATURATING; }
+    TruncFlags flags() const { return flags_; }
     wasm::BytecodeOffset bytecodeOffset() const { return bytecodeOffset_; }
 };
 
 } // namespace jit
 } // namespace js
 
 #endif /* jit_shared_CodeGenerator_shared_h */
--- a/js/src/jit/x64/CodeGenerator-x64.cpp
+++ b/js/src/jit/x64/CodeGenerator-x64.cpp
@@ -735,33 +735,38 @@ CodeGeneratorX64::visitWasmTruncateToInt
     FloatRegister input = ToFloatRegister(lir->input());
     Register64 output = ToOutRegister64(lir);
 
     MWasmTruncateToInt64* mir = lir->mir();
     MIRType inputType = mir->input()->type();
 
     MOZ_ASSERT(inputType == MIRType::Double || inputType == MIRType::Float32);
 
-    auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input);
+    auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input, output);
     addOutOfLineCode(ool, mir);
 
     FloatRegister temp = mir->isUnsigned() ? ToFloatRegister(lir->temp()) : InvalidFloatReg;
 
     Label* oolEntry = ool->entry();
     Label* oolRejoin = ool->rejoin();
+    bool isSaturating = mir->isSaturating();
     if (inputType == MIRType::Double) {
         if (mir->isUnsigned())
-            masm.wasmTruncateDoubleToUInt64(input, output, oolEntry, oolRejoin, temp);
+            masm.wasmTruncateDoubleToUInt64(input, output, isSaturating,
+                                            oolEntry, oolRejoin, temp);
         else
-            masm.wasmTruncateDoubleToInt64(input, output, oolEntry, oolRejoin, temp);
+            masm.wasmTruncateDoubleToInt64(input, output, isSaturating,
+                                           oolEntry, oolRejoin, temp);
     } else {
         if (mir->isUnsigned())
-            masm.wasmTruncateFloat32ToUInt64(input, output, oolEntry, oolRejoin, temp);
+            masm.wasmTruncateFloat32ToUInt64(input, output, isSaturating,
+                                             oolEntry, oolRejoin, temp);
         else
-            masm.wasmTruncateFloat32ToInt64(input, output, oolEntry, oolRejoin, temp);
+            masm.wasmTruncateFloat32ToInt64(input, output, isSaturating,
+                                            oolEntry, oolRejoin, temp);
     }
 }
 
 void
 CodeGeneratorX64::visitInt64ToFloatingPoint(LInt64ToFloatingPoint* lir)
 {
     Register64 input = ToRegister64(lir->getInt64Operand(0));
     FloatRegister output = ToFloatRegister(lir->output());
--- a/js/src/jit/x64/MacroAssembler-x64.cpp
+++ b/js/src/jit/x64/MacroAssembler-x64.cpp
@@ -765,62 +765,64 @@ MacroAssembler::wasmStore(const wasm::Me
         MOZ_CRASH("unexpected array type");
     }
     append(access, storeOffset, framePushed());
 
     memoryBarrierAfter(access.sync());
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                           Label* oolEntry)
 {
     vcvttsd2sq(input, output);
 
     // Check that the result is in the uint32_t range.
     ScratchRegisterScope scratch(*this);
     move32(Imm32(0xffffffff), scratch);
     cmpq(scratch, output);
     j(Assembler::Above, oolEntry);
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                            Label* oolEntry)
 {
     vcvttss2sq(input, output);
 
     // Check that the result is in the uint32_t range.
     ScratchRegisterScope scratch(*this);
     move32(Imm32(0xffffffff), scratch);
     cmpq(scratch, output);
     j(Assembler::Above, oolEntry);
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                          Label* oolRejoin, FloatRegister tempReg)
+MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                          Label* oolEntry, Label* oolRejoin, FloatRegister tempReg)
 {
     vcvttsd2sq(input, output.reg);
     cmpq(Imm32(1), output.reg);
     j(Assembler::Overflow, oolEntry);
     bind(oolRejoin);
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                           Label* oolRejoin, FloatRegister tempReg)
+MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                           Label* oolEntry, Label* oolRejoin, FloatRegister tempReg)
 {
     vcvttss2sq(input, output.reg);
     cmpq(Imm32(1), output.reg);
     j(Assembler::Overflow, oolEntry);
     bind(oolRejoin);
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                           Label* oolRejoin, FloatRegister tempReg)
+MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                           Label* oolEntry, Label* oolRejoin, FloatRegister tempReg)
 {
     // If the input < INT64_MAX, vcvttsd2sq will do the right thing, so
     // we use it directly. Else, we subtract INT64_MAX, convert to int64,
     // and then add INT64_MAX to the result.
 
     Label isLarge;
 
     ScratchDoubleScope scratch(*this);
@@ -839,17 +841,18 @@ MacroAssembler::wasmTruncateDoubleToUInt
     testq(output.reg, output.reg);
     j(Assembler::Signed, oolEntry);
     or64(Imm64(0x8000000000000000), output);
 
     bind(oolRejoin);
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output,
+                                            bool isSaturating, Label* oolEntry,
                                             Label* oolRejoin, FloatRegister tempReg)
 {
     // If the input < INT64_MAX, vcvttss2sq will do the right thing, so
     // we use it directly. Else, we subtract INT64_MAX, convert to int64,
     // and then add INT64_MAX to the result.
 
     Label isLarge;
 
--- a/js/src/jit/x86-shared/CodeGenerator-x86-shared.cpp
+++ b/js/src/jit/x86-shared/CodeGenerator-x86-shared.cpp
@@ -444,34 +444,36 @@ CodeGeneratorX86Shared::visitWasmTruncat
     FloatRegister input = ToFloatRegister(lir->input());
     Register output = ToRegister(lir->output());
 
     MWasmTruncateToInt32* mir = lir->mir();
     MIRType inputType = mir->input()->type();
 
     MOZ_ASSERT(inputType == MIRType::Double || inputType == MIRType::Float32);
 
-    auto* ool = new (alloc()) OutOfLineWasmTruncateCheck(mir, input);
+    auto* ool = new (alloc()) OutOfLineWasmTruncateCheck(mir, input, output);
     addOutOfLineCode(ool, mir);
 
     Label* oolEntry = ool->entry();
     if (mir->isUnsigned()) {
         if (inputType == MIRType::Double)
-            masm.wasmTruncateDoubleToUInt32(input, output, oolEntry);
+            masm.wasmTruncateDoubleToUInt32(input, output, mir->isSaturating(), oolEntry);
         else if (inputType == MIRType::Float32)
-            masm.wasmTruncateFloat32ToUInt32(input, output, oolEntry);
+            masm.wasmTruncateFloat32ToUInt32(input, output, mir->isSaturating(), oolEntry);
         else
             MOZ_CRASH("unexpected type");
+        if (mir->isSaturating())
+            masm.bind(ool->rejoin());
         return;
     }
 
     if (inputType == MIRType::Double)
-        masm.wasmTruncateDoubleToInt32(input, output, oolEntry);
+        masm.wasmTruncateDoubleToInt32(input, output, mir->isSaturating(), oolEntry);
     else if (inputType == MIRType::Float32)
-        masm.wasmTruncateFloat32ToInt32(input, output, oolEntry);
+        masm.wasmTruncateFloat32ToInt32(input, output, mir->isSaturating(), oolEntry);
     else
         MOZ_CRASH("unexpected type");
 
     masm.bind(ool->rejoin());
 }
 
 bool
 CodeGeneratorX86Shared::generateOutOfLineCode()
@@ -4359,34 +4361,36 @@ CodeGeneratorX86Shared::setReturnDoubleR
     regs->add(ReturnDoubleReg);
     regs->add(ReturnSimd128Reg);
 }
 
 void
 CodeGeneratorX86Shared::visitOutOfLineWasmTruncateCheck(OutOfLineWasmTruncateCheck* ool)
 {
     FloatRegister input = ool->input();
+    Register output = ool->output();
+    Register64 output64 = ool->output64();
     MIRType fromType = ool->fromType();
     MIRType toType = ool->toType();
     Label* oolRejoin = ool->rejoin();
-    bool isUnsigned = ool->isUnsigned();
+    TruncFlags flags = ool->flags();
     wasm::BytecodeOffset off = ool->bytecodeOffset();
 
     if (fromType == MIRType::Float32) {
         if (toType == MIRType::Int32)
-            masm.oolWasmTruncateCheckF32ToI32(input, isUnsigned, off, oolRejoin);
+            masm.oolWasmTruncateCheckF32ToI32(input, output, flags, off, oolRejoin);
         else if (toType == MIRType::Int64)
-            masm.oolWasmTruncateCheckF32ToI64(input, isUnsigned, off, oolRejoin);
+            masm.oolWasmTruncateCheckF32ToI64(input, output64, flags, off, oolRejoin);
         else
             MOZ_CRASH("unexpected type");
     } else if (fromType == MIRType::Double) {
         if (toType == MIRType::Int32)
-            masm.oolWasmTruncateCheckF64ToI32(input, isUnsigned, off, oolRejoin);
+            masm.oolWasmTruncateCheckF64ToI32(input, output, flags, off, oolRejoin);
         else if (toType == MIRType::Int64)
-            masm.oolWasmTruncateCheckF64ToI64(input, isUnsigned, off, oolRejoin);
+            masm.oolWasmTruncateCheckF64ToI64(input, output64, flags, off, oolRejoin);
         else
             MOZ_CRASH("unexpected type");
     } else {
         MOZ_CRASH("unexpected type");
     }
 }
 
 void
--- a/js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp
+++ b/js/src/jit/x86-shared/MacroAssembler-x86-shared.cpp
@@ -697,62 +697,130 @@ struct MOZ_RAII AutoHandleWasmTruncateTo
         masm.wasmTrap(wasm::Trap::IntegerOverflow, off);
 
         masm.bind(&inputIsNaN);
         masm.wasmTrap(wasm::Trap::InvalidConversionToInteger, off);
     }
 };
 
 void
-MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToInt32(FloatRegister input, Register output,
+                                          bool isSaturating, Label* oolEntry)
 {
     vcvttsd2si(input, output);
     cmp32(output, Imm32(1));
     j(Assembler::Overflow, oolEntry);
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToInt32(FloatRegister input, Register output,
+                                           bool isSaturating, Label* oolEntry)
 {
     vcvttss2si(input, output);
     cmp32(output, Imm32(1));
     j(Assembler::Overflow, oolEntry);
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
+MacroAssembler::oolWasmTruncateCheckF64ToI32(FloatRegister input, Register output,
+                                             TruncFlags flags, wasm::BytecodeOffset off,
+                                             Label* rejoin)
 {
+    bool isUnsigned = flags & TRUNC_UNSIGNED;
+    bool isSaturating = flags & TRUNC_SATURATING;
+
     AutoHandleWasmTruncateToIntErrors traps(*this, off);
 
+    if (isSaturating) {
+        if (isUnsigned) {
+            // Negative overflow and NaN both are converted to 0, and the only other case
+            // is positive overflow which is converted to UINT32_MAX.
+            Label nonNegative;
+            loadConstantDouble(0.0, ScratchDoubleReg);
+            branchDouble(Assembler::DoubleGreaterThanOrEqual, input, ScratchDoubleReg, &nonNegative);
+            move32(Imm32(0), output);
+            jump(rejoin);
+            bind(&nonNegative);
+
+            move32(Imm32(UINT32_MAX), output);
+        } else {
+            // Negative overflow is already saturated to INT32_MIN, so we only have
+            // to handle NaN and positive overflow here.
+            Label notNaN;
+            branchDouble(Assembler::DoubleOrdered, input, input, &notNaN);
+            move32(Imm32(0), output);
+            jump(rejoin);
+            bind(&notNaN);
+
+            loadConstantDouble(0.0, ScratchDoubleReg);
+            branchDouble(Assembler::DoubleLessThan, input, ScratchDoubleReg, rejoin);
+            sub32(Imm32(1), output);
+        }
+        jump(rejoin);
+        return;
+    }
+
     // Eagerly take care of NaNs.
     branchDouble(Assembler::DoubleUnordered, input, input, &traps.inputIsNaN);
 
     // For unsigned, fall through to intOverflow failure case.
     if (isUnsigned)
         return;
 
     // Handle special values.
 
     // We've used vcvttsd2si. The only valid double values that can
     // truncate to INT32_MIN are in ]INT32_MIN - 1; INT32_MIN].
     loadConstantDouble(double(INT32_MIN) - 1.0, ScratchDoubleReg);
     branchDouble(Assembler::DoubleLessThanOrEqual, input, ScratchDoubleReg, &traps.intOverflow);
 
-    loadConstantDouble(double(INT32_MIN), ScratchDoubleReg);
+    loadConstantDouble(0.0, ScratchDoubleReg);
     branchDouble(Assembler::DoubleGreaterThan, input, ScratchDoubleReg, &traps.intOverflow);
     jump(rejoin);
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
+MacroAssembler::oolWasmTruncateCheckF32ToI32(FloatRegister input, Register output,
+                                             TruncFlags flags, wasm::BytecodeOffset off,
+                                             Label* rejoin)
 {
+    bool isUnsigned = flags & TRUNC_UNSIGNED;
+    bool isSaturating = flags & TRUNC_SATURATING;
+
     AutoHandleWasmTruncateToIntErrors traps(*this, off);
 
+    if (isSaturating) {
+        if (isUnsigned) {
+            // Negative overflow and NaN both are converted to 0, and the only other case
+            // is positive overflow which is converted to UINT32_MAX.
+            Label nonNegative;
+            loadConstantFloat32(0.0f, ScratchDoubleReg);
+            branchFloat(Assembler::DoubleGreaterThanOrEqual, input, ScratchDoubleReg, &nonNegative);
+            move32(Imm32(0), output);
+            jump(rejoin);
+            bind(&nonNegative);
+
+            move32(Imm32(UINT32_MAX), output);
+        } else {
+            // Negative overflow is already saturated to INT32_MIN, so we only have
+            // to handle NaN and positive overflow here.
+            Label notNaN;
+            branchFloat(Assembler::DoubleOrdered, input, input, &notNaN);
+            move32(Imm32(0), output);
+            jump(rejoin);
+            bind(&notNaN);
+
+            loadConstantFloat32(0.0f, ScratchFloat32Reg);
+            branchFloat(Assembler::DoubleLessThan, input, ScratchFloat32Reg, rejoin);
+            sub32(Imm32(1), output);
+        }
+        jump(rejoin);
+        return;
+    }
+
     // Eagerly take care of NaNs.
     branchFloat(Assembler::DoubleUnordered, input, input, &traps.inputIsNaN);
 
     // For unsigned, fall through to intOverflow failure case.
     if (isUnsigned)
         return;
 
     // Handle special values.
@@ -761,54 +829,120 @@ MacroAssembler::oolWasmTruncateCheckF32T
     // float(INT32_MIN), which is the only legimitate input that
     // would truncate to INT32_MIN.
     loadConstantFloat32(float(INT32_MIN), ScratchFloat32Reg);
     branchFloat(Assembler::DoubleNotEqual, input, ScratchFloat32Reg, &traps.intOverflow);
     jump(rejoin);
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
+MacroAssembler::oolWasmTruncateCheckF64ToI64(FloatRegister input, Register64 output,
+                                             TruncFlags flags, wasm::BytecodeOffset off,
+                                             Label* rejoin)
 {
+    bool isUnsigned = flags & TRUNC_UNSIGNED;
+    bool isSaturating = flags & TRUNC_SATURATING;
+
     AutoHandleWasmTruncateToIntErrors traps(*this, off);
 
+    if (isSaturating) {
+        if (isUnsigned) {
+            // Negative overflow and NaN both are converted to 0, and the only other case
+            // is positive overflow which is converted to UINT64_MAX.
+            Label nonNegative;
+            loadConstantDouble(0.0, ScratchDoubleReg);
+            branchDouble(Assembler::DoubleGreaterThanOrEqual, input, ScratchDoubleReg, &nonNegative);
+            move64(Imm64(0), output);
+            jump(rejoin);
+            bind(&nonNegative);
+
+            move64(Imm64(UINT64_MAX), output);
+        } else {
+            // Negative overflow is already saturated to INT64_MIN, so we only have
+            // to handle NaN and positive overflow here.
+            Label notNaN;
+            branchDouble(Assembler::DoubleOrdered, input, input, &notNaN);
+            move64(Imm64(0), output);
+            jump(rejoin);
+            bind(&notNaN);
+
+            loadConstantDouble(0.0, ScratchDoubleReg);
+            branchDouble(Assembler::DoubleLessThan, input, ScratchDoubleReg, rejoin);
+            sub64(Imm64(1), output);
+        }
+        jump(rejoin);
+        return;
+    }
+
     // Eagerly take care of NaNs.
     branchDouble(Assembler::DoubleUnordered, input, input, &traps.inputIsNaN);
 
     // Handle special values.
     if (isUnsigned) {
-        loadConstantDouble(-0.0, ScratchDoubleReg);
+        loadConstantDouble(0.0, ScratchDoubleReg);
         branchDouble(Assembler::DoubleGreaterThan, input, ScratchDoubleReg, &traps.intOverflow);
         loadConstantDouble(-1.0, ScratchDoubleReg);
         branchDouble(Assembler::DoubleLessThanOrEqual, input, ScratchDoubleReg, &traps.intOverflow);
         jump(rejoin);
         return;
     }
 
     // We've used vcvtsd2sq. The only legit value whose i64
     // truncation is INT64_MIN is double(INT64_MIN): exponent is so
     // high that the highest resolution around is much more than 1.
     loadConstantDouble(double(int64_t(INT64_MIN)), ScratchDoubleReg);
     branchDouble(Assembler::DoubleNotEqual, input, ScratchDoubleReg, &traps.intOverflow);
     jump(rejoin);
 }
 
 void
-MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, bool isUnsigned,
-                                             wasm::BytecodeOffset off, Label* rejoin)
+MacroAssembler::oolWasmTruncateCheckF32ToI64(FloatRegister input, Register64 output,
+                                             TruncFlags flags, wasm::BytecodeOffset off,
+                                             Label* rejoin)
 {
+    bool isUnsigned = flags & TRUNC_UNSIGNED;
+    bool isSaturating = flags & TRUNC_SATURATING;
+
     AutoHandleWasmTruncateToIntErrors traps(*this, off);
 
+    if (isSaturating) {
+        if (isUnsigned) {
+            // Negative overflow and NaN both are converted to 0, and the only other case
+            // is positive overflow which is converted to UINT64_MAX.
+            Label nonNegative;
+            loadConstantFloat32(0.0f, ScratchDoubleReg);
+            branchFloat(Assembler::DoubleGreaterThanOrEqual, input, ScratchDoubleReg, &nonNegative);
+            move64(Imm64(0), output);
+            jump(rejoin);
+            bind(&nonNegative);
+
+            move64(Imm64(UINT64_MAX), output);
+        } else {
+            // Negative overflow is already saturated to INT64_MIN, so we only have
+            // to handle NaN and positive overflow here.
+            Label notNaN;
+            branchFloat(Assembler::DoubleOrdered, input, input, &notNaN);
+            move64(Imm64(0), output);
+            jump(rejoin);
+            bind(&notNaN);
+
+            loadConstantFloat32(0.0f, ScratchFloat32Reg);
+            branchFloat(Assembler::DoubleLessThan, input, ScratchFloat32Reg, rejoin);
+            sub64(Imm64(1), output);
+        }
+        jump(rejoin);
+        return;
+    }
+
     // Eagerly take care of NaNs.
     branchFloat(Assembler::DoubleUnordered, input, input, &traps.inputIsNaN);
 
     // Handle special values.
     if (isUnsigned) {
-        loadConstantFloat32(-0.0f, ScratchFloat32Reg);
+        loadConstantFloat32(0.0f, ScratchFloat32Reg);
         branchFloat(Assembler::DoubleGreaterThan, input, ScratchFloat32Reg, &traps.intOverflow);
         loadConstantFloat32(-1.0f, ScratchFloat32Reg);
         branchFloat(Assembler::DoubleLessThanOrEqual, input, ScratchFloat32Reg, &traps.intOverflow);
         jump(rejoin);
         return;
     }
 
     // We've used vcvtss2sq. See comment in outOfLineWasmTruncateDoubleToInt64.
--- a/js/src/jit/x86/CodeGenerator-x86.cpp
+++ b/js/src/jit/x86/CodeGenerator-x86.cpp
@@ -1260,29 +1260,34 @@ CodeGeneratorX86::visitWasmTruncateToInt
 
     MWasmTruncateToInt64* mir = lir->mir();
     FloatRegister floatTemp = ToFloatRegister(lir->temp());
 
     Label fail, convert;
 
     MOZ_ASSERT (mir->input()->type() == MIRType::Double || mir->input()->type() == MIRType::Float32);
 
-    auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input);
+    auto* ool = new(alloc()) OutOfLineWasmTruncateCheck(mir, input, output);
     addOutOfLineCode(ool, mir);
 
+    bool isSaturating = mir->isSaturating();
     if (mir->input()->type() == MIRType::Float32) {
         if (mir->isUnsigned())
-            masm.wasmTruncateFloat32ToUInt64(input, output, ool->entry(), ool->rejoin(), floatTemp);
+            masm.wasmTruncateFloat32ToUInt64(input, output, isSaturating,
+                                             ool->entry(), ool->rejoin(), floatTemp);
         else
-            masm.wasmTruncateFloat32ToInt64(input, output, ool->entry(), ool->rejoin(), floatTemp);
+            masm.wasmTruncateFloat32ToInt64(input, output, isSaturating,
+                                            ool->entry(), ool->rejoin(), floatTemp);
     } else {
         if (mir->isUnsigned())
-            masm.wasmTruncateDoubleToUInt64(input, output, ool->entry(), ool->rejoin(), floatTemp);
+            masm.wasmTruncateDoubleToUInt64(input, output, isSaturating,
+                                            ool->entry(), ool->rejoin(), floatTemp);
         else
-            masm.wasmTruncateDoubleToInt64(input, output, ool->entry(), ool->rejoin(), floatTemp);
+            masm.wasmTruncateDoubleToInt64(input, output, isSaturating,
+                                           ool->entry(), ool->rejoin(), floatTemp);
     }
 }
 
 void
 CodeGeneratorX86::visitInt64ToFloatingPoint(LInt64ToFloatingPoint* lir)
 {
     Register64 input = ToRegister64(lir->getInt64Operand(0));
     FloatRegister output = ToFloatRegister(lir->output());
--- a/js/src/jit/x86/MacroAssembler-x86.cpp
+++ b/js/src/jit/x86/MacroAssembler-x86.cpp
@@ -980,52 +980,54 @@ MacroAssembler::atomicFetchOp64(const Sy
 void
 MacroAssembler::atomicFetchOp64(const Synchronization&, AtomicOp op, const Address& value,
                                 const BaseIndex& mem, Register64 temp, Register64 output)
 {
     AtomicFetchOp64(*this, op, value, mem, temp, output);
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateDoubleToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                           Label* oolEntry)
 {
     Label done;
     vcvttsd2si(input, output);
     branch32(Assembler::Condition::NotSigned, output, Imm32(0), &done);
 
     loadConstantDouble(double(int32_t(0x80000000)), ScratchDoubleReg);
     addDouble(input, ScratchDoubleReg);
     vcvttsd2si(ScratchDoubleReg, output);
 
     branch32(Assembler::Condition::Signed, output, Imm32(0), oolEntry);
     or32(Imm32(0x80000000), output);
 
     bind(&done);
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, Label* oolEntry)
+MacroAssembler::wasmTruncateFloat32ToUInt32(FloatRegister input, Register output, bool isSaturating,
+                                            Label* oolEntry)
 {
     Label done;
     vcvttss2si(input, output);
     branch32(Assembler::Condition::NotSigned, output, Imm32(0), &done);
 
     loadConstantFloat32(float(int32_t(0x80000000)), ScratchFloat32Reg);
     addFloat32(input, ScratchFloat32Reg);
     vcvttss2si(ScratchFloat32Reg, output);
 
     branch32(Assembler::Condition::Signed, output, Imm32(0), oolEntry);
     or32(Imm32(0x80000000), output);
 
     bind(&done);
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                          Label* oolRejoin, FloatRegister tempReg)
+MacroAssembler::wasmTruncateDoubleToInt64(FloatRegister input, Register64 output, bool isSaturating,
+                                          Label* oolEntry, Label* oolRejoin, FloatRegister tempReg)
 {
     Label fail, convert;
     Register temp = output.high;
 
     // Make sure input fits in (u)int64.
     reserveStack(2 * sizeof(int32_t));
     storeDouble(input, Operand(esp, 0));
     branchDoubleNotInInt64Range(Address(esp, 0), temp, &fail);
@@ -1044,18 +1046,19 @@ MacroAssembler::wasmTruncateDoubleToInt6
     truncateDoubleToInt64(Address(esp, 0), Address(esp, 0), temp);
 
     // Load value into int64 register.
     load64(Address(esp, 0), output);
     freeStack(2 * sizeof(int32_t));
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output, Label* oolEntry,
-                                           Label* oolRejoin, FloatRegister tempReg)
+MacroAssembler::wasmTruncateFloat32ToInt64(FloatRegister input, Register64 output,
+                                           bool isSaturating,
+                                           Label* oolEntry, Label* oolRejoin, FloatRegister tempReg)
 {
     Label fail, convert;
     Register temp = output.high;
 
     // Make sure input fits in (u)int64.
     reserveStack(2 * sizeof(int32_t));
     storeFloat32(input, Operand(esp, 0));
     branchFloat32NotInInt64Range(Address(esp, 0), temp, &fail);
@@ -1074,17 +1077,18 @@ MacroAssembler::wasmTruncateFloat32ToInt
     truncateFloat32ToInt64(Address(esp, 0), Address(esp, 0), temp);
 
     // Load value into int64 register.
     load64(Address(esp, 0), output);
     freeStack(2 * sizeof(int32_t));
 }
 
 void
-MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateDoubleToUInt64(FloatRegister input, Register64 output,
+                                           bool isSaturating, Label* oolEntry,
                                            Label* oolRejoin, FloatRegister tempReg)
 {
     Label fail, convert;
     Register temp = output.high;
 
     // Make sure input fits in (u)int64.
     reserveStack(2 * sizeof(int32_t));
     storeDouble(input, Operand(esp, 0));
@@ -1104,17 +1108,18 @@ MacroAssembler::wasmTruncateDoubleToUInt
     truncateDoubleToUInt64(Address(esp, 0), Address(esp, 0), temp, tempReg);
 
     // Load value into int64 register.
     load64(Address(esp, 0), output);
     freeStack(2 * sizeof(int32_t));
 }
 
 void
-MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output, Label* oolEntry,
+MacroAssembler::wasmTruncateFloat32ToUInt64(FloatRegister input, Register64 output,
+                                            bool isSaturating, Label* oolEntry,
                                             Label* oolRejoin, FloatRegister tempReg)
 {
     Label fail, convert;
     Register temp = output.high;
 
     // Make sure input fits in (u)int64.
     reserveStack(2 * sizeof(int32_t));
     storeFloat32(input, Operand(esp, 0));
--- a/js/src/moz.build
+++ b/js/src/moz.build
@@ -651,16 +651,17 @@ DIRS += [
     'build',
 ]
 
 FINAL_LIBRARY = 'js'
 
 if CONFIG['NIGHTLY_BUILD']:
     DEFINES['ENABLE_BINARYDATA'] = True
     DEFINES['ENABLE_SIMD'] = True
+    DEFINES['ENABLE_WASM_SATURATING_TRUNC_OPS'] = True
     DEFINES['ENABLE_WASM_SIGNEXTEND_OPS'] = True
     DEFINES['ENABLE_WASM_THREAD_OPS'] = True
     # An experiment we want to run on Nightly: Can we change the JS
     # representation of an exported global from the global's value
     # to an instance of WebAssembly.Global without breaking existing
     # wasm content?
     DEFINES['ENABLE_WASM_GLOBAL'] = True
 
--- a/js/src/wasm/WasmAST.h
+++ b/js/src/wasm/WasmAST.h
@@ -193,16 +193,17 @@ enum class AstExprKind
     BranchTable,
     Call,
     CallIndirect,
     ComparisonOperator,
     Const,
     ConversionOperator,
     CurrentMemory,
     Drop,
+    ExtraConversionOperator,
     First,
     GetGlobal,
     GetLocal,
     GrowMemory,
     If,
     Load,
     Nop,
     Pop,
@@ -1136,16 +1137,35 @@ class AstConversionOperator final : publ
       : AstExpr(Kind, ExprType::Limit),
         op_(op), operand_(operand)
     {}
 
     Op op() const { return op_; }
     AstExpr* operand() const { return operand_; }
 };
 
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+// Like AstConversionOperator, but for opcodes encoded with the Numeric prefix.
+class AstExtraConversionOperator final : public AstExpr
+{
+    NumericOp op_;
+    AstExpr* operand_;
+
+  public:
+    static const AstExprKind Kind = AstExprKind::ExtraConversionOperator;
+    explicit AstExtraConversionOperator(NumericOp op, AstExpr* operand)
+      : AstExpr(Kind, ExprType::Limit),
+        op_(op), operand_(operand)
+    {}
+
+    NumericOp op() const { return op_; }
+    AstExpr* operand() const { return operand_; }
+};
+#endif
+
 // This is an artificial AST node which can fill operand slots in an AST
 // constructed from parsing or decoding stack-machine code that doesn't have
 // an inherent AST structure.
 class AstPop final : public AstExpr
 {
   public:
     static const AstExprKind Kind = AstExprKind::Pop;
     AstPop()
--- a/js/src/wasm/WasmBaselineCompile.cpp
+++ b/js/src/wasm/WasmBaselineCompile.cpp
@@ -3575,125 +3575,142 @@ class BaseCompiler final : public BaseCo
 #else
         MOZ_CRASH("BaseCompiler platform hook: needPopcnt64Temp");
 #endif
     }
 
     class OutOfLineTruncateCheckF32OrF64ToI32 : public OutOfLineCode
     {
         AnyReg src;
-        bool isUnsigned;
+        RegI32 dest;
+        TruncFlags flags;
         BytecodeOffset off;
 
       public:
-        OutOfLineTruncateCheckF32OrF64ToI32(AnyReg src, bool isUnsigned, BytecodeOffset off)
+        OutOfLineTruncateCheckF32OrF64ToI32(AnyReg src, RegI32 dest, TruncFlags flags,
+                                            BytecodeOffset off)
           : src(src),
-            isUnsigned(isUnsigned),
+            dest(dest),
+            flags(flags),
             off(off)
         {}
 
         virtual void generate(MacroAssembler* masm) override {
             if (src.tag == AnyReg::F32)
-                masm->oolWasmTruncateCheckF32ToI32(src.f32(), isUnsigned, off, rejoin());
+                masm->oolWasmTruncateCheckF32ToI32(src.f32(), dest, flags, off, rejoin());
             else if (src.tag == AnyReg::F64)
-                masm->oolWasmTruncateCheckF64ToI32(src.f64(), isUnsigned, off, rejoin());
+                masm->oolWasmTruncateCheckF64ToI32(src.f64(), dest, flags, off, rejoin());
             else
                 MOZ_CRASH("unexpected type");
         }
     };
 
-    MOZ_MUST_USE bool truncateF32ToI32(RegF32 src, RegI32 dest, bool isUnsigned) {
+    MOZ_MUST_USE bool truncateF32ToI32(RegF32 src, RegI32 dest, TruncFlags flags) {
         BytecodeOffset off = bytecodeOffset();
         OutOfLineCode* ool =
             addOutOfLineCode(new(alloc_) OutOfLineTruncateCheckF32OrF64ToI32(AnyReg(src),
-                                                                             isUnsigned,
+                                                                             dest,
+                                                                             flags,
                                                                              off));
         if (!ool)
             return false;
-        if (isUnsigned)
-            masm.wasmTruncateFloat32ToUInt32(src, dest, ool->entry());
+        bool isSaturating = flags & TRUNC_SATURATING;
+        if (flags & TRUNC_UNSIGNED)
+            masm.wasmTruncateFloat32ToUInt32(src, dest, isSaturating, ool->entry());
         else
-            masm.wasmTruncateFloat32ToInt32(src, dest, ool->entry());
+            masm.wasmTruncateFloat32ToInt32(src, dest, isSaturating, ool->entry());
         masm.bind(ool->rejoin());
         return true;
     }
 
-    MOZ_MUST_USE bool truncateF64ToI32(RegF64 src, RegI32 dest, bool isUnsigned) {
+    MOZ_MUST_USE bool truncateF64ToI32(RegF64 src, RegI32 dest, TruncFlags flags) {
         BytecodeOffset off = bytecodeOffset();
         OutOfLineCode* ool =
             addOutOfLineCode(new(alloc_) OutOfLineTruncateCheckF32OrF64ToI32(AnyReg(src),
-                                                                             isUnsigned,
+                                                                             dest,
+                                                                             flags,
                                                                              off));
         if (!ool)
             return false;
-        if (isUnsigned)
-            masm.wasmTruncateDoubleToUInt32(src, dest, ool->entry());
+        bool isSaturating = flags & TRUNC_SATURATING;
+        if (flags & TRUNC_UNSIGNED)
+            masm.wasmTruncateDoubleToUInt32(src, dest, isSaturating, ool->entry());
         else
-            masm.wasmTruncateDoubleToInt32(src, dest, ool->entry());
+            masm.wasmTruncateDoubleToInt32(src, dest, isSaturating, ool->entry());
         masm.bind(ool->rejoin());
         return true;
     }
 
     class OutOfLineTruncateCheckF32OrF64ToI64 : public OutOfLineCode
     {
         AnyReg src;
-        bool isUnsigned;
+        RegI64 dest;
+        TruncFlags flags;
         BytecodeOffset off;
 
       public:
-        OutOfLineTruncateCheckF32OrF64ToI64(AnyReg src, bool isUnsigned, BytecodeOffset off)
+        OutOfLineTruncateCheckF32OrF64ToI64(AnyReg src, RegI64 dest, TruncFlags flags, BytecodeOffset off)
           : src(src),
-            isUnsigned(isUnsigned),
+            dest(dest),
+            flags(flags),
             off(off)
         {}
 
         virtual void generate(MacroAssembler* masm) override {
             if (src.tag == AnyReg::F32)
-                masm->oolWasmTruncateCheckF32ToI64(src.f32(), isUnsigned, off, rejoin());
+                masm->oolWasmTruncateCheckF32ToI64(src.f32(), dest, flags, off, rejoin());
             else if (src.tag == AnyReg::F64)
-                masm->oolWasmTruncateCheckF64ToI64(src.f64(), isUnsigned, off, rejoin());
+                masm->oolWasmTruncateCheckF64ToI64(src.f64(), dest, flags, off, rejoin());
             else
                 MOZ_CRASH("unexpected type");
         }
     };
 
 #ifndef RABALDR_FLOAT_TO_I64_CALLOUT
-    MOZ_MUST_USE RegF64 needTempForFloatingToI64(bool isUnsigned) {
+    MOZ_MUST_USE RegF64 needTempForFloatingToI64(TruncFlags flags) {
 # if defined(JS_CODEGEN_X86) || defined(JS_CODEGEN_X64)
-        if (isUnsigned)
+        if (flags & TRUNC_UNSIGNED)
             return needF64();
 # endif
         return RegF64::Invalid();
     }
 
-    MOZ_MUST_USE bool truncateF32ToI64(RegF32 src, RegI64 dest, bool isUnsigned, RegF64 temp) {
+    MOZ_MUST_USE bool truncateF32ToI64(RegF32 src, RegI64 dest, TruncFlags flags, RegF64 temp) {
         OutOfLineCode* ool = addOutOfLineCode(
             new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(AnyReg(src),
-                                                             isUnsigned,
+                                                             dest,
+                                                             flags,
                                                              bytecodeOffset()));
         if (!ool)
             return false;
-        if (isUnsigned)
-            masm.wasmTruncateFloat32ToUInt64(src, dest, ool->entry(), ool->rejoin(), temp);
+        bool isSaturating = flags & TRUNC_SATURATING;
+        if (flags & TRUNC_UNSIGNED)
+            masm.wasmTruncateFloat32ToUInt64(src, dest, isSaturating, ool->entry(),
+                                             ool->rejoin(), temp);
         else
-            masm.wasmTruncateFloat32ToInt64(src, dest, ool->entry(), ool->rejoin(), temp);
+            masm.wasmTruncateFloat32ToInt64(src, dest, isSaturating, ool->entry(),
+                                            ool->rejoin(), temp);
         return true;
     }
 
-    MOZ_MUST_USE bool truncateF64ToI64(RegF64 src, RegI64 dest, bool isUnsigned, RegF64 temp) {
+    MOZ_MUST_USE bool truncateF64ToI64(RegF64 src, RegI64 dest, TruncFlags flags, RegF64 temp) {
         OutOfLineCode* ool = addOutOfLineCode(
             new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(AnyReg(src),
-                                                             isUnsigned,
+                                                             dest,
+                                                             flags,
                                                              bytecodeOffset()));
         if (!ool)
             return false;
-        if (isUnsigned)
-            masm.wasmTruncateDoubleToUInt64(src, dest, ool->entry(), ool->rejoin(), temp);
+        bool isSaturating = flags & TRUNC_SATURATING;
+        if (flags & TRUNC_UNSIGNED)
+            masm.wasmTruncateDoubleToUInt64(src, dest, isSaturating, ool->entry(),
+                                            ool->rejoin(), temp);
         else
-            masm.wasmTruncateDoubleToInt64(src, dest, ool->entry(), ool->rejoin(), temp);
+            masm.wasmTruncateDoubleToInt64(src, dest, isSaturating, ool->entry(),
+                                           ool->rejoin(), temp);
         return true;
     }
 #endif // RABALDR_FLOAT_TO_I64_CALLOUT
 
 #ifndef RABALDR_I64_TO_FLOAT_CALLOUT
     RegI32 needConvertI64ToFloatTemp(ValType to, bool isUnsigned) {
         bool needs = false;
         if (to == ValType::F64) {
@@ -5082,24 +5099,24 @@ class BaseCompiler final : public BaseCo
     void emitPopcntI32();
     void emitPopcntI64();
     void emitAbsF32();
     void emitAbsF64();
     void emitNegateF32();
     void emitNegateF64();
     void emitSqrtF32();
     void emitSqrtF64();
-    template<bool isUnsigned> MOZ_MUST_USE bool emitTruncateF32ToI32();
-    template<bool isUnsigned> MOZ_MUST_USE bool emitTruncateF64ToI32();
+    template<TruncFlags flags> MOZ_MUST_USE bool emitTruncateF32ToI32();
+    template<TruncFlags flags> MOZ_MUST_USE bool emitTruncateF64ToI32();
 #ifdef RABALDR_FLOAT_TO_I64_CALLOUT
     MOZ_MUST_USE bool emitConvertFloatingToInt64Callout(SymbolicAddress callee, ValType operandType,
                                                         ValType resultType);
 #else
-    template<bool isUnsigned> MOZ_MUST_USE bool emitTruncateF32ToI64();
-    template<bool isUnsigned> MOZ_MUST_USE bool emitTruncateF64ToI64();
+    template<TruncFlags flags> MOZ_MUST_USE bool emitTruncateF32ToI64();
+    template<TruncFlags flags> MOZ_MUST_USE bool emitTruncateF64ToI64();
 #endif
     void emitWrapI64ToI32();
     void emitExtendI32_8();
     void emitExtendI32_16();
     void emitExtendI64_8();
     void emitExtendI64_16();
     void emitExtendI64_32();
     void emitExtendI32ToI64();
@@ -6067,66 +6084,66 @@ BaseCompiler::emitSqrtF32()
 void
 BaseCompiler::emitSqrtF64()
 {
     RegF64 r = popF64();
     masm.sqrtDouble(r, r);
     pushF64(r);
 }
 
-template<bool isUnsigned>
+template<TruncFlags flags>
 bool
 BaseCompiler::emitTruncateF32ToI32()
 {
     RegF32 rs = popF32();
     RegI32 rd = needI32();
-    if (!truncateF32ToI32(rs, rd, isUnsigned))
+    if (!truncateF32ToI32(rs, rd, flags))
         return false;
     freeF32(rs);
     pushI32(rd);
     return true;
 }
 
-template<bool isUnsigned>
+template<TruncFlags flags>
 bool
 BaseCompiler::emitTruncateF64ToI32()
 {
     RegF64 rs = popF64();
     RegI32 rd = needI32();
-    if (!truncateF64ToI32(rs, rd, isUnsigned))
+    if (!truncateF64ToI32(rs, rd, flags))
         return false;
     freeF64(rs);
     pushI32(rd);
     return true;
 }
 
 #ifndef RABALDR_FLOAT_TO_I64_CALLOUT
-template<bool isUnsigned>
+template<TruncFlags flags>
 bool
 BaseCompiler::emitTruncateF32ToI64()
 {
     RegF32 rs = popF32();
     RegI64 rd = needI64();
-    RegF64 temp = needTempForFloatingToI64(isUnsigned);
-    if (!truncateF32ToI64(rs, rd, isUnsigned, temp))
+    RegF64 temp = needTempForFloatingToI64(flags);
+    if (!truncateF32ToI64(rs, rd, flags, temp))
         return false;
     maybeFreeF64(temp);
     freeF32(rs);
     pushI64(rd);
     return true;
 }
 
-template<bool isUnsigned>
+template<TruncFlags flags>
 bool
 BaseCompiler::emitTruncateF64ToI64()
 {
     RegF64 rs = popF64();
     RegI64 rd = needI64();
-    RegF64 temp = needTempForFloatingToI64(isUnsigned);
-    if (!truncateF64ToI64(rs, rd, isUnsigned, temp))
+    RegF64 temp = needTempForFloatingToI64(flags);
+    if (!truncateF64ToI64(rs, rd, flags, temp))
         return false;
     maybeFreeF64(temp);
     freeF64(rs);
     pushI64(rd);
     return true;
 }
 #endif // RABALDR_FLOAT_TO_I64_CALLOUT
 
@@ -7279,28 +7296,39 @@ BaseCompiler::emitConvertFloatingToInt64
     masm.callWithABI(bytecodeOffset(), callee);
 
     freeF64(doubleInput);
 
     RegI64 rv = captureReturnedI64();
 
     RegF64 inputVal = popF64();
 
-    bool isUnsigned = callee == SymbolicAddress::TruncateDoubleToUint64;
-
-    // The OOL check just succeeds or fails, it does not generate a value.
-    OutOfLineCode* ool =
-        addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(AnyReg(inputVal),
-                                                                          isUnsigned,
-                                                                          bytecodeOffset()));
-    if (!ool)
-        return false;
-
-    masm.branch64(Assembler::Equal, rv, Imm64(0x8000000000000000), ool->entry());
-    masm.bind(ool->rejoin());
+    TruncFlags flags = 0;
+    if (callee == SymbolicAddress::TruncateDoubleToUint64)
+        flags |= TRUNC_UNSIGNED;
+    if (callee == SymbolicAddress::SaturatingTruncateDoubleToInt64 ||
+        callee == SymbolicAddress::SaturatingTruncateDoubleToUint64) {
+        flags |= TRUNC_SATURATING;
+    }
+
+    // If we're saturating, the callout will always produce the final result
+    // value. Otherwise, the callout value will return 0x8000000000000000
+    // and we need to produce traps.
+    OutOfLineCode* ool = nullptr;
+    if (!(flags & TRUNC_SATURATING)) {
+        // The OOL check just succeeds or fails, it does not generate a value.
+        ool = addOutOfLineCode(new (alloc_) OutOfLineTruncateCheckF32OrF64ToI64(AnyReg(inputVal),
+                                                                                rv, flags,
+                                                                                bytecodeOffset()));
+        if (!ool)
+            return false;
+
+        masm.branch64(Assembler::Equal, rv, Imm64(0x8000000000000000), ool->entry());
+        masm.bind(ool->rejoin());
+    }
 
     pushI64(rv);
     freeF64(inputVal);
 
     return true;
 }
 #endif // RABALDR_FLOAT_TO_I64_CALLOUT
 
@@ -8514,23 +8542,23 @@ BaseCompiler::emitBody()
             CHECK_NEXT(emitBinary(emitQuotientU32, ValType::I32));
           case uint16_t(Op::I32RemS):
             CHECK_NEXT(emitBinary(emitRemainderI32, ValType::I32));
           case uint16_t(Op::I32RemU):
             CHECK_NEXT(emitBinary(emitRemainderU32, ValType::I32));
           case uint16_t(Op::I32Eqz):
             CHECK_NEXT(emitConversion(emitEqzI32, ValType::I32, ValType::I32));
           case uint16_t(Op::I32TruncSF32):
-            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI32<false>, ValType::F32, ValType::I32));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI32<0>, ValType::F32, ValType::I32));
           case uint16_t(Op::I32TruncUF32):
-            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI32<true>, ValType::F32, ValType::I32));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI32<TRUNC_UNSIGNED>, ValType::F32, ValType::I32));
           case uint16_t(Op::I32TruncSF64):
-            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI32<false>, ValType::F64, ValType::I32));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI32<0>, ValType::F64, ValType::I32));
           case uint16_t(Op::I32TruncUF64):
-            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI32<true>, ValType::F64, ValType::I32));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI32<TRUNC_UNSIGNED>, ValType::F64, ValType::I32));
           case uint16_t(Op::I32WrapI64):
             CHECK_NEXT(emitConversion(emitWrapI64ToI32, ValType::I64, ValType::I32));
           case uint16_t(Op::I32ReinterpretF32):
             CHECK_NEXT(emitConversion(emitReinterpretF32AsI32, ValType::F32, ValType::I32));
           case uint16_t(Op::I32Clz):
             CHECK_NEXT(emitUnary(emitClzI32, ValType::I32));
           case uint16_t(Op::I32Ctz):
             CHECK_NEXT(emitUnary(emitCtzI32, ValType::I32));
@@ -8612,41 +8640,41 @@ BaseCompiler::emitBody()
             CHECK_NEXT(emitBinary(emitRemainderU64, ValType::I64));
 #endif
           case uint16_t(Op::I64TruncSF32):
 #ifdef RABALDR_FLOAT_TO_I64_CALLOUT
             CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
                                                 SymbolicAddress::TruncateDoubleToInt64,
                                                 ValType::F32, ValType::I64));
 #else
-            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI64<false>, ValType::F32, ValType::I64));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI64<0>, ValType::F32, ValType::I64));
 #endif
           case uint16_t(Op::I64TruncUF32):
 #ifdef RABALDR_FLOAT_TO_I64_CALLOUT
             CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
                                                 SymbolicAddress::TruncateDoubleToUint64,
                                                 ValType::F32, ValType::I64));
 #else
-            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI64<true>, ValType::F32, ValType::I64));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI64<TRUNC_UNSIGNED>, ValType::F32, ValType::I64));
 #endif
           case uint16_t(Op::I64TruncSF64):
 #ifdef RABALDR_FLOAT_TO_I64_CALLOUT
             CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
                                                 SymbolicAddress::TruncateDoubleToInt64,
                                                 ValType::F64, ValType::I64));
 #else
-            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI64<false>, ValType::F64, ValType::I64));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI64<0>, ValType::F64, ValType::I64));
 #endif
           case uint16_t(Op::I64TruncUF64):
 #ifdef RABALDR_FLOAT_TO_I64_CALLOUT
             CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
                                                 SymbolicAddress::TruncateDoubleToUint64,
                                                 ValType::F64, ValType::I64));
 #else
-            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI64<true>, ValType::F64, ValType::I64));
+            CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI64<TRUNC_UNSIGNED>, ValType::F64, ValType::I64));
 #endif
           case uint16_t(Op::I64ExtendSI32):
             CHECK_NEXT(emitConversion(emitExtendI32ToI64, ValType::I32, ValType::I64));
           case uint16_t(Op::I64ExtendUI32):
             CHECK_NEXT(emitConversion(emitExtendU32ToI64, ValType::I32, ValType::I64));
           case uint16_t(Op::I64ReinterpretF64):
             CHECK_NEXT(emitConversion(emitReinterpretF64AsI64, ValType::F64, ValType::I64));
           case uint16_t(Op::I64Or):
@@ -8907,16 +8935,77 @@ BaseCompiler::emitBody()
 #endif
 
           // Memory Related
           case uint16_t(Op::GrowMemory):
             CHECK_NEXT(emitGrowMemory());
           case uint16_t(Op::CurrentMemory):
             CHECK_NEXT(emitCurrentMemory());
 
+          // Numeric operations
+          case uint16_t(Op::NumericPrefix): {
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+            switch (op.b1) {
+              case uint16_t(NumericOp::I32TruncSSatF32):
+                CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI32<TRUNC_SATURATING>,
+                                             ValType::F32, ValType::I32));
+              case uint16_t(NumericOp::I32TruncUSatF32):
+                CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI32<TRUNC_UNSIGNED | TRUNC_SATURATING>,
+                                             ValType::F32, ValType::I32));
+              case uint16_t(NumericOp::I32TruncSSatF64):
+                CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI32<TRUNC_SATURATING>,
+                                             ValType::F64, ValType::I32));
+              case uint16_t(NumericOp::I32TruncUSatF64):
+                CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI32<TRUNC_UNSIGNED | TRUNC_SATURATING>,
+                                             ValType::F64, ValType::I32));
+              case uint16_t(NumericOp::I64TruncSSatF32):
+#ifdef RABALDR_FLOAT_TO_I64_CALLOUT
+                CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
+                                                    SymbolicAddress::SaturatingTruncateDoubleToInt64,
+                                                    ValType::F32, ValType::I64));
+#else
+                CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI64<TRUNC_SATURATING>,
+                                             ValType::F32, ValType::I64));
+#endif
+              case uint16_t(NumericOp::I64TruncUSatF32):
+#ifdef RABALDR_FLOAT_TO_I64_CALLOUT
+                CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
+                                                    SymbolicAddress::SaturatingTruncateDoubleToUint64,
+                                                    ValType::F32, ValType::I64));
+#else
+                CHECK_NEXT(emitConversionOOM(emitTruncateF32ToI64<TRUNC_UNSIGNED | TRUNC_SATURATING>,
+                                             ValType::F32, ValType::I64));
+#endif
+              case uint16_t(NumericOp::I64TruncSSatF64):
+#ifdef RABALDR_FLOAT_TO_I64_CALLOUT
+                CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
+                                                    SymbolicAddress::SaturatingTruncateDoubleToInt64,
+                                                    ValType::F64, ValType::I64));
+#else
+                CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI64<TRUNC_SATURATING>,
+                                             ValType::F64, ValType::I64));
+#endif
+              case uint16_t(NumericOp::I64TruncUSatF64):
+#ifdef RABALDR_FLOAT_TO_I64_CALLOUT
+                CHECK_NEXT(emitCalloutConversionOOM(emitConvertFloatingToInt64Callout,
+                                                    SymbolicAddress::SaturatingTruncateDoubleToUint64,
+                                                    ValType::F64, ValType::I64));
+#else
+                CHECK_NEXT(emitConversionOOM(emitTruncateF64ToI64<TRUNC_UNSIGNED | TRUNC_SATURATING>,
+                                             ValType::F64, ValType::I64));
+#endif
+              default:
+                return iter_.unrecognizedOpcode(&op);
+            }
+            break;
+#else
+            return iter_.unrecognizedOpcode(&op);
+#endif
+          }
+
           // Thread operations
           case uint16_t(Op::ThreadPrefix): {
 #ifdef ENABLE_WASM_THREAD_OPS
             switch (op.b1) {
               case uint16_t(ThreadOp::Wake):
                 CHECK_NEXT(emitWake());
 
               case uint16_t(ThreadOp::I32Wait):
--- a/js/src/wasm/WasmBinaryConstants.h
+++ b/js/src/wasm/WasmBinaryConstants.h
@@ -318,28 +318,46 @@ enum class Op
     // Sign extension
     I32Extend8S                          = 0xc0,
     I32Extend16S                         = 0xc1,
     I64Extend8S                          = 0xc2,
     I64Extend16S                         = 0xc3,
     I64Extend32S                         = 0xc4,
 #endif
 
+    FirstPrefix                          = 0xfc,
+    NumericPrefix                        = 0xfc,
     ThreadPrefix                         = 0xfe,
     MozPrefix                            = 0xff,
 
     Limit                                = 0x100
 };
 
 inline bool
 IsPrefixByte(uint8_t b)
 {
-    return b >= uint8_t(Op::ThreadPrefix);
+    return b >= uint8_t(Op::FirstPrefix);
 }
 
+// Opcodes in the "numeric" opcode space.
+enum class NumericOp
+{
+    // Saturating float-to-int conversions
+    I32TruncSSatF32                      = 0x00,
+    I32TruncUSatF32                      = 0x01,
+    I32TruncSSatF64                      = 0x02,
+    I32TruncUSatF64                      = 0x03,
+    I64TruncSSatF32                      = 0x04,
+    I64TruncUSatF32                      = 0x05,
+    I64TruncSSatF64                      = 0x06,
+    I64TruncUSatF64                      = 0x07,
+
+    Limit
+};
+
 // Opcodes from threads proposal as of June 30, 2017
 enum class ThreadOp
 {
     // Wait and wake
     Wake                                 = 0x00,
     I32Wait                              = 0x01,
     I64Wait                              = 0x02,
 
--- a/js/src/wasm/WasmBinaryIterator.cpp
+++ b/js/src/wasm/WasmBinaryIterator.cpp
@@ -235,16 +235,34 @@ wasm::Classify(OpBytes op)
       case Op::Else:
         return OpKind::Else;
       case Op::End:
         return OpKind::End;
       case Op::CurrentMemory:
         return OpKind::CurrentMemory;
       case Op::GrowMemory:
         return OpKind::GrowMemory;
+      case Op::NumericPrefix: {
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+          switch (NumericOp(op.b1)) {
+            case NumericOp::I32TruncSSatF32:
+            case NumericOp::I32TruncUSatF32:
+            case NumericOp::I32TruncSSatF64:
+            case NumericOp::I32TruncUSatF64:
+            case NumericOp::I64TruncSSatF32:
+            case NumericOp::I64TruncUSatF32:
+            case NumericOp::I64TruncSSatF64:
+            case NumericOp::I64TruncUSatF64:
+              return OpKind::Conversion;
+            default:
+              break;
+          }
+#endif
+          break;
+      }
       case Op::ThreadPrefix: {
 #ifdef ENABLE_WASM_THREAD_OPS
           switch (ThreadOp(op.b1)) {
             case ThreadOp::Limit:
               // Reject Limit for ThreadPrefix encoding
               break;
             case ThreadOp::Wake:
               return OpKind::Wake;
--- a/js/src/wasm/WasmBinaryToAST.cpp
+++ b/js/src/wasm/WasmBinaryToAST.cpp
@@ -700,16 +700,37 @@ AstDecodeConversion(AstDecodeContext& c,
         return false;
 
     if (!c.push(AstDecodeStackItem(conversion)))
         return false;
 
     return true;
 }
 
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+static bool
+AstDecodeExtraConversion(AstDecodeContext& c, ValType fromType, ValType toType, NumericOp op)
+{
+    if (!c.iter().readConversion(fromType, toType, nullptr))
+        return false;
+
+    AstDecodeStackItem operand = c.popCopy();
+
+    AstExtraConversionOperator* conversion =
+        new(c.lifo) AstExtraConversionOperator(op, operand.expr);
+    if (!conversion)
+        return false;
+
+    if (!c.push(AstDecodeStackItem(conversion)))
+        return false;
+
+    return true;
+}
+#endif
+
 static AstLoadStoreAddress
 AstDecodeLoadStoreAddress(const LinearMemoryAddress<Nothing>& addr, const AstDecodeStackItem& item)
 {
     uint32_t flags = FloorLog2(addr.align);
     return AstLoadStoreAddress(item.expr, flags, addr.offset);
 }
 
 static bool
@@ -1635,16 +1656,44 @@ AstDecodeExpr(AstDecodeContext& c)
         if (!c.iter().readUnreachable())
             return false;
         tmp = new(c.lifo) AstUnreachable();
         if (!tmp)
             return false;
         if (!c.push(AstDecodeStackItem(tmp)))
             return false;
         break;
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+      case uint16_t(Op::NumericPrefix):
+        switch (op.b1) {
+          case uint16_t(NumericOp::I32TruncSSatF32):
+          case uint16_t(NumericOp::I32TruncUSatF32):
+            if (!AstDecodeExtraConversion(c, ValType::F32, ValType::I32, NumericOp(op.b1)))
+                return false;
+            break;
+          case uint16_t(NumericOp::I32TruncSSatF64):
+          case uint16_t(NumericOp::I32TruncUSatF64):
+            if (!AstDecodeExtraConversion(c, ValType::F64, ValType::I32, NumericOp(op.b1)))
+                return false;
+            break;
+          case uint16_t(NumericOp::I64TruncSSatF32):
+          case uint16_t(NumericOp::I64TruncUSatF32):
+            if (!AstDecodeExtraConversion(c, ValType::F32, ValType::I64, NumericOp(op.b1)))
+                return false;
+            break;
+          case uint16_t(NumericOp::I64TruncSSatF64):
+          case uint16_t(NumericOp::I64TruncUSatF64):
+            if (!AstDecodeExtraConversion(c, ValType::F64, ValType::I64, NumericOp(op.b1)))
+                return false;
+            break;
+          default:
+            return c.iter().unrecognizedOpcode(&op);
+        }
+        break;
+#endif
       case uint16_t(Op::ThreadPrefix):
         switch (op.b1) {
           case uint16_t(ThreadOp::Wake):
             if (!AstDecodeWake(c))
                 return false;
             break;
           case uint16_t(ThreadOp::I32Wait):
           case uint16_t(ThreadOp::I64Wait):
--- a/js/src/wasm/WasmBinaryToText.cpp
+++ b/js/src/wasm/WasmBinaryToText.cpp
@@ -746,16 +746,43 @@ RenderConversionOperator(WasmRenderConte
       case Op::I32Eqz:            opStr = "i32.eqz"; break;
       case Op::I64Eqz:            opStr = "i64.eqz"; break;
       default:                      return Fail(c, "unexpected conversion operator");
     }
     return c.buffer.append(opStr, strlen(opStr));
 }
 
 static bool
+RenderExtraConversionOperator(WasmRenderContext& c, AstExtraConversionOperator& conv)
+{
+    if (!RenderExpr(c, *conv.operand()))
+        return false;
+
+    if (!RenderIndent(c))
+        return false;
+
+    MAP_AST_EXPR(c, conv);
+    const char* opStr;
+    switch (conv.op()) {
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+      case NumericOp::I32TruncSSatF32:   opStr = "i32.trunc_s:sat/f32"; break;
+      case NumericOp::I32TruncUSatF32:   opStr = "i32.trunc_u:sat/f32"; break;
+      case NumericOp::I32TruncSSatF64:   opStr = "i32.trunc_s:sat/f64"; break;
+      case NumericOp::I32TruncUSatF64:   opStr = "i32.trunc_u:sat/f64"; break;
+      case NumericOp::I64TruncSSatF32:   opStr = "i64.trunc_s:sat/f32"; break;
+      case NumericOp::I64TruncUSatF32:   opStr = "i64.trunc_u:sat/f32"; break;
+      case NumericOp::I64TruncSSatF64:   opStr = "i64.trunc_s:sat/f64"; break;
+      case NumericOp::I64TruncUSatF64:   opStr = "i64.trunc_u:sat/f64"; break;
+#endif
+      default:                      return Fail(c, "unexpected extra conversion operator");
+    }
+    return c.buffer.append(opStr, strlen(opStr));
+}
+
+static bool
 RenderIf(WasmRenderContext& c, AstIf& if_)
 {
     if (!RenderExpr(c, if_.cond()))
         return false;
 
     if (!RenderIndent(c))
         return false;
 
@@ -1322,16 +1349,22 @@ RenderExpr(WasmRenderContext& c, AstExpr
       case AstExprKind::ComparisonOperator:
         if (!RenderComparisonOperator(c, expr.as<AstComparisonOperator>()))
             return false;
         break;
       case AstExprKind::ConversionOperator:
         if (!RenderConversionOperator(c, expr.as<AstConversionOperator>()))
             return false;
         break;
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+      case AstExprKind::ExtraConversionOperator:
+        if (!RenderExtraConversionOperator(c, expr.as<AstExtraConversionOperator>()))
+            return false;
+        break;
+#endif
       case AstExprKind::Load:
         if (!RenderLoad(c, expr.as<AstLoad>()))
             return false;
         break;
       case AstExprKind::Store:
         if (!RenderStore(c, expr.as<AstStore>()))
             return false;
         break;
--- a/js/src/wasm/WasmBuiltins.cpp
+++ b/js/src/wasm/WasmBuiltins.cpp
@@ -436,16 +436,45 @@ TruncateDoubleToUint64(double input)
 {
     // Note: UINT64_MAX is not representable in double. It is actually UINT64_MAX + 1.
     // Therefore also sending the failure value.
     if (input >= double(UINT64_MAX) || input <= -1.0 || IsNaN(input))
         return 0x8000000000000000;
     return uint64_t(input);
 }
 
+static int64_t
+SaturatingTruncateDoubleToInt64(double input)
+{
+    // Handle in-range values (except INT64_MIN).
+    if (fabs(input) < -double(INT64_MIN))
+        return int64_t(input);
+    // Handle NaN.
+    if (IsNaN(input))
+        return 0;
+    // Handle positive overflow.
+    if (input > 0)
+        return INT64_MAX;
+    // Handle negative overflow.
+    return INT64_MIN;
+}
+
+static uint64_t
+SaturatingTruncateDoubleToUint64(double input)
+{
+    // Handle positive overflow.
+    if (input >= -double(INT64_MIN) * 2.0)
+        return UINT64_MAX;
+    // Handle in-range values.
+    if (input >= -1.0)
+        return uint64_t(input);
+    // Handle NaN and negative overflow.
+    return 0;
+}
+
 static double
 Int64ToDouble(int32_t x_hi, uint32_t x_lo)
 {
     int64_t x = int64_t((uint64_t(x_hi) << 32)) + int64_t(x_lo);
     return double(x);
 }
 
 static float
@@ -545,16 +574,22 @@ AddressOf(SymbolicAddress imm, ABIFuncti
         *abiType = Args_General4;
         return FuncCast(UModI64, *abiType);
       case SymbolicAddress::TruncateDoubleToUint64:
         *abiType = Args_Int64_Double;
         return FuncCast(TruncateDoubleToUint64, *abiType);
       case SymbolicAddress::TruncateDoubleToInt64:
         *abiType = Args_Int64_Double;
         return FuncCast(TruncateDoubleToInt64, *abiType);
+      case SymbolicAddress::SaturatingTruncateDoubleToUint64:
+        *abiType = Args_Int64_Double;
+        return FuncCast(SaturatingTruncateDoubleToUint64, *abiType);
+      case SymbolicAddress::SaturatingTruncateDoubleToInt64:
+        *abiType = Args_Int64_Double;
+        return FuncCast(SaturatingTruncateDoubleToInt64, *abiType);
       case SymbolicAddress::Uint64ToDouble:
         *abiType = Args_Double_IntInt;
         return FuncCast(Uint64ToDouble, *abiType);
       case SymbolicAddress::Uint64ToFloat32:
         *abiType = Args_Float32_IntInt;
         return FuncCast(Uint64ToFloat32, *abiType);
       case SymbolicAddress::Int64ToDouble:
         *abiType = Args_Double_IntInt;
@@ -678,16 +713,18 @@ wasm::NeedsBuiltinThunk(SymbolicAddress 
         return false;
       case SymbolicAddress::ToInt32:
       case SymbolicAddress::DivI64:
       case SymbolicAddress::UDivI64:
       case SymbolicAddress::ModI64:
       case SymbolicAddress::UModI64:
       case SymbolicAddress::TruncateDoubleToUint64:
       case SymbolicAddress::TruncateDoubleToInt64:
+      case SymbolicAddress::SaturatingTruncateDoubleToUint64:
+      case SymbolicAddress::SaturatingTruncateDoubleToInt64:
       case SymbolicAddress::Uint64ToDouble:
       case SymbolicAddress::Uint64ToFloat32:
       case SymbolicAddress::Int64ToDouble:
       case SymbolicAddress::Int64ToFloat32:
 #if defined(JS_CODEGEN_ARM)
       case SymbolicAddress::aeabi_idivmod:
       case SymbolicAddress::aeabi_uidivmod:
 #endif
--- a/js/src/wasm/WasmFrameIter.cpp
+++ b/js/src/wasm/WasmFrameIter.cpp
@@ -1113,16 +1113,20 @@ ThunkedNativeToDescription(SymbolicAddre
       case SymbolicAddress::ModI64:
         return "call to native i64.rem_s (in wasm)";
       case SymbolicAddress::UModI64:
         return "call to native i64.rem_u (in wasm)";
       case SymbolicAddress::TruncateDoubleToUint64:
         return "call to native i64.trunc_u/f64 (in wasm)";
       case SymbolicAddress::TruncateDoubleToInt64:
         return "call to native i64.trunc_s/f64 (in wasm)";
+      case SymbolicAddress::SaturatingTruncateDoubleToUint64:
+        return "call to native i64.trunc_u:sat/f64 (in wasm)";
+      case SymbolicAddress::SaturatingTruncateDoubleToInt64:
+        return "call to native i64.trunc_s:sat/f64 (in wasm)";
       case SymbolicAddress::Uint64ToDouble:
         return "call to native f64.convert_u/i64 (in wasm)";
       case SymbolicAddress::Uint64ToFloat32:
         return "call to native f32.convert_u/i64 (in wasm)";
       case SymbolicAddress::Int64ToDouble:
         return "call to native f64.convert_s/i64 (in wasm)";
       case SymbolicAddress::Int64ToFloat32:
         return "call to native f32.convert_s/i64 (in wasm)";
--- a/js/src/wasm/WasmIonCompile.cpp
+++ b/js/src/wasm/WasmIonCompile.cpp
@@ -720,21 +720,21 @@ class FunctionCompiler
         if (inDeadCode())
             return nullptr;
         auto* ins = MRotate::New(alloc(), input, count, type, left);
         curBlock_->add(ins);
         return ins;
     }
 
     template <class T>
-    MDefinition* truncate(MDefinition* op, bool isUnsigned)
+    MDefinition* truncate(MDefinition* op, TruncFlags flags)
     {
         if (inDeadCode())
             return nullptr;
-        auto* ins = T::New(alloc(), op, isUnsigned, bytecodeOffset());
+        auto* ins = T::New(alloc(), op, flags, bytecodeOffset());
         curBlock_->add(ins);
         return ins;
     }
 
     MDefinition* compare(MDefinition* lhs, MDefinition* rhs, JSOp op, MCompare::CompareType type)
     {
         if (inDeadCode())
             return nullptr;
@@ -2408,31 +2408,36 @@ EmitConversionWithType(FunctionCompiler&
         return false;
 
     f.iter().setResult(f.unary<MIRClass>(input, mirType));
     return true;
 }
 
 static bool
 EmitTruncate(FunctionCompiler& f, ValType operandType, ValType resultType,
-             bool isUnsigned)
+             bool isUnsigned, bool isSaturating)
 {
     MDefinition* input;
     if (!f.iter().readConversion(operandType, resultType, &input))
         return false;
 
+    TruncFlags flags = 0;
+    if (isUnsigned)
+        flags |= TRUNC_UNSIGNED;
+    if (isSaturating)
+        flags |= TRUNC_SATURATING;
     if (resultType == ValType::I32) {
         if (f.env().isAsmJS())
             f.iter().setResult(f.unary<MTruncateToInt32>(input));
         else
-            f.iter().setResult(f.truncate<MWasmTruncateToInt32>(input, isUnsigned));
+            f.iter().setResult(f.truncate<MWasmTruncateToInt32>(input, flags));
     } else {
         MOZ_ASSERT(resultType == ValType::I64);
         MOZ_ASSERT(!f.env().isAsmJS());
-        f.iter().setResult(f.truncate<MWasmTruncateToInt64>(input, isUnsigned));
+        f.iter().setResult(f.truncate<MWasmTruncateToInt64>(input, flags));
     }
     return true;
 }
 
 #ifdef ENABLE_WASM_SIGNEXTEND_OPS
 static bool
 EmitSignExtend(FunctionCompiler& f, uint32_t srcSize, uint32_t targetSize)
 {
@@ -3919,29 +3924,29 @@ EmitBodyExprs(FunctionCompiler& f)
           case uint16_t(Op::F64CopySign):
             CHECK(EmitCopySign(f, ValType::F64));
 
           // Conversions
           case uint16_t(Op::I32WrapI64):
             CHECK(EmitConversion<MWrapInt64ToInt32>(f, ValType::I64, ValType::I32));
           case uint16_t(Op::I32TruncSF32):
           case uint16_t(Op::I32TruncUF32):
-            CHECK(EmitTruncate(f, ValType::F32, ValType::I32, Op(op.b0) == Op::I32TruncUF32));
+            CHECK(EmitTruncate(f, ValType::F32, ValType::I32, Op(op.b0) == Op::I32TruncUF32, false));
           case uint16_t(Op::I32TruncSF64):
           case uint16_t(Op::I32TruncUF64):
-            CHECK(EmitTruncate(f, ValType::F64, ValType::I32, Op(op.b0) == Op::I32TruncUF64));
+            CHECK(EmitTruncate(f, ValType::F64, ValType::I32, Op(op.b0) == Op::I32TruncUF64, false));
           case uint16_t(Op::I64ExtendSI32):
           case uint16_t(Op::I64ExtendUI32):
             CHECK(EmitExtendI32(f, Op(op.b0) == Op::I64ExtendUI32));
           case uint16_t(Op::I64TruncSF32):
           case uint16_t(Op::I64TruncUF32):
-            CHECK(EmitTruncate(f, ValType::F32, ValType::I64, Op(op.b0) == Op::I64TruncUF32));
+            CHECK(EmitTruncate(f, ValType::F32, ValType::I64, Op(op.b0) == Op::I64TruncUF32, false));
           case uint16_t(Op::I64TruncSF64):
           case uint16_t(Op::I64TruncUF64):
-            CHECK(EmitTruncate(f, ValType::F64, ValType::I64, Op(op.b0) == Op::I64TruncUF64));
+            CHECK(EmitTruncate(f, ValType::F64, ValType::I64, Op(op.b0) == Op::I64TruncUF64, false));
           case uint16_t(Op::F32ConvertSI32):
             CHECK(EmitConversion<MToFloat32>(f, ValType::I32, ValType::F32));
           case uint16_t(Op::F32ConvertUI32):
             CHECK(EmitConversion<MWasmUnsignedToFloat32>(f, ValType::I32, ValType::F32));
           case uint16_t(Op::F32ConvertSI64):
           case uint16_t(Op::F32ConvertUI64):
             CHECK(EmitConvertI64ToFloatingPoint(f, ValType::F32, MIRType::Float32, Op(op.b0) == Op::F32ConvertUI64));
           case uint16_t(Op::F32DemoteF64):
@@ -3975,16 +3980,45 @@ EmitBodyExprs(FunctionCompiler& f)
           case uint16_t(Op::I64Extend8S):
             CHECK(EmitSignExtend(f, 1, 8));
           case uint16_t(Op::I64Extend16S):
             CHECK(EmitSignExtend(f, 2, 8));
           case uint16_t(Op::I64Extend32S):
             CHECK(EmitSignExtend(f, 4, 8));
 #endif
 
+          // Numeric operations
+          case uint16_t(Op::NumericPrefix): {
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+            switch (op.b1) {
+              case uint16_t(NumericOp::I32TruncSSatF32):
+              case uint16_t(NumericOp::I32TruncUSatF32):
+                CHECK(EmitTruncate(f, ValType::F32, ValType::I32,
+                                   NumericOp(op.b1) == NumericOp::I32TruncUSatF32, true));
+              case uint16_t(NumericOp::I32TruncSSatF64):
+              case uint16_t(NumericOp::I32TruncUSatF64):
+                CHECK(EmitTruncate(f, ValType::F64, ValType::I32,
+                                   NumericOp(op.b1) == NumericOp::I32TruncUSatF64, true));
+              case uint16_t(NumericOp::I64TruncSSatF32):
+              case uint16_t(NumericOp::I64TruncUSatF32):
+                CHECK(EmitTruncate(f, ValType::F32, ValType::I64,
+                                   NumericOp(op.b1) == NumericOp::I64TruncUSatF32, true));
+              case uint16_t(NumericOp::I64TruncSSatF64):
+              case uint16_t(NumericOp::I64TruncUSatF64):
+                CHECK(EmitTruncate(f, ValType::F64, ValType::I64,
+                                   NumericOp(op.b1) == NumericOp::I64TruncUSatF64, true));
+              default:
+                return f.iter().unrecognizedOpcode(&op);
+            }
+            break;
+#else
+            return f.iter().unrecognizedOpcode(&op);
+#endif
+          }
+
           // Thread operations
           case uint16_t(Op::ThreadPrefix): {
 #ifdef ENABLE_WASM_THREAD_OPS
             switch (op.b1) {
               case uint16_t(ThreadOp::Wake):
                 CHECK(EmitWake(f));
 
               case uint16_t(ThreadOp::I32Wait):
--- a/js/src/wasm/WasmTextToBinary.cpp
+++ b/js/src/wasm/WasmTextToBinary.cpp
@@ -87,16 +87,17 @@ class WasmToken
         Drop,
         Elem,
         Else,
         End,
         EndOfFile,
         Equal,
         Error,
         Export,
+        ExtraConversionOpcode,
         Float,
         Func,
         GetGlobal,
         GetLocal,
         Global,
         GrowMemory,
         If,
         Import,
@@ -141,16 +142,17 @@ class WasmToken
     const char16_t* end_;
     union {
         uint32_t index_;
         uint64_t uint_;
         int64_t sint_;
         FloatLiteralKind floatLiteralKind_;
         ValType valueType_;
         Op op_;
+        NumericOp numericOp_;
         ThreadOp threadOp_;
     } u;
   public:
     WasmToken()
       : kind_(Kind::Invalid),
         begin_(nullptr),
         end_(nullptr),
         u()
@@ -212,16 +214,25 @@ class WasmToken
         end_(end)
     {
         MOZ_ASSERT(begin != end);
         MOZ_ASSERT(kind_ == UnaryOpcode || kind_ == BinaryOpcode || kind_ == TernaryOpcode ||
                    kind_ == ComparisonOpcode || kind_ == ConversionOpcode ||
                    kind_ == Load || kind_ == Store);
         u.op_ = op;
     }
+    explicit WasmToken(Kind kind, NumericOp op, const char16_t* begin, const char16_t* end)
+      : kind_(kind),
+        begin_(begin),
+        end_(end)
+    {
+        MOZ_ASSERT(begin != end);
+        MOZ_ASSERT(kind_ == ExtraConversionOpcode);
+        u.numericOp_ = op;
+    }
     explicit WasmToken(Kind kind, ThreadOp op, const char16_t* begin, const char16_t* end)
       : kind_(kind),
         begin_(begin),
         end_(end)
     {
         MOZ_ASSERT(begin != end);
         MOZ_ASSERT(kind_ == AtomicCmpXchg || kind_ == AtomicLoad || kind_ == AtomicRMW ||
                    kind_ == AtomicStore || kind_ == Wait || kind_ == Wake);
@@ -273,16 +284,20 @@ class WasmToken
         return u.valueType_;
     }
     Op op() const {
         MOZ_ASSERT(kind_ == UnaryOpcode || kind_ == BinaryOpcode || kind_ == TernaryOpcode ||
                    kind_ == ComparisonOpcode || kind_ == ConversionOpcode ||
                    kind_ == Load || kind_ == Store);
         return u.op_;
     }
+    NumericOp numericOp() const {
+        MOZ_ASSERT(kind_ == ExtraConversionOpcode);
+        return u.numericOp_;
+    }
     ThreadOp threadOp() const {
         MOZ_ASSERT(kind_ == AtomicCmpXchg || kind_ == AtomicLoad || kind_ == AtomicRMW ||
                    kind_ == AtomicStore || kind_ == Wait || kind_ == Wake);
         return u.threadOp_;
     }
     bool isOpcode() const {
         switch (kind_) {
           case AtomicCmpXchg:
@@ -294,16 +309,19 @@ class WasmToken
           case Br:
           case BrIf:
           case BrTable:
           case Call:
           case CallIndirect:
           case ComparisonOpcode:
           case Const:
           case ConversionOpcode:
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+          case ExtraConversionOpcode:
+#endif
           case CurrentMemory:
           case Drop:
           case GetGlobal:
           case GetLocal:
           case GrowMemory:
           case If:
           case Load:
           case Loop:
@@ -1317,16 +1335,30 @@ WasmTokenStream::next()
                     return WasmToken(WasmToken::ConversionOpcode, Op::I32TruncSF64,
                                      begin, cur_);
                 if (consume(u"trunc_u/f32"))
                     return WasmToken(WasmToken::ConversionOpcode, Op::I32TruncUF32,
                                      begin, cur_);
                 if (consume(u"trunc_u/f64"))
                     return WasmToken(WasmToken::ConversionOpcode, Op::I32TruncUF64,
                                      begin, cur_);
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+                if (consume(u"trunc_s:sat/f32"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I32TruncSSatF32,
+                                     begin, cur_);
+                if (consume(u"trunc_s:sat/f64"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I32TruncSSatF64,
+                                     begin, cur_);
+                if (consume(u"trunc_u:sat/f32"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I32TruncUSatF32,
+                                     begin, cur_);
+                if (consume(u"trunc_u:sat/f64"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I32TruncUSatF64,
+                                     begin, cur_);
+#endif
                 break;
               case 'w':
                 if (consume(u"wrap/i64"))
                     return WasmToken(WasmToken::ConversionOpcode, Op::I32WrapI64,
                                      begin, cur_);
                 break;
               case 'x':
                 if (consume(u"xor"))
@@ -1553,16 +1585,30 @@ WasmTokenStream::next()
                     return WasmToken(WasmToken::ConversionOpcode, Op::I64TruncSF64,
                                      begin, cur_);
                 if (consume(u"trunc_u/f32"))
                     return WasmToken(WasmToken::ConversionOpcode, Op::I64TruncUF32,
                                      begin, cur_);
                 if (consume(u"trunc_u/f64"))
                     return WasmToken(WasmToken::ConversionOpcode, Op::I64TruncUF64,
                                      begin, cur_);
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+                if (consume(u"trunc_s:sat/f32"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I64TruncSSatF32,
+                                     begin, cur_);
+                if (consume(u"trunc_s:sat/f64"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I64TruncSSatF64,
+                                     begin, cur_);
+                if (consume(u"trunc_u:sat/f32"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I64TruncUSatF32,
+                                     begin, cur_);
+                if (consume(u"trunc_u:sat/f64"))
+                    return WasmToken(WasmToken::ExtraConversionOpcode, NumericOp::I64TruncUSatF64,
+                                     begin, cur_);
+#endif
                 break;
               case 'w':
                 break;
               case 'x':
                 if (consume(u"xor"))
                     return WasmToken(WasmToken::BinaryOpcode, Op::I64Xor, begin, cur_);
                 break;
             }
@@ -2365,16 +2411,28 @@ ParseConversionOperator(WasmParseContext
 {
     AstExpr* operand = ParseExpr(c, inParens);
     if (!operand)
         return nullptr;
 
     return new(c.lifo) AstConversionOperator(op, operand);
 }
 
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+static AstExtraConversionOperator*
+ParseExtraConversionOperator(WasmParseContext& c, NumericOp op, bool inParens)
+{
+    AstExpr* operand = ParseExpr(c, inParens);
+    if (!operand)
+        return nullptr;
+
+    return new(c.lifo) AstExtraConversionOperator(op, operand);
+}
+#endif
+
 static AstDrop*
 ParseDrop(WasmParseContext& c, bool inParens)
 {
     AstExpr* value = ParseExpr(c, inParens);
     if (!value)
         return nullptr;
 
     return new(c.lifo) AstDrop(*value);
@@ -2904,16 +2962,20 @@ ParseExprBody(WasmParseContext& c, WasmT
       case WasmToken::CallIndirect:
         return ParseCallIndirect(c, inParens);
       case WasmToken::ComparisonOpcode:
         return ParseComparisonOperator(c, token.op(), inParens);
       case WasmToken::Const:
         return ParseConst(c, token);
       case WasmToken::ConversionOpcode:
         return ParseConversionOperator(c, token.op(), inParens);
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+      case WasmToken::ExtraConversionOpcode:
+        return ParseExtraConversionOperator(c, token.numericOp(), inParens);
+#endif
       case WasmToken::Drop:
         return ParseDrop(c, inParens);
       case WasmToken::If:
         return ParseIf(c, inParens);
       case WasmToken::GetGlobal:
         return ParseGetGlobal(c);
       case WasmToken::GetLocal:
         return ParseGetLocal(c);
@@ -4135,16 +4197,24 @@ ResolveComparisonOperator(Resolver& r, A
 }
 
 static bool
 ResolveConversionOperator(Resolver& r, AstConversionOperator& b)
 {
     return ResolveExpr(r, *b.operand());
 }
 
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+static bool
+ResolveExtraConversionOperator(Resolver& r, AstExtraConversionOperator& b)
+{
+    return ResolveExpr(r, *b.operand());
+}
+#endif
+
 static bool
 ResolveIfElse(Resolver& r, AstIf& i)
 {
     if (!ResolveExpr(r, i.cond()))
         return false;
     if (!r.pushTarget(i.name()))
         return false;
     if (!ResolveExprList(r, i.thenExprs()))
@@ -4264,16 +4334,20 @@ ResolveExpr(Resolver& r, AstExpr& expr)
       case AstExprKind::CallIndirect:
         return ResolveCallIndirect(r, expr.as<AstCallIndirect>());
       case AstExprKind::ComparisonOperator:
         return ResolveComparisonOperator(r, expr.as<AstComparisonOperator>());
       case AstExprKind::Const:
         return true;
       case AstExprKind::ConversionOperator:
         return ResolveConversionOperator(r, expr.as<AstConversionOperator>());
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+      case AstExprKind::ExtraConversionOperator:
+        return ResolveExtraConversionOperator(r, expr.as<AstExtraConversionOperator>());
+#endif
       case AstExprKind::First:
         return ResolveFirst(r, expr.as<AstFirst>());
       case AstExprKind::GetGlobal:
         return ResolveGetGlobal(r, expr.as<AstGetGlobal>());
       case AstExprKind::GetLocal:
         return ResolveGetLocal(r, expr.as<AstGetLocal>());
       case AstExprKind::If:
         return ResolveIfElse(r, expr.as<AstIf>());
@@ -4660,16 +4734,25 @@ EncodeComparisonOperator(Encoder& e, Ast
 
 static bool
 EncodeConversionOperator(Encoder& e, AstConversionOperator& b)
 {
     return EncodeExpr(e, *b.operand()) &&
            e.writeOp(b.op());
 }
 
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+static bool
+EncodeExtraConversionOperator(Encoder& e, AstExtraConversionOperator& b)
+{
+    return EncodeExpr(e, *b.operand()) &&
+           e.writeOp(b.op());
+}
+#endif
+
 static bool
 EncodeIf(Encoder& e, AstIf& i)
 {
     if (!EncodeExpr(e, i.cond()) || !e.writeOp(Op::If))
         return false;
 
     if (!e.writeBlockType(i.type()))
         return false;
@@ -4864,16 +4947,20 @@ EncodeExpr(Encoder& e, AstExpr& expr)
       case AstExprKind::ComparisonOperator:
         return EncodeComparisonOperator(e, expr.as<AstComparisonOperator>());
       case AstExprKind::Const:
         return EncodeConst(e, expr.as<AstConst>());
       case AstExprKind::ConversionOperator:
         return EncodeConversionOperator(e, expr.as<AstConversionOperator>());
       case AstExprKind::Drop:
         return EncodeDrop(e, expr.as<AstDrop>());
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+      case AstExprKind::ExtraConversionOperator:
+        return EncodeExtraConversionOperator(e, expr.as<AstExtraConversionOperator>());
+#endif
       case AstExprKind::First:
         return EncodeFirst(e, expr.as<AstFirst>());
       case AstExprKind::GetLocal:
         return EncodeGetLocal(e, expr.as<AstGetLocal>());
       case AstExprKind::GetGlobal:
         return EncodeGetGlobal(e, expr.as<AstGetGlobal>());
       case AstExprKind::If:
         return EncodeIf(e, expr.as<AstIf>());
--- a/js/src/wasm/WasmTypes.h
+++ b/js/src/wasm/WasmTypes.h
@@ -1389,16 +1389,18 @@ enum class SymbolicAddress
     CoerceInPlace_ToNumber,
     CoerceInPlace_JitEntry,
     DivI64,
     UDivI64,
     ModI64,
     UModI64,
     TruncateDoubleToInt64,
     TruncateDoubleToUint64,
+    SaturatingTruncateDoubleToInt64,
+    SaturatingTruncateDoubleToUint64,
     Uint64ToFloat32,
     Uint64ToDouble,
     Int64ToFloat32,
     Int64ToDouble,
     GrowMemory,
     CurrentMemory,
     WaitI32,
     WaitI64,
--- a/js/src/wasm/WasmValidate.cpp
+++ b/js/src/wasm/WasmValidate.cpp
@@ -715,16 +715,39 @@ DecodeFunctionBodyExprs(const ModuleEnvi
             uint32_t unusedDefault;
             ExprType unusedType;
             CHECK(iter.readBrTable(&unusedDepths, &unusedDefault, &unusedType, &nothing, &nothing));
           }
           case uint16_t(Op::Return):
             CHECK(iter.readReturn(&nothing));
           case uint16_t(Op::Unreachable):
             CHECK(iter.readUnreachable());
+          case uint16_t(Op::NumericPrefix): {
+#ifdef ENABLE_WASM_SATURATING_TRUNC_OPS
+            switch (op.b1) {
+              case uint16_t(NumericOp::I32TruncSSatF32):
+              case uint16_t(NumericOp::I32TruncUSatF32):
+                CHECK(iter.readConversion(ValType::F32, ValType::I32, &nothing));
+              case uint16_t(NumericOp::I32TruncSSatF64):
+              case uint16_t(NumericOp::I32TruncUSatF64):
+                CHECK(iter.readConversion(ValType::F64, ValType::I32, &nothing));
+              case uint16_t(NumericOp::I64TruncSSatF32):
+              case uint16_t(NumericOp::I64TruncUSatF32):
+                CHECK(iter.readConversion(ValType::F32, ValType::I64, &nothing));
+              case uint16_t(NumericOp::I64TruncSSatF64):
+              case uint16_t(NumericOp::I64TruncUSatF64):
+                CHECK(iter.readConversion(ValType::F64, ValType::I64, &nothing));
+              default:
+                return iter.unrecognizedOpcode(&op);
+            }
+            break;
+#else
+            return iter.unrecognizedOpcode(&op);
+#endif
+          }
           case uint16_t(Op::ThreadPrefix): {
 #ifdef ENABLE_WASM_THREAD_OPS
             switch (op.b1) {
               case uint16_t(ThreadOp::Wake): {
                 LinearMemoryAddress<Nothing> addr;
                 CHECK(iter.readWake(&addr, &nothing));
               }
               case uint16_t(ThreadOp::I32Wait): {
--- a/js/src/wasm/WasmValidate.h
+++ b/js/src/wasm/WasmValidate.h
@@ -279,16 +279,22 @@ class Encoder
         return writeFixedU8(uint8_t(op));
     }
     MOZ_MUST_USE bool writeOp(MozOp op) {
         static_assert(size_t(MozOp::Limit) <= 256, "fits");
         MOZ_ASSERT(size_t(op) < size_t(MozOp::Limit));
         return writeFixedU8(uint8_t(Op::MozPrefix)) &&
                writeFixedU8(uint8_t(op));
     }
+    MOZ_MUST_USE bool writeOp(NumericOp op) {
+        static_assert(size_t(NumericOp::Limit) <= 256, "fits");
+        MOZ_ASSERT(size_t(op) < size_t(NumericOp::Limit));
+        return writeFixedU8(uint8_t(Op::NumericPrefix)) &&
+               writeFixedU8(uint8_t(op));
+    }
     MOZ_MUST_USE bool writeOp(ThreadOp op) {
         static_assert(size_t(ThreadOp::Limit) <= 256, "fits");
         MOZ_ASSERT(size_t(op) < size_t(ThreadOp::Limit));
         return writeFixedU8(uint8_t(Op::ThreadPrefix)) &&
                writeFixedU8(uint8_t(op));
     }
 
     // Fixed-length encodings that allow back-patching.