Repository: tetratelabs/wazero
Pull request #119 (2022-01-06 12:10:24)

jit: support for various float operations


Patch:
diff --git a/wasm/jit/compiler.go b/wasm/jit/compiler.go
index d10d548f..8bf62f5e 100644
--- a/wasm/jit/compiler.go
+++ b/wasm/jit/compiler.go
@@ -38,6 +38,13 @@ type compiler interface {
 	compileShr(o *wazeroir.OperationShr) error
 	compileRotl(o *wazeroir.OperationRotl) error
 	compileRotr(o *wazeroir.OperationRotr) error
+	compileAbs(o *wazeroir.OperationAbs) error
+	compileNeg(o *wazeroir.OperationNeg) error
+	compileCeil(o *wazeroir.OperationCeil) error
+	compileFloor(o *wazeroir.OperationFloor) error
+	compileTrunc(o *wazeroir.OperationTrunc) error
+	compileNearest(o *wazeroir.OperationNearest) error
+	compileSqrt(o *wazeroir.OperationSqrt) error
 	compileEq(o *wazeroir.OperationEq) error
 	compileNe(o *wazeroir.OperationNe) error
 	compileEqz(o *wazeroir.OperationEqz) error
diff --git a/wasm/jit/engine.go b/wasm/jit/engine.go
index 7920c16d..c6c8fb92 100644
--- a/wasm/jit/engine.go
+++ b/wasm/jit/engine.go
@@ -563,19 +563,19 @@ func (e *engine) compileWasmFunction(f *wasm.FunctionInstance) (*compiledWasmFun
 		case *wazeroir.OperationRotr:
 			err = compiler.compileRotr(o)
 		case *wazeroir.OperationAbs:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileAbs(o)
 		case *wazeroir.OperationNeg:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileNeg(o)
 		case *wazeroir.OperationCeil:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileCeil(o)
 		case *wazeroir.OperationFloor:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileFloor(o)
 		case *wazeroir.OperationTrunc:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileTrunc(o)
 		case *wazeroir.OperationNearest:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileNearest(o)
 		case *wazeroir.OperationSqrt:
-			err = fmt.Errorf("unsupported operation")
+			err = compiler.compileSqrt(o)
 		case *wazeroir.OperationMin:
 			err = fmt.Errorf("unsupported operation")
 		case *wazeroir.OperationMax:
diff --git a/wasm/jit/jit_amd64.go b/wasm/jit/jit_amd64.go
index 92707921..96fee6ce 100644
--- a/wasm/jit/jit_amd64.go
+++ b/wasm/jit/jit_amd64.go
@@ -14,6 +14,7 @@ import (
 	"fmt"
 	"math"
 	"runtime"
+	"unsafe"
 
 	asm "github.com/twitchyliquid64/golang-asm"
 	"github.com/twitchyliquid64/golang-asm/obj"
@@ -23,6 +24,21 @@ import (
 	"github.com/tetratelabs/wazero/wasm/wazeroir"
 )
 
+var (
+	float32SignBitMask        int64 = 1 << 31
+	float32SignBitMaskAddress uintptr
+	// float64SignBitMask equals 1 << 63 but Go compiler errors as the number doesn't fit into
+	// the range of "signed" 64bit int, so we have the literal signed 64-bit integer
+	// whose bit pattern equals 1 << 63 here.
+	float64SignBitMask        int64 = -9223372036854775808
+	float64SignBitMaskAddress uintptr
+)
+
+func init() {
+	float32SignBitMaskAddress = uintptr(unsafe.Pointer(&float32SignBitMask))
+	float64SignBitMaskAddress = uintptr(unsafe.Pointer(&float64SignBitMask))
+}
+
 // jitcall is implemented in jit_amd64.s as a Go Assembler function.
 // This is used by engine.exec and the entrypoint to enter the JITed native code.
 // codeSegment is the pointer to the initial instruction of the compiled native code.
@@ -1440,6 +1456,193 @@ func (c *amd64Compiler) emitSimpleBinaryOp(instruction obj.As) error {
 	return nil
 }
 
+// compileAbs adds instructions to replace the top value of float type on the stack with its absolute value.
+// For example, stack [..., -1.123] results in [..., 1.123].
+// See the following discussions for how we could take the abs of floats on x86 assembly.
+// https://stackoverflow.com/questions/32408665/fastest-way-to-compute-absolute-value-using-sse/32422471#32422471
+// https://stackoverflow.com/questions/44630015/how-would-fabsdouble-be-implemented-on-x86-is-it-an-expensive-operation
+func (c *amd64Compiler) compileAbs(o *wazeroir.OperationAbs) (err error) {
+	target := c.locationStack.peek() // Note this is peek!
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	// First shift left by one to clear the sign bit.
+	shiftLeftByOne := c.newProg()
+	if o.Type == wazeroir.Float32 {
+		shiftLeftByOne.As = x86.APSLLL
+	} else {
+		shiftLeftByOne.As = x86.APSLLQ
+	}
+	shiftLeftByOne.To.Type = obj.TYPE_REG
+	shiftLeftByOne.To.Reg = target.register
+	shiftLeftByOne.From.Type = obj.TYPE_CONST
+	shiftLeftByOne.From.Offset = 1
+	c.addInstruction(shiftLeftByOne)
+
+	// Then shift right by one.
+	shiftRightByOne := c.newProg()
+	if o.Type == wazeroir.Float32 {
+		shiftRightByOne.As = x86.APSRLL
+	} else {
+		shiftRightByOne.As = x86.APSRLQ
+	}
+	shiftRightByOne.To.Type = obj.TYPE_REG
+	shiftRightByOne.To.Reg = target.register
+	shiftRightByOne.From.Type = obj.TYPE_CONST
+	shiftRightByOne.From.Offset = 1
+	c.addInstruction(shiftRightByOne)
+	return nil
+}
+
+// compileNeg adds instructions to replace the top value of float type on the stack with its negated value.
+// For example, stack [..., -1.123] results in [..., 1.123].
+func (c *amd64Compiler) compileNeg(o *wazeroir.OperationNeg) (err error) {
+	target := c.locationStack.peek() // Note this is peek!
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	tmpReg, err := c.allocateRegister(generalPurposeRegisterTypeFloat)
+	if err != nil {
+		return err
+	}
+
+	// First we move the sign-bit mask (placed in memory) to the tmp register,
+	// since we cannot take XOR directly with float reg and const.
+	movToTmp := c.newProg()
+	var maskAddr uintptr
+	if o.Type == wazeroir.Float32 {
+		movToTmp.As = x86.AMOVL
+		maskAddr = float32SignBitMaskAddress
+	} else {
+		movToTmp.As = x86.AMOVQ
+		maskAddr = float64SignBitMaskAddress
+	}
+	movToTmp.From.Type = obj.TYPE_MEM
+	movToTmp.From.Offset = int64(maskAddr)
+	movToTmp.To.Type = obj.TYPE_REG
+	movToTmp.To.Reg = tmpReg
+	c.addInstruction(movToTmp)
+
+	// Negate the value by XOR it with the sign-bit mask.
+	negate := c.newProg()
+	if o.Type == wazeroir.Float32 {
+		negate.As = x86.AXORPS
+	} else {
+		negate.As = x86.AXORPD
+	}
+	negate.From.Type = obj.TYPE_REG
+	negate.From.Reg = tmpReg
+	negate.To.Type = obj.TYPE_REG
+	negate.To.Reg = target.register
+	c.addInstruction(negate)
+	return nil
+}
+
+// compileCeil adds instructions to replace the top value of float type on the stack with its ceiling value.
+// For example, stack [..., 1.123] results in [..., 2.0]. This is equivalent to math.Ceil.
+func (c *amd64Compiler) compileCeil(o *wazeroir.OperationCeil) (err error) {
+	// Internally, ceil can be performed via ROUND instruction with 0x02 mode.
+	// See https://android.googlesource.com/platform/bionic/+/882b8af/libm/x86_64/ceilf.S for example.
+	return c.emitRoundInstruction(o.Type == wazeroir.Float32, 0x02)
+}
+
+// compileFloor adds instructions to replace the top value of float type on the stack with its floor value.
+// For example, stack [..., 1.123] results in [..., 1.0]. This is equivalent to math.Floor.
+func (c *amd64Compiler) compileFloor(o *wazeroir.OperationFloor) (err error) {
+	// Internally, floor can be performed via ROUND instruction with 0x01 mode.
+	// See https://android.googlesource.com/platform/bionic/+/882b8af/libm/x86_64/floorf.S for example.
+	return c.emitRoundInstruction(o.Type == wazeroir.Float32, 0x01)
+}
+
+// compileTrunc adds instructions to replace the top value of float type on the stack with its truncated value.
+// For example, stack [..., 1.9] results in [..., 1.0]. This is equivalent to math.Trunc.
+func (c *amd64Compiler) compileTrunc(o *wazeroir.OperationTrunc) error {
+	// Internally, trunc can be performed via ROUND instruction with 0x03 mode.
+	// See https://android.googlesource.com/platform/bionic/+/882b8af/libm/x86_64/truncf.S for example.
+	return c.emitRoundInstruction(o.Type == wazeroir.Float32, 0x03)
+}
+
+// compileNearest adds instructions to replace the top value of float type on the stack with its nearest integer value.
+// For example, stack [..., 1.9] results in [..., 2.0]. This is NOT equivalent to math.Round and instead has the same
+// the semantics of LLVM's rint instrinsic. See https://llvm.org/docs/LangRef.html#llvm-rint-intrinsic.
+// For example, math.Round(-4.5) produces -5 while ROUND with 0x00 mode produces -4.
+func (c *amd64Compiler) compileNearest(o *wazeroir.OperationNearest) error {
+	// Internally, nearest can be performed via ROUND instruction with 0x00 mode.
+	// If we compile the following Wat by "wasmtime wasm2obj",
+	//
+	// (module
+	//   (func (export "nearest_f32") (param $x f32) (result f32) (f32.nearest (local.get $x)))
+	//   (func (export "nearest_f64") (param $x f64) (result f64) (f64.nearest (local.get $x)))
+	// )
+	//
+	// we see a disassemble of the object via "objdump --disassemble-all" like:
+	//
+	// 0000000000000000 <_wasm_function_0>:
+	// 	0:       55                      push   %rbp
+	// 	1:       48 89 e5                mov    %rsp,%rbp
+	// 	4:       66 0f 3a 0a c0 00       roundss $0x0,%xmm0,%xmm0
+	// 	a:       48 89 ec                mov    %rbp,%rsp
+	// 	d:       5d                      pop    %rbp
+	// 	e:       c3                      retq
+	//
+	// 000000000000000f <_wasm_function_1>:
+	// 	f:        55                      push   %rbp
+	//  10:       48 89 e5                mov    %rsp,%rbp
+	//  13:       66 0f 3a 0b c0 00       roundsd $0x0,%xmm0,%xmm0
+	//  19:       48 89 ec                mov    %rbp,%rsp
+	//  1c:       5d                      pop    %rbp
+	//  1d:       c3                      retq
+	//
+	// Below, we use the same implementation: "rounds{s,d} $0x0,%xmm0,%xmm0" where the mode is set to zero.
+	return c.emitRoundInstruction(o.Type == wazeroir.Float32, 0x00)
+}
+
+func (c *amd64Compiler) emitRoundInstruction(is32Bit bool, mode int64) error {
+	target := c.locationStack.peek() // Note this is peek!
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	round := c.newProg()
+	if is32Bit {
+		round.As = x86.AROUNDSS
+	} else {
+		round.As = x86.AROUNDSD
+	}
+	round.To.Type = obj.TYPE_REG
+	round.To.Reg = target.register
+	round.From.Type = obj.TYPE_CONST
+	round.From.Offset = mode
+	round.RestArgs = append(round.RestArgs,
+		obj.Addr{Reg: target.register, Type: obj.TYPE_REG})
+	c.addInstruction(round)
+	return nil
+}
+
+// compileSqrt adds instructions to replace the top value of float type on the stack with its square root.
+// For example, stack [..., 9.0] results in [..., 3.0]. This is equivalent to "math.Sqrt".
+func (c *amd64Compiler) compileSqrt(o *wazeroir.OperationSqrt) error {
+	target := c.locationStack.peek() // Note this is peek!
+	if err := c.ensureOnGeneralPurposeRegister(target); err != nil {
+		return err
+	}
+
+	sqrt := c.newProg()
+	if o.Type == wazeroir.Float32 {
+		sqrt.As = x86.ASQRTSS
+	} else {
+		sqrt.As = x86.ASQRTSD
+	}
+	sqrt.From.Type = obj.TYPE_REG
+	sqrt.From.Reg = target.register
+	sqrt.To.Type = obj.TYPE_REG
+	sqrt.To.Reg = target.register
+	c.addInstruction(sqrt)
+	return nil
+}
+
 func (c *amd64Compiler) compileEq(o *wazeroir.OperationEq) error {
 	return c.emitEqOrNe(o.Type, true)
 }
diff --git a/wasm/wazeroir/interpreter.go b/wasm/wazeroir/interpreter.go
index 561689f5..d7239280 100644
--- a/wasm/wazeroir/interpreter.go
+++ b/wasm/wazeroir/interpreter.go
@@ -1211,38 +1211,40 @@ func (it *interpreter) callNativeFunc(f *interpreterFunction) {
 			}
 		case OperationKindNearest:
 			{
-				// Borrowed from https://github.com/wasmerio/wasmer/blob/703bb4ee2ffb17b2929a194fc045a7e351b696e2/lib/vm/src/libcalls.rs#L77
+				// TODO: look at https://github.com/bytecodealliance/wasmtime/pull/2171 and reconsider this algorithm
 				if op.b1 == 0 {
 					// Float32
 					f := math.Float32frombits(uint32(it.pop()))
-					f64 := float64(f)
 					if f != -0 && f != 0 {
-						u := float32(math.Ceil(f64))
-						d := float32(math.Floor(f64))
-						um := math.Abs(float64(f - u))
-						dm := math.Abs(float64(f - d))
-						h := u / 2.0
-						if um < dm || float32(math.Floor(float64(h))) == h {
-							f = u
+						ceil := float32(math.Ceil(float64(f)))
+						floor := float32(math.Floor(float64(f)))
+						distToCeil := math.Abs(float64(f - ceil))
+						distToFloor := math.Abs(float64(f - floor))
+						h := ceil / 2.0
+						if distToCeil < distToFloor {
+							f = ceil
+						} else if distToCeil == distToFloor && float32(math.Floor(float64(h))) == h {
+							f = ceil
 						} else {
-							f = d
+							f = floor
 						}
 					}
 					it.push(uint64(math.Float32bits(f)))
 				} else {
 					// Float64
 					f := math.Float64frombits(it.pop())
-					f64 := float64(f)
 					if f != -0 && f != 0 {
-						u := math.Ceil(f64)
-						d := math.Floor(f64)
-						um := math.Abs(f - u)
-						dm := math.Abs(f - d)
-						h := u / 2.0
-						if um < dm || math.Floor(float64(h)) == h {
-							f = u
+						ceil := math.Ceil(f)
+						floor := math.Floor(f)
+						distToCeil := math.Abs(f - ceil)
+						distToFloor := math.Abs(f - floor)
+						h := ceil / 2.0
+						if distToCeil < distToFloor {
+							f = ceil
+						} else if distToCeil == distToFloor && math.Floor(float64(h)) == h {
+							f = ceil
 						} else {
-							f = d
+							f = floor
 						}
 					}
 					it.push(math.Float64bits(f))


Test patch:
diff --git a/wasm/jit/jit_amd64_test.go b/wasm/jit/jit_amd64_test.go
index 89d86d75..41505b5a 100644
--- a/wasm/jit/jit_amd64_test.go
+++ b/wasm/jit/jit_amd64_test.go
@@ -3877,6 +3877,216 @@ func TestAmd64Compiler_compileRem(t *testing.T) {
 	})
 }
 
+func TestAmd64Compiler_compile_abs_neg_ceil_floor(t *testing.T) {
+	for _, tc := range []struct {
+		name string
+		op   wazeroir.Operation
+	}{
+		{name: "abs-32-bit", op: &wazeroir.OperationAbs{Type: wazeroir.Float32}},
+		{name: "abs-64-bit", op: &wazeroir.OperationAbs{Type: wazeroir.Float64}},
+		{name: "neg-32-bit", op: &wazeroir.OperationNeg{Type: wazeroir.Float32}},
+		{name: "neg-64-bit", op: &wazeroir.OperationNeg{Type: wazeroir.Float64}},
+		{name: "ceil-32-bit", op: &wazeroir.OperationCeil{Type: wazeroir.Float32}},
+		{name: "ceil-64-bit", op: &wazeroir.OperationCeil{Type: wazeroir.Float64}},
+		{name: "floor-32-bit", op: &wazeroir.OperationFloor{Type: wazeroir.Float32}},
+		{name: "floor-64-bit", op: &wazeroir.OperationFloor{Type: wazeroir.Float64}},
+		{name: "trunc-32-bit", op: &wazeroir.OperationTrunc{Type: wazeroir.Float32}},
+		{name: "trunc-64-bit", op: &wazeroir.OperationTrunc{Type: wazeroir.Float64}},
+		{name: "sqrt-32-bit", op: &wazeroir.OperationSqrt{Type: wazeroir.Float32}},
+		{name: "sqrt-64-bit", op: &wazeroir.OperationSqrt{Type: wazeroir.Float64}},
+		{name: "nearest-32-bit", op: &wazeroir.OperationNearest{Type: wazeroir.Float32}},
+		{name: "nearest-64-bit", op: &wazeroir.OperationNearest{Type: wazeroir.Float64}},
+	} {
+		tc := tc
+		t.Run(tc.name, func(t *testing.T) {
+			for i, v := range []uint64{
+				0,
+				1 << 63,
+				1<<63 | 12345,
+				1 << 31,
+				1<<31 | 123455,
+				6.8719476736e+10,
+				math.Float64bits(-4.5), // This produces the different result between math.Round and ROUND with 0x00 mode.
+				1.37438953472e+11,
+				math.Float64bits(-1.3),
+				uint64(math.Float32bits(-1231.123)),
+				math.Float64bits(1.3),
+				math.Float64bits(100.3),
+				math.Float64bits(-100.3),
+				uint64(math.Float32bits(1231.123)),
+				math.Float64bits(math.Inf(1)),
+				math.Float64bits(math.Inf(-1)),
+				math.Float64bits(math.NaN()),
+			} {
+				t.Run(fmt.Sprintf("%d", i), func(t *testing.T) {
+					compiler := requireNewCompiler(t)
+					compiler.initializeReservedRegisters()
+					eng := newEngine()
+
+					var is32Bit bool
+					var expFloat32 float32
+					var expFloat64 float64
+					var compileOperationFunc func()
+					switch o := tc.op.(type) {
+					case *wazeroir.OperationAbs:
+						compileOperationFunc = func() {
+							err := compiler.compileAbs(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						if is32Bit {
+							expFloat32 = float32(math.Abs(float64(math.Float32frombits(uint32(v)))))
+						} else {
+							expFloat64 = math.Abs(math.Float64frombits(v))
+						}
+					case *wazeroir.OperationNeg:
+						compileOperationFunc = func() {
+							err := compiler.compileNeg(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						if is32Bit {
+							expFloat32 = -math.Float32frombits(uint32(v))
+						} else {
+							expFloat64 = -math.Float64frombits(v)
+						}
+					case *wazeroir.OperationCeil:
+						compileOperationFunc = func() {
+							err := compiler.compileCeil(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						if is32Bit {
+							expFloat32 = float32(math.Ceil(float64(math.Float32frombits(uint32(v)))))
+						} else {
+							expFloat64 = math.Ceil(math.Float64frombits(v))
+						}
+					case *wazeroir.OperationFloor:
+						compileOperationFunc = func() {
+							err := compiler.compileFloor(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						if is32Bit {
+							expFloat32 = float32(math.Floor(float64(math.Float32frombits(uint32(v)))))
+						} else {
+							expFloat64 = math.Floor(math.Float64frombits(v))
+						}
+					case *wazeroir.OperationTrunc:
+						compileOperationFunc = func() {
+							err := compiler.compileTrunc(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						if is32Bit {
+							expFloat32 = float32(math.Trunc(float64(math.Float32frombits(uint32(v)))))
+						} else {
+							expFloat64 = math.Trunc(math.Float64frombits(v))
+						}
+					case *wazeroir.OperationSqrt:
+						compileOperationFunc = func() {
+							err := compiler.compileSqrt(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						if is32Bit {
+							expFloat32 = float32(math.Sqrt(float64(math.Float32frombits(uint32(v)))))
+						} else {
+							expFloat64 = math.Sqrt(math.Float64frombits(v))
+						}
+					case *wazeroir.OperationNearest:
+						compileOperationFunc = func() {
+							err := compiler.compileNearest(o)
+							require.NoError(t, err)
+						}
+						is32Bit = o.Type == wazeroir.Float32
+						// The same algorithm as in wazeroir/interpreter.go.
+						if is32Bit {
+							expFloat32 = math.Float32frombits(uint32(v))
+							f64 := float64(expFloat32)
+							if expFloat32 != -0 && expFloat32 != 0 {
+								ceil := float32(math.Ceil(f64))
+								floor := float32(math.Floor(f64))
+								distToCeil := math.Abs(float64(expFloat32 - ceil))
+								distToFloor := math.Abs(float64(expFloat32 - floor))
+								h := ceil / 2.0
+								if distToCeil < distToFloor {
+									expFloat32 = ceil
+								} else if distToCeil == distToFloor && float32(math.Floor(float64(h))) == h {
+									expFloat32 = ceil
+								} else {
+									expFloat32 = floor
+								}
+							}
+						} else {
+							expFloat64 = math.Float64frombits(v)
+							if expFloat64 != -0 && expFloat64 != 0 {
+								ceil := math.Ceil(expFloat64)
+								floor := math.Floor(expFloat64)
+								distToCeil := math.Abs(expFloat64 - ceil)
+								distToFloor := math.Abs(expFloat64 - floor)
+								h := ceil / 2.0
+								if distToCeil < distToFloor {
+									expFloat64 = ceil
+								} else if distToCeil == distToFloor && math.Floor(h) == h {
+									expFloat64 = ceil
+								} else {
+									expFloat64 = floor
+								}
+							}
+						}
+					}
+
+					// Setup the target values.
+					var err error
+					if is32Bit {
+						err = compiler.compileConstF32(&wazeroir.OperationConstF32{Value: math.Float32frombits(uint32(v))})
+					} else {
+						err = compiler.compileConstF64(&wazeroir.OperationConstF64{Value: math.Float64frombits(v)})
+					}
+					require.NoError(t, err)
+
+					// Compile the operation.
+					compileOperationFunc()
+
+					// To verify the behavior, we release the value
+					// to the stack.
+					compiler.releaseAllRegistersToStack()
+					compiler.returnFunction()
+
+					// Generate and run the code under test.
+					code, _, err := compiler.generate()
+					require.NoError(t, err)
+					mem := newMemoryInst()
+					jitcall(
+						uintptr(unsafe.Pointer(&code[0])),
+						uintptr(unsafe.Pointer(eng)),
+						uintptr(unsafe.Pointer(&mem.Buffer[0])),
+					)
+
+					// Check the result.
+					require.Equal(t, uint64(1), eng.stackPointer)
+					if is32Bit {
+						actual := math.Float32frombits(uint32(eng.stack[eng.stackPointer-1]))
+						if math.IsNaN(float64(expFloat32)) {
+							require.True(t, math.IsNaN(float64(actual)))
+						} else {
+							require.Equal(t, actual, expFloat32)
+						}
+					} else {
+						actual := math.Float64frombits(eng.stack[eng.stackPointer-1])
+						if math.IsNaN(expFloat64) {
+							require.True(t, math.IsNaN(actual))
+						} else {
+							require.Equal(t, expFloat64, actual)
+						}
+					}
+				})
+			}
+		})
+	}
+}
+
 func TestAmd64Compiler_compileCall(t *testing.T) {
 	t.Run("host function", func(t *testing.T) {
 		const functionIndex = 5