Repository: tetratelabs/wazero
Pull request #1685 (2023-09-04 23:32:27)

wazevo: add nontrapping fp conversions (wasm v2)
moves forward #1496 with support for non-trapping floating point conversions to integer.

This also changes the signature of `lowerOpcode(op)` to `lowerCurrentOpcode()`. In order to parse `wasm.OpcodeMisc*TruncSat*` etc. it is necessary to parse `wasm.OpcodeMiscPrefix` first, and then advance to the next opcode, which will be in the range `[0x00..0x07]` They cannot be parsed otherwise, because then these opcodes would be mismatched in the `switch` statement for other unrelated opcodes.

Now `lowerCurrentOpcode()` instead moves `pc`itself; for this reason, all the related logic such as the debugging statements are now moved to this procedure, and  `return` statements had had to be changed to `break`s, otherwise the final `pc++` would be skipped in many cases.



Patch:
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
index ed4a82e2..2cba1ee1 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr.go
@@ -205,20 +205,22 @@ func (m *machine) LowerInstr(instr *ssa.Instruction) {
 		x := instr.Arg()
 		result := instr.Return()
 		m.lowerPopcnt(x, result)
-	case ssa.OpcodeFcvtToSint:
+	case ssa.OpcodeFcvtToSint, ssa.OpcodeFcvtToSintSat:
 		x, ctx := instr.Arg2()
 		result := instr.Return()
 		rn := m.getOperand_NR(m.compiler.ValueDefinition(x), extModeNone)
 		rd := operandNR(m.compiler.VRegOf(result))
 		ctxVReg := m.compiler.VRegOf(ctx)
-		m.lowerFpuToInt(rd, rn, ctxVReg, true, x.Type() == ssa.TypeF64, result.Type().Bits() == 64)
-	case ssa.OpcodeFcvtToUint:
+		m.lowerFpuToInt(rd, rn, ctxVReg, true, x.Type() == ssa.TypeF64,
+			result.Type().Bits() == 64, op == ssa.OpcodeFcvtToSintSat)
+	case ssa.OpcodeFcvtToUint, ssa.OpcodeFcvtToUintSat:
 		x, ctx := instr.Arg2()
 		result := instr.Return()
 		rn := m.getOperand_NR(m.compiler.ValueDefinition(x), extModeNone)
 		rd := operandNR(m.compiler.VRegOf(result))
 		ctxVReg := m.compiler.VRegOf(ctx)
-		m.lowerFpuToInt(rd, rn, ctxVReg, false, x.Type() == ssa.TypeF64, result.Type().Bits() == 64)
+		m.lowerFpuToInt(rd, rn, ctxVReg, false, x.Type() == ssa.TypeF64,
+			result.Type().Bits() == 64, op == ssa.OpcodeFcvtToUintSat)
 	case ssa.OpcodeFcvtFromSint:
 		x := instr.Arg()
 		result := instr.Return()
@@ -427,44 +429,48 @@ func (m *machine) lowerFpuUniOp(op fpuUniOp, in, out ssa.Value) {
 	m.insert(neg)
 }
 
-func (m *machine) lowerFpuToInt(rd, rn operand, ctx regalloc.VReg, signed, src64bit, dst64bit bool) {
-	// First of all, we have to clear the FPU flags.
-	flagClear := m.allocateInstr()
-	flagClear.asMovToFPSR(xzrVReg)
-	m.insert(flagClear)
+func (m *machine) lowerFpuToInt(rd, rn operand, ctx regalloc.VReg, signed, src64bit, dst64bit, nonTrapping bool) {
+	if !nonTrapping {
+		// First of all, we have to clear the FPU flags.
+		flagClear := m.allocateInstr()
+		flagClear.asMovToFPSR(xzrVReg)
+		m.insert(flagClear)
+	}
 
 	// Then, do the conversion which doesn't trap inherently.
 	cvt := m.allocateInstr()
 	cvt.asFpuToInt(rd, rn, signed, src64bit, dst64bit)
 	m.insert(cvt)
 
-	// After the conversion, check the FPU flags.
-	getFlag := m.allocateInstr()
-	getFlag.asMovFromFPSR(tmpRegVReg)
-	m.insert(getFlag)
+	if !nonTrapping {
+		// After the conversion, check the FPU flags.
+		getFlag := m.allocateInstr()
+		getFlag.asMovFromFPSR(tmpRegVReg)
+		m.insert(getFlag)
 
-	// Check if the conversion was undefined by comparing the status with 1.
-	// See https://developer.arm.com/documentation/ddi0595/2020-12/AArch64-Registers/FPSR--Floating-point-Status-Register
-	alu := m.allocateInstr()
-	alu.asALU(aluOpSubS, operandNR(xzrVReg), operandNR(tmpRegVReg), operandImm12(1, 0), true)
-	m.insert(alu)
+		// Check if the conversion was undefined by comparing the status with 1.
+		// See https://developer.arm.com/documentation/ddi0595/2020-12/AArch64-Registers/FPSR--Floating-point-Status-Register
+		alu := m.allocateInstr()
+		alu.asALU(aluOpSubS, operandNR(xzrVReg), operandNR(tmpRegVReg), operandImm12(1, 0), true)
+		m.insert(alu)
 
-	// If it is not undefined, we can return the result.
-	ok := m.allocateInstr()
-	ok.asCondBr(ne.asCond(), invalidLabel, false /* ignored */)
-	ok.condBrOffsetResolve(4 /* fpuCmp */ + exitIfNotSequenceEncodingSize + exitWithCodeEncodingSize + 4)
-	m.insert(ok)
-
-	// Otherwise, we have to choose the status depending on it is overflow or NaN conversion.
-
-	// Comparing itself to check if it is a NaN.
-	fpuCmp := m.allocateInstr()
-	fpuCmp.asFpuCmp(rn, rn, src64bit)
-	m.insert(fpuCmp)
-	// If the VC flag is not set (== VS flag is set), it is a NaN.
-	m.exitIfNot(ctx, vc.asCond(), wazevoapi.ExitCodeInvalidConversionToInteger)
-	// Otherwise, it is an overflow.
-	m.lowerExitWithCode(ctx, wazevoapi.ExitCodeIntegerOverflow)
+		// If it is not undefined, we can return the result.
+		ok := m.allocateInstr()
+		ok.asCondBr(ne.asCond(), invalidLabel, false /* ignored */)
+		ok.condBrOffsetResolve(4 /* fpuCmp */ + exitIfNotSequenceEncodingSize + exitWithCodeEncodingSize + 4)
+		m.insert(ok)
+
+		// Otherwise, we have to choose the status depending on it is overflow or NaN conversion.
+
+		// Comparing itself to check if it is a NaN.
+		fpuCmp := m.allocateInstr()
+		fpuCmp.asFpuCmp(rn, rn, src64bit)
+		m.insert(fpuCmp)
+		// If the VC flag is not set (== VS flag is set), it is a NaN.
+		m.exitIfNot(ctx, vc.asCond(), wazevoapi.ExitCodeInvalidConversionToInteger)
+		// Otherwise, it is an overflow.
+		m.lowerExitWithCode(ctx, wazevoapi.ExitCodeIntegerOverflow)
+	}
 }
 
 func (m *machine) lowerIntToFpu(rd, rn operand, signed, src64bit, dst64bit bool) {
diff --git a/internal/engine/wazevo/frontend/lower.go b/internal/engine/wazevo/frontend/lower.go
index 90726cf2..748bf2ba 100644
--- a/internal/engine/wazevo/frontend/lower.go
+++ b/internal/engine/wazevo/frontend/lower.go
@@ -4,6 +4,7 @@ import (
 	"encoding/binary"
 	"fmt"
 	"math"
+	"strconv"
 	"strings"
 
 	"github.com/tetratelabs/wazero/api"
@@ -138,15 +139,7 @@ func (c *Compiler) lowerBody(entryBlk ssa.BasicBlock) {
 	})
 
 	for c.loweringState.pc < len(c.wasmFunctionBody) {
-		op := c.wasmFunctionBody[c.loweringState.pc]
-		c.lowerOpcode(op)
-		if wazevoapi.FrontEndLoggingEnabled {
-			fmt.Println("--------- Translated " + wasm.InstructionName(op) + " --------")
-			fmt.Println("state: " + c.loweringState.String())
-			fmt.Println(c.formatBuilder())
-			fmt.Println("--------------------------")
-		}
-		c.loweringState.pc++
+		c.lowerCurrentOpcode()
 	}
 }
 
@@ -154,14 +147,16 @@ func (c *Compiler) state() *loweringState {
 	return &c.loweringState
 }
 
-func (c *Compiler) lowerOpcode(op wasm.Opcode) {
+func (c *Compiler) lowerCurrentOpcode() {
+	op := c.wasmFunctionBody[c.loweringState.pc]
+
 	builder := c.ssaBuilder
 	state := c.state()
 	switch op {
 	case wasm.OpcodeI32Const:
 		c := c.readI32s()
 		if state.unreachable {
-			return
+			break
 		}
 
 		iconst := builder.AllocateInstruction().AsIconst32(uint32(c)).Insert(builder)
@@ -170,7 +165,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeI64Const:
 		c := c.readI64s()
 		if state.unreachable {
-			return
+			break
 		}
 		iconst := builder.AllocateInstruction().AsIconst64(uint64(c)).Insert(builder)
 		value := iconst.Return()
@@ -178,7 +173,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeF32Const:
 		f32 := c.readF32()
 		if state.unreachable {
-			return
+			break
 		}
 		f32const := builder.AllocateInstruction().
 			AsF32const(f32).
@@ -188,7 +183,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeF64Const:
 		f64 := c.readF64()
 		if state.unreachable {
-			return
+			break
 		}
 		f64const := builder.AllocateInstruction().
 			AsF64const(f64).
@@ -197,7 +192,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(f64const)
 	case wasm.OpcodeI32Add, wasm.OpcodeI64Add:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		iadd := builder.AllocateInstruction()
@@ -207,7 +202,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Sub, wasm.OpcodeI64Sub:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		isub := builder.AllocateInstruction()
@@ -217,7 +212,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeF32Add, wasm.OpcodeF64Add:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		iadd := builder.AllocateInstruction()
@@ -227,7 +222,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Mul, wasm.OpcodeI64Mul:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		imul := builder.AllocateInstruction()
@@ -237,7 +232,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeF32Sub, wasm.OpcodeF64Sub:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		isub := builder.AllocateInstruction()
@@ -247,7 +242,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeF32Mul, wasm.OpcodeF64Mul:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		isub := builder.AllocateInstruction()
@@ -257,7 +252,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeF32Div, wasm.OpcodeF64Div:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		isub := builder.AllocateInstruction()
@@ -267,7 +262,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeF32Max, wasm.OpcodeF64Max:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		isub := builder.AllocateInstruction()
@@ -277,7 +272,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeF32Min, wasm.OpcodeF64Min:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		isub := builder.AllocateInstruction()
@@ -287,37 +282,37 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI64Extend8S:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIntegerExtend(true, 8, 64)
 	case wasm.OpcodeI64Extend16S:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIntegerExtend(true, 16, 64)
 	case wasm.OpcodeI64Extend32S, wasm.OpcodeI64ExtendI32S:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIntegerExtend(true, 32, 64)
 	case wasm.OpcodeI64ExtendI32U:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIntegerExtend(false, 32, 64)
 	case wasm.OpcodeI32Extend8S:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIntegerExtend(true, 8, 32)
 	case wasm.OpcodeI32Extend16S:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIntegerExtend(true, 16, 32)
 	case wasm.OpcodeI32Eqz, wasm.OpcodeI64Eqz:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		zero := builder.AllocateInstruction()
@@ -334,109 +329,109 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(icmp)
 	case wasm.OpcodeI32Eq, wasm.OpcodeI64Eq:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondEqual)
 	case wasm.OpcodeI32Ne, wasm.OpcodeI64Ne:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondNotEqual)
 	case wasm.OpcodeI32LtS, wasm.OpcodeI64LtS:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondSignedLessThan)
 	case wasm.OpcodeI32LtU, wasm.OpcodeI64LtU:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondUnsignedLessThan)
 	case wasm.OpcodeI32GtS, wasm.OpcodeI64GtS:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondSignedGreaterThan)
 	case wasm.OpcodeI32GtU, wasm.OpcodeI64GtU:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondUnsignedGreaterThan)
 	case wasm.OpcodeI32LeS, wasm.OpcodeI64LeS:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondSignedLessThanOrEqual)
 	case wasm.OpcodeI32LeU, wasm.OpcodeI64LeU:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondUnsignedLessThanOrEqual)
 	case wasm.OpcodeI32GeS, wasm.OpcodeI64GeS:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondSignedGreaterThanOrEqual)
 	case wasm.OpcodeI32GeU, wasm.OpcodeI64GeU:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertIcmp(ssa.IntegerCmpCondUnsignedGreaterThanOrEqual)
 
 	case wasm.OpcodeF32Eq, wasm.OpcodeF64Eq:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertFcmp(ssa.FloatCmpCondEqual)
 	case wasm.OpcodeF32Ne, wasm.OpcodeF64Ne:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertFcmp(ssa.FloatCmpCondNotEqual)
 	case wasm.OpcodeF32Lt, wasm.OpcodeF64Lt:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertFcmp(ssa.FloatCmpCondLessThan)
 	case wasm.OpcodeF32Gt, wasm.OpcodeF64Gt:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertFcmp(ssa.FloatCmpCondGreaterThan)
 	case wasm.OpcodeF32Le, wasm.OpcodeF64Le:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertFcmp(ssa.FloatCmpCondLessThanOrEqual)
 	case wasm.OpcodeF32Ge, wasm.OpcodeF64Ge:
 		if state.unreachable {
-			return
+			break
 		}
 		c.insertFcmp(ssa.FloatCmpCondGreaterThanOrEqual)
 	case wasm.OpcodeF32Neg, wasm.OpcodeF64Neg:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsFneg(x).Insert(builder).Return()
 		state.push(v)
 	case wasm.OpcodeF32Sqrt, wasm.OpcodeF64Sqrt:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsSqrt(x).Insert(builder).Return()
 		state.push(v)
 	case wasm.OpcodeF32Abs, wasm.OpcodeF64Abs:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsFabs(x).Insert(builder).Return()
 		state.push(v)
 	case wasm.OpcodeF32Copysign, wasm.OpcodeF64Copysign:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		v := builder.AllocateInstruction().AsFcopysign(x, y).Insert(builder).Return()
@@ -444,28 +439,28 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeF32Ceil, wasm.OpcodeF64Ceil:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsCeil(x).Insert(builder).Return()
 		state.push(v)
 	case wasm.OpcodeF32Floor, wasm.OpcodeF64Floor:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsFloor(x).Insert(builder).Return()
 		state.push(v)
 	case wasm.OpcodeF32Trunc, wasm.OpcodeF64Trunc:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsTrunc(x).Insert(builder).Return()
 		state.push(v)
 	case wasm.OpcodeF32Nearest, wasm.OpcodeF64Nearest:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		v := builder.AllocateInstruction().AsNearest(x).Insert(builder).Return()
@@ -475,18 +470,49 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		wasm.OpcodeI64TruncF64U, wasm.OpcodeI64TruncF32U,
 		wasm.OpcodeI32TruncF64U, wasm.OpcodeI32TruncF32U:
 		if state.unreachable {
-			return
+			break
 		}
 		ret := builder.AllocateInstruction().AsFcvtToInt(
 			state.pop(),
 			c.execCtxPtrValue,
 			op == wasm.OpcodeI64TruncF64S || op == wasm.OpcodeI64TruncF32S || op == wasm.OpcodeI32TruncF32S || op == wasm.OpcodeI32TruncF64S,
 			op == wasm.OpcodeI64TruncF64S || op == wasm.OpcodeI64TruncF32S || op == wasm.OpcodeI64TruncF64U || op == wasm.OpcodeI64TruncF32U,
+			false,
 		).Insert(builder).Return()
 		state.push(ret)
+	case wasm.OpcodeMiscPrefix:
+		c.loweringState.pc++
+		// A misc opcode is encoded as an unsigned variable 32-bit integer.
+		miscOpUint, num, err := leb128.LoadUint32(c.wasmFunctionBody[c.loweringState.pc:])
+		if err != nil {
+			// In normal conditions this should never happen because the function has passed validation.
+			panic(fmt.Sprintf("failed to read misc opcode: %v", err))
+		}
+		c.loweringState.pc += int(num - 1)
+		miscOp := wasm.OpcodeMisc(miscOpUint)
+		switch miscOp {
+		case wasm.OpcodeMiscI64TruncSatF64S, wasm.OpcodeMiscI64TruncSatF32S,
+			wasm.OpcodeMiscI32TruncSatF64S, wasm.OpcodeMiscI32TruncSatF32S,
+			wasm.OpcodeMiscI64TruncSatF64U, wasm.OpcodeMiscI64TruncSatF32U,
+			wasm.OpcodeMiscI32TruncSatF64U, wasm.OpcodeMiscI32TruncSatF32U:
+			if state.unreachable {
+				break
+			}
+			ret := builder.AllocateInstruction().AsFcvtToInt(
+				state.pop(),
+				c.execCtxPtrValue,
+				miscOp == wasm.OpcodeMiscI64TruncSatF64S || miscOp == wasm.OpcodeMiscI64TruncSatF32S || miscOp == wasm.OpcodeMiscI32TruncSatF32S || miscOp == wasm.OpcodeMiscI32TruncSatF64S,
+				miscOp == wasm.OpcodeMiscI64TruncSatF64S || miscOp == wasm.OpcodeMiscI64TruncSatF32S || miscOp == wasm.OpcodeMiscI64TruncSatF64U || miscOp == wasm.OpcodeMiscI64TruncSatF32U,
+				true,
+			).Insert(builder).Return()
+			state.push(ret)
+		default:
+			panic("Unknown MiscOp " + strconv.Itoa(int(miscOpUint)))
+		}
+
 	case wasm.OpcodeI32ReinterpretF32:
 		if state.unreachable {
-			return
+			break
 		}
 		reinterpret := builder.AllocateInstruction().
 			AsBitcast(state.pop(), ssa.TypeI32).
@@ -495,7 +521,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI64ReinterpretF64:
 		if state.unreachable {
-			return
+			break
 		}
 		reinterpret := builder.AllocateInstruction().
 			AsBitcast(state.pop(), ssa.TypeI64).
@@ -504,7 +530,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeF32ReinterpretI32:
 		if state.unreachable {
-			return
+			break
 		}
 		reinterpret := builder.AllocateInstruction().
 			AsBitcast(state.pop(), ssa.TypeF32).
@@ -513,7 +539,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeF64ReinterpretI64:
 		if state.unreachable {
-			return
+			break
 		}
 		reinterpret := builder.AllocateInstruction().
 			AsBitcast(state.pop(), ssa.TypeF64).
@@ -522,7 +548,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI32DivS, wasm.OpcodeI64DivS:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		result := builder.AllocateInstruction().AsSDiv(x, y, c.execCtxPtrValue).Insert(builder).Return()
@@ -530,7 +556,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI32DivU, wasm.OpcodeI64DivU:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		result := builder.AllocateInstruction().AsUDiv(x, y, c.execCtxPtrValue).Insert(builder).Return()
@@ -538,7 +564,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI32RemS, wasm.OpcodeI64RemS:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		result := builder.AllocateInstruction().AsSRem(x, y, c.execCtxPtrValue).Insert(builder).Return()
@@ -546,7 +572,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI32RemU, wasm.OpcodeI64RemU:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		result := builder.AllocateInstruction().AsURem(x, y, c.execCtxPtrValue).Insert(builder).Return()
@@ -554,7 +580,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI32And, wasm.OpcodeI64And:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		and := builder.AllocateInstruction()
@@ -564,7 +590,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Or, wasm.OpcodeI64Or:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		or := builder.AllocateInstruction()
@@ -574,7 +600,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Xor, wasm.OpcodeI64Xor:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		xor := builder.AllocateInstruction()
@@ -584,7 +610,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Shl, wasm.OpcodeI64Shl:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		ishl := builder.AllocateInstruction()
@@ -594,7 +620,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32ShrU, wasm.OpcodeI64ShrU:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		ishl := builder.AllocateInstruction()
@@ -604,7 +630,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32ShrS, wasm.OpcodeI64ShrS:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		ishl := builder.AllocateInstruction()
@@ -614,7 +640,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Rotl, wasm.OpcodeI64Rotl:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		rotl := builder.AllocateInstruction()
@@ -624,7 +650,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Rotr, wasm.OpcodeI64Rotr:
 		if state.unreachable {
-			return
+			break
 		}
 		y, x := state.pop(), state.pop()
 		rotr := builder.AllocateInstruction()
@@ -634,7 +660,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Clz, wasm.OpcodeI64Clz:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		clz := builder.AllocateInstruction()
@@ -644,7 +670,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Ctz, wasm.OpcodeI64Ctz:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		ctz := builder.AllocateInstruction()
@@ -654,7 +680,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(value)
 	case wasm.OpcodeI32Popcnt, wasm.OpcodeI64Popcnt:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		popcnt := builder.AllocateInstruction()
@@ -665,7 +691,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeI32WrapI64:
 		if state.unreachable {
-			return
+			break
 		}
 		x := state.pop()
 		wrap := builder.AllocateInstruction().AsIreduce(x, ssa.TypeI32).Insert(builder).Return()
@@ -673,21 +699,21 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeGlobalGet:
 		index := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 		v := c.getWasmGlobalValue(index, false)
 		state.push(v)
 	case wasm.OpcodeGlobalSet:
 		index := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 		v := state.pop()
 		c.setWasmGlobalValue(index, v)
 	case wasm.OpcodeLocalGet:
 		index := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 		variable := c.localVariable(index)
 		v := builder.MustFindValue(variable)
@@ -695,7 +721,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeLocalSet:
 		index := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 		variable := c.localVariable(index)
 		newValue := state.pop()
@@ -704,7 +730,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeLocalTee:
 		index := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 		variable := c.localVariable(index)
 		newValue := state.peek()
@@ -716,7 +742,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		}
 
 		if state.unreachable {
-			return
+			break
 		}
 
 		cond := state.pop()
@@ -732,7 +758,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeMemorySize:
 		state.pc++ // skips the memory index.
 		if state.unreachable {
-			return
+			break
 		}
 
 		var memSizeInBytes ssa.Value
@@ -765,7 +791,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeMemoryGrow:
 		state.pc++ // skips the memory index.
 		if state.unreachable {
-			return
+			break
 		}
 
 		c.storeCallerModuleContext()
@@ -801,7 +827,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 		_, offset := c.readMemArg()
 		if state.unreachable {
-			return
+			break
 		}
 
 		var opSize uint64
@@ -849,7 +875,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		wasm.OpcodeI64Load32U:
 		_, offset := c.readMemArg()
 		if state.unreachable {
-			return
+			break
 		}
 
 		var opSize uint64
@@ -917,7 +943,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 		if state.unreachable {
 			state.unreachableDepth++
-			return
+			break
 		}
 
 		followingBlk := builder.AllocateBasicBlock()
@@ -934,7 +960,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 		if state.unreachable {
 			state.unreachableDepth++
-			return
+			break
 		}
 
 		loopHeader, afterLoopBlock := builder.AllocateBasicBlock(), builder.AllocateBasicBlock()
@@ -967,7 +993,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 		if state.unreachable {
 			state.unreachableDepth++
-			return
+			break
 		}
 
 		v := state.pop()
@@ -1016,7 +1042,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		if unreachable := state.unreachable; unreachable && state.unreachableDepth > 0 {
 			// If it is currently in unreachable and is a nested if,
 			// we just remove the entire else block.
-			return
+			break
 		}
 
 		if !state.unreachable {
@@ -1040,7 +1066,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeEnd:
 		if state.unreachableDepth > 0 {
 			state.unreachableDepth--
-			return
+			break
 		}
 
 		ctrl := state.ctrlPop()
@@ -1059,7 +1085,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 		switch ctrl.kind {
 		case controlFrameKindFunction:
-			return // This is the very end of function.
+			break // This is the very end of function.
 		case controlFrameKindLoop:
 			// Loop header block can be reached from any br/br_table contained in the loop,
 			// so now that we've reached End of it, we can seal it.
@@ -1079,7 +1105,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeBr:
 		labelIndex := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 
 		targetBlk, argNum := state.brTargetArgNumFor(labelIndex)
@@ -1091,7 +1117,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeBrIf:
 		labelIndex := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 
 		v := state.pop()
@@ -1121,7 +1147,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		}
 		labels = append(labels, c.readI32u()) // default label.
 		if state.unreachable {
-			return
+			break
 		}
 
 		index := state.pop()
@@ -1137,7 +1163,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	case wasm.OpcodeNop:
 	case wasm.OpcodeReturn:
 		if state.unreachable {
-			return
+			break
 		}
 		results := c.loweringState.nPeekDup(c.results())
 		instr := builder.AllocateInstruction()
@@ -1148,7 +1174,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeUnreachable:
 		if state.unreachable {
-			return
+			break
 		}
 		exit := builder.AllocateInstruction()
 		exit.AsExitWithCode(c.execCtxPtrValue, wazevoapi.ExitCodeUnreachable)
@@ -1159,14 +1185,14 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		typeIndex := c.readI32u()
 		tableIndex := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 		c.lowerCallIndirect(typeIndex, tableIndex)
 
 	case wasm.OpcodeCall:
 		fnIndex := c.readI32u()
 		if state.unreachable {
-			return
+			break
 		}
 
 		// Before transfer the control to the callee, we have to store the current module's moduleContextPtr
@@ -1233,12 +1259,12 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 
 	case wasm.OpcodeDrop:
 		if state.unreachable {
-			return
+			break
 		}
 		_ = state.pop()
 	case wasm.OpcodeF64ConvertI32S, wasm.OpcodeF64ConvertI64S, wasm.OpcodeF64ConvertI32U, wasm.OpcodeF64ConvertI64U:
 		if state.unreachable {
-			return
+			break
 		}
 		result := builder.AllocateInstruction().AsFcvtFromInt(
 			state.pop(),
@@ -1248,7 +1274,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(result)
 	case wasm.OpcodeF32ConvertI32S, wasm.OpcodeF32ConvertI64S, wasm.OpcodeF32ConvertI32U, wasm.OpcodeF32ConvertI64U:
 		if state.unreachable {
-			return
+			break
 		}
 		result := builder.AllocateInstruction().AsFcvtFromInt(
 			state.pop(),
@@ -1258,7 +1284,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(result)
 	case wasm.OpcodeF32DemoteF64:
 		if state.unreachable {
-			return
+			break
 		}
 		cvt := builder.AllocateInstruction()
 		cvt.AsFdemote(state.pop())
@@ -1266,7 +1292,7 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 		state.push(cvt.Return())
 	case wasm.OpcodeF64PromoteF32:
 		if state.unreachable {
-			return
+			break
 		}
 		cvt := builder.AllocateInstruction()
 		cvt.AsFpromote(state.pop())
@@ -1275,6 +1301,14 @@ func (c *Compiler) lowerOpcode(op wasm.Opcode) {
 	default:
 		panic("TODO: unsupported in wazevo yet: " + wasm.InstructionName(op))
 	}
+
+	if wazevoapi.FrontEndLoggingEnabled {
+		fmt.Println("--------- Translated " + wasm.InstructionName(op) + " --------")
+		fmt.Println("state: " + c.loweringState.String())
+		fmt.Println(c.formatBuilder())
+		fmt.Println("--------------------------")
+	}
+	c.loweringState.pc++
 }
 
 const (
diff --git a/internal/engine/wazevo/ssa/instructions.go b/internal/engine/wazevo/ssa/instructions.go
index 317ff0c9..b526f1dd 100644
--- a/internal/engine/wazevo/ssa/instructions.go
+++ b/internal/engine/wazevo/ssa/instructions.go
@@ -780,6 +780,8 @@ var instructionSideEffects = [opcodeEnd]sideEffect{
 	OpcodeFcvtToUint:         sideEffectTraps,
 	OpcodeFcvtFromSint:       sideEffectNone,
 	OpcodeFcvtFromUint:       sideEffectNone,
+	OpcodeFcvtToSintSat:      sideEffectNone,
+	OpcodeFcvtToUintSat:      sideEffectNone,
 	OpcodeFdemote:            sideEffectNone,
 	OpcodeFpromote:           sideEffectNone,
 	OpcodeBitcast:            sideEffectNone,
@@ -904,6 +906,8 @@ var instructionReturnTypes = [opcodeEnd]returnTypesFn{
 	OpcodeFcvtToUint:         returnTypesFnSingle,
 	OpcodeFcvtFromSint:       returnTypesFnSingle,
 	OpcodeFcvtFromUint:       returnTypesFnSingle,
+	OpcodeFcvtToSintSat:      returnTypesFnSingle,
+	OpcodeFcvtToUintSat:      returnTypesFnSingle,
 	OpcodeFneg:               returnTypesFnSingle,
 	OpcodeFdemote:            returnTypesFnF32,
 	OpcodeFpromote:           returnTypesFnF64,
@@ -1509,11 +1513,16 @@ func (i *Instruction) AsFcvtFromInt(x Value, signed bool, dst64bit bool) *Instru
 }
 
 // AsFcvtToInt initializes this instruction as an instruction with either OpcodeFcvtToUint or OpcodeFcvtToSint
-func (i *Instruction) AsFcvtToInt(x, ctx Value, signed bool, dst64bit bool) *Instruction {
-	if signed {
+func (i *Instruction) AsFcvtToInt(x, ctx Value, signed bool, dst64bit bool, sat bool) *Instruction {
+	switch {
+	case signed && !sat:
 		i.opcode = OpcodeFcvtToSint
-	} else {
+	case !signed && !sat:
 		i.opcode = OpcodeFcvtToUint
+	case signed && sat:
+		i.opcode = OpcodeFcvtToSintSat
+	case !signed && sat:
+		i.opcode = OpcodeFcvtToUintSat
 	}
 	i.v = x
 	i.v2 = ctx
@@ -1677,7 +1686,7 @@ func (i *Instruction) Format(b Builder) string {
 		instSuffix = fmt.Sprintf(" %s, %s", i.v.Format(b), i.v2.Format(b))
 	case OpcodeUndefined:
 	case OpcodeClz, OpcodeCtz, OpcodePopcnt, OpcodeFneg, OpcodeFcvtToSint, OpcodeFcvtToUint, OpcodeFcvtFromSint,
-		OpcodeFcvtFromUint, OpcodeFdemote, OpcodeFpromote, OpcodeIreduce, OpcodeBitcast, OpcodeSqrt, OpcodeFabs,
+		OpcodeFcvtFromUint, OpcodeFcvtToSintSat, OpcodeFcvtToUintSat, OpcodeFdemote, OpcodeFpromote, OpcodeIreduce, OpcodeBitcast, OpcodeSqrt, OpcodeFabs,
 		OpcodeCeil, OpcodeFloor, OpcodeTrunc, OpcodeNearest:
 		instSuffix = " " + i.v.Format(b)
 	default:


Test patch:
diff --git a/internal/engine/wazevo/backend/backend_test.go b/internal/engine/wazevo/backend/backend_test.go
index b7cbe8a3..5b40ab66 100644
--- a/internal/engine/wazevo/backend/backend_test.go
+++ b/internal/engine/wazevo/backend/backend_test.go
@@ -1722,6 +1722,24 @@ L1 (SSA Block: blk0):
 	fcvt d1, s1
 	ldr x30, [sp], #0x10
 	ret
+`,
+		},
+		{
+			name: "nontrapping_float_conversions",
+			m:    testcases.NonTrappingFloatConversions.Module,
+			afterFinalizeARM64: `
+L1 (SSA Block: blk0):
+	str x30, [sp, #-0x10]!
+	fcvtzs x0, d0
+	fcvtzs x1, s1
+	fcvtzs w2, d0
+	fcvtzs w3, s1
+	fcvtzu x4, d0
+	fcvtzu x5, s1
+	fcvtzu w6, d0
+	fcvtzu w7, s1
+	ldr x30, [sp], #0x10
+	ret
 `,
 		},
 		{
diff --git a/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go b/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
index 473c13a6..4e84e5a5 100644
--- a/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
+++ b/internal/engine/wazevo/backend/isa/arm64/lower_instr_test.go
@@ -364,9 +364,16 @@ func Test_exitIfNotSequenceEncodingSize(t *testing.T) {
 }
 
 func TestMachine_lowerFpuToInt(t *testing.T) {
-	_, _, m := newSetupWithMockContext()
-	m.lowerFpuToInt(operandNR(x1VReg), operandNR(x2VReg), x15VReg, false, false, false)
-	require.Equal(t, `
+	for _, tc := range []struct {
+		name          string
+		nontrapping   bool
+		expectedAsm   string
+		expectedBytes string
+	}{
+		{
+			name:        "trapping",
+			nontrapping: false,
+			expectedAsm: `
 msr fpsr, xzr
 fcvtzu w1, s2
 mrs x27 fpsr
@@ -380,10 +387,27 @@ exit_sequence x15
 movz x27, #0xb, lsl 0
 str w27, [x15]
 exit_sequence x15
-`, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
+`,
+			expectedBytes: "3f441bd54100391e3b443bd57f0700f1210200544020221e070100549b0180d2fb0100b9fd0940f9fb0d40f97f030091fe1140f9c0035fd67b0180d2fb0100b9fd0940f9fb0d40f97f030091fe1140f9c0035fd6",
+		},
+		{
+			name:        "nontrapping",
+			nontrapping: true,
+			expectedAsm: `
+fcvtzu w1, s2
+`,
+			expectedBytes: "4100391e",
+		},
+	} {
+		t.Run(tc.name, func(t *testing.T) {
+			_, _, m := newSetupWithMockContext()
+			m.lowerFpuToInt(operandNR(x1VReg), operandNR(x2VReg), x15VReg, false, false, false, tc.nontrapping)
+			require.Equal(t, tc.expectedAsm, "\n"+formatEmittedInstructionsInCurrentBlock(m)+"\n")
 
-	m.FlushPendingInstructions()
-	m.encode(m.perBlockHead)
-	buf := m.compiler.Buf()
-	require.Equal(t, "3f441bd54100391e3b443bd57f0700f1210200544020221e070100549b0180d2fb0100b9fd0940f9fb0d40f97f030091fe1140f9c0035fd67b0180d2fb0100b9fd0940f9fb0d40f97f030091fe1140f9c0035fd6", hex.EncodeToString(buf))
+			m.FlushPendingInstructions()
+			m.encode(m.perBlockHead)
+			buf := m.compiler.Buf()
+			require.Equal(t, tc.expectedBytes, hex.EncodeToString(buf))
+		})
+	}
 }
diff --git a/internal/engine/wazevo/e2e_test.go b/internal/engine/wazevo/e2e_test.go
index c1ed6dcc..a91ffac4 100644
--- a/internal/engine/wazevo/e2e_test.go
+++ b/internal/engine/wazevo/e2e_test.go
@@ -130,7 +130,7 @@ func TestSpectestV2(t *testing.T) {
 	for _, tc := range []struct {
 		name string
 	}{
-		// {"conversions"}, includes non-trapping conversions.
+		{"conversions"}, // includes non-trapping conversions.
 	} {
 		t.Run(tc.name, func(t *testing.T) {
 			spectest.RunCase(t, v2.Testcases, tc.name, context.Background(), config,
diff --git a/internal/engine/wazevo/frontend/frontend_test.go b/internal/engine/wazevo/frontend/frontend_test.go
index 0ddd1192..53f6482f 100644
--- a/internal/engine/wazevo/frontend/frontend_test.go
+++ b/internal/engine/wazevo/frontend/frontend_test.go
@@ -677,6 +677,21 @@ blk0: (exec_ctx:i64, module_ctx:i64, v2:f64, v3:f32)
 	v12:f32 = Fdemote v2
 	v13:f64 = Fpromote v3
 	Jump blk_ret, v4, v5, v6, v7, v8, v9, v10, v11, v12, v13
+`,
+		},
+		{
+			name: "non-trapping float conversions", m: testcases.NonTrappingFloatConversions.Module,
+			exp: `
+blk0: (exec_ctx:i64, module_ctx:i64, v2:f64, v3:f32)
+	v4:i64 = FcvtToSintSat v2
+	v5:i64 = FcvtToSintSat v3
+	v6:i32 = FcvtToSintSat v2
+	v7:i32 = FcvtToSintSat v3
+	v8:i64 = FcvtToUintSat v2
+	v9:i64 = FcvtToUintSat v3
+	v10:i32 = FcvtToUintSat v2
+	v11:i32 = FcvtToUintSat v3
+	Jump blk_ret, v4, v5, v6, v7, v8, v9, v10, v11
 `,
 		},
 		{
diff --git a/internal/engine/wazevo/testcases/testcases.go b/internal/engine/wazevo/testcases/testcases.go
index 81e9eaf2..1455faa7 100644
--- a/internal/engine/wazevo/testcases/testcases.go
+++ b/internal/engine/wazevo/testcases/testcases.go
@@ -895,6 +895,39 @@ var (
 			wasm.OpcodeEnd,
 		}, []wasm.ValueType{}),
 	}
+	NonTrappingFloatConversions = TestCase{
+		Name: "float_conversions",
+		Module: SingleFunctionModule(wasm.FunctionType{
+			Params:  []wasm.ValueType{f64, f32},
+			Results: []wasm.ValueType{i64, i64, i32, i32, i64, i64, i32, i32},
+		}, []byte{
+			wasm.OpcodeLocalGet, 0,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF64S,
+
+			wasm.OpcodeLocalGet, 1,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF32S,
+
+			wasm.OpcodeLocalGet, 0,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF64S,
+
+			wasm.OpcodeLocalGet, 1,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32S,
+
+			wasm.OpcodeLocalGet, 0,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF64U,
+
+			wasm.OpcodeLocalGet, 1,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI64TruncSatF32U,
+
+			wasm.OpcodeLocalGet, 0,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF64U,
+
+			wasm.OpcodeLocalGet, 1,
+			wasm.OpcodeMiscPrefix, wasm.OpcodeMiscI32TruncSatF32U,
+
+			wasm.OpcodeEnd,
+		}, []wasm.ValueType{}),
+	}
 	FibonacciRecursive = TestCase{
 		Name: "recursive_fibonacci",
 		Module: SingleFunctionModule(i32_i32, []byte{