package spectest import ( "context" "embed" "encoding/json" "fmt" "math" "strconv" "strings" "testing" "github.com/tetratelabs/wazero" "github.com/tetratelabs/wazero/api" "github.com/tetratelabs/wazero/internal/moremath" "github.com/tetratelabs/wazero/internal/testing/require" "github.com/tetratelabs/wazero/internal/wasm" "github.com/tetratelabs/wazero/internal/wasmruntime" ) type ( testbase struct { SourceFile string `json:"source_filename"` Commands []command `json:"commands"` } command struct { CommandType string `json:"type"` Line int `json:"line"` // Set when type == "module" || "register" Name string `json:"name,omitempty"` // Set when type == "module" || "assert_uninstantiable" || "assert_malformed" Filename string `json:"filename,omitempty"` // Set when type == "register" As string `json:"as,omitempty"` // Set when type == "assert_return" || "action" Action commandAction `json:"action,omitempty"` Exps []commandActionVal `json:"expected"` // Set when type == "assert_malformed" ModuleType string `json:"module_type"` // Set when type == "assert_trap" Text string `json:"text"` } commandAction struct { ActionType string `json:"type"` Args []commandActionVal `json:"args"` // Set when ActionType == "invoke" Field string `json:"field,omitempty"` Module string `json:"module,omitempty"` } commandActionVal struct { ValType string `json:"type"` // LaneType is not empty if ValueType == "v128" LaneType laneType `json:"lane_type"` Value interface{} `json:"value"` } ) // laneType is a type of each lane of vector value. // // See https://github.com/WebAssembly/wabt/blob/main/docs/wast2json.md#const type laneType = string const ( laneTypeI8 laneType = "i8" laneTypeI16 laneType = "i16" laneTypeI32 laneType = "i32" laneTypeI64 laneType = "i64" laneTypeF32 laneType = "f32" laneTypeF64 laneType = "f64" ) func (c commandActionVal) String() string { var v string valTypeStr := c.ValType switch c.ValType { case "i32": v = c.Value.(string) case "f32": str := c.Value.(string) if strings.Contains(str, "nan") { v = str } else { ret, _ := strconv.ParseUint(str, 10, 32) v = fmt.Sprintf("%f", math.Float32frombits(uint32(ret))) } case "i64": v = c.Value.(string) case "f64": str := c.Value.(string) if strings.Contains(str, "nan") { v = str } else { ret, _ := strconv.ParseUint(str, 10, 64) v = fmt.Sprintf("%f", math.Float64frombits(ret)) } case "externref": if c.Value == "null" { v = "null" } else { original, _ := strconv.ParseUint(c.Value.(string), 10, 64) // In wazero, externref is opaque pointer, so "0" is considered as null. // So in order to treat "externref 0" in spectest non nullref, we increment the value. v = fmt.Sprintf("%d", original+1) } case "funcref": // All the in and out funcref params are null in spectest (cannot represent non-null as it depends on runtime impl). v = "null" case "v128": simdValues, ok := c.Value.([]interface{}) if !ok { panic("BUG") } var strs []string for _, v := range simdValues { strs = append(strs, v.(string)) } v = strings.Join(strs, ",") valTypeStr = fmt.Sprintf("v128[lane=%s]", c.LaneType) } return fmt.Sprintf("{type: %s, value: %v}", valTypeStr, v) } func (c command) String() string { msg := fmt.Sprintf("line: %d, type: %s", c.Line, c.CommandType) switch c.CommandType { case "register": msg += fmt.Sprintf(", name: %s, as: %s", c.Name, c.As) case "module": if c.Name != "" { msg += fmt.Sprintf(", name: %s, filename: %s", c.Name, c.Filename) } else { msg += fmt.Sprintf(", filename: %s", c.Filename) } case "assert_return", "action": msg += fmt.Sprintf(", action type: %s", c.Action.ActionType) if c.Action.Module != "" { msg += fmt.Sprintf(", module: %s", c.Action.Module) } msg += fmt.Sprintf(", field: %s", c.Action.Field) msg += fmt.Sprintf(", args: %v, expected: %v", c.Action.Args, c.Exps) case "assert_malformed": // TODO: case "assert_trap": msg += fmt.Sprintf(", args: %v, error text: %s", c.Action.Args, c.Text) case "assert_invalid": // TODO: case "assert_exhaustion": // TODO: case "assert_unlinkable": // TODO: case "assert_uninstantiable": // TODO: } return "{" + msg + "}" } func (c command) getAssertReturnArgs() []uint64 { var args []uint64 for _, arg := range c.Action.Args { args = append(args, arg.toUint64s()...) } return args } func (c command) getAssertReturnArgsExps() (args []uint64, exps []uint64) { for _, arg := range c.Action.Args { args = append(args, arg.toUint64s()...) } for _, exp := range c.Exps { exps = append(exps, exp.toUint64s()...) } return } func (c commandActionVal) toUint64s() (ret []uint64) { if c.ValType == "v128" { strValues, ok := c.Value.([]interface{}) if !ok { panic("BUG") } var width, valNum int switch c.LaneType { case "i8": width, valNum = 8, 16 case "i16": width, valNum = 16, 8 case "i32": width, valNum = 32, 4 case "i64": width, valNum = 64, 2 case "f32": width, valNum = 32, 4 case "f64": width, valNum = 64, 2 default: panic("BUG") } lo, hi := buildLaneUint64(strValues, width, valNum) return []uint64{lo, hi} } else { return []uint64{c.toUint64()} } } func buildLaneUint64(raw []interface{}, width, valNum int) (lo, hi uint64) { for i := 0; i < valNum; i++ { str := raw[i].(string) var v uint64 var err error if strings.Contains(str, "nan") { v = getNaNBits(str, width == 32) } else { v, err = strconv.ParseUint(str, 10, width) if err != nil { panic(err) } } if half := valNum / 2; i < half { lo |= v << (i * width) } else { hi |= v << ((i - half) * width) } } return } func getNaNBits(strValue string, is32bit bool) (ret uint64) { // Note: nan:canonical, nan:arithmetic only appears on the expected values. if is32bit { switch strValue { case "nan:canonical": ret = uint64(moremath.F32CanonicalNaNBits) case "nan:arithmetic": ret = uint64(moremath.F32ArithmeticNaNBits) default: panic("BUG") } } else { switch strValue { case "nan:canonical": ret = moremath.F64CanonicalNaNBits case "nan:arithmetic": ret = moremath.F64ArithmeticNaNBits default: panic("BUG") } } return } func (c commandActionVal) toUint64() (ret uint64) { strValue := c.Value.(string) if strings.Contains(strValue, "nan") { ret = getNaNBits(strValue, c.ValType == "f32") } else if c.ValType == "externref" { if c.Value == "null" { ret = 0 } else { original, _ := strconv.ParseUint(strValue, 10, 64) // In wazero, externref is opaque pointer, so "0" is considered as null. // So in order to treat "externref 0" in spectest non nullref, we increment the value. ret = original + 1 } } else if strings.Contains(c.ValType, "32") { ret, _ = strconv.ParseUint(strValue, 10, 32) } else { ret, _ = strconv.ParseUint(strValue, 10, 64) } return } // expectedError returns the expected runtime error when the command type equals assert_trap // which expects engines to emit the errors corresponding command.Text field. func (c command) expectedError() (err error) { if c.CommandType != "assert_trap" { panic("unreachable") } switch c.Text { case "out of bounds memory access": err = wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess case "indirect call type mismatch", "indirect call": err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch case "undefined element", "undefined", "out of bounds table access": err = wasmruntime.ErrRuntimeInvalidTableAccess case "integer overflow": err = wasmruntime.ErrRuntimeIntegerOverflow case "invalid conversion to integer": err = wasmruntime.ErrRuntimeInvalidConversionToInteger case "integer divide by zero": err = wasmruntime.ErrRuntimeIntegerDivideByZero case "unreachable": err = wasmruntime.ErrRuntimeUnreachable default: if strings.HasPrefix(c.Text, "uninitialized") { err = wasmruntime.ErrRuntimeInvalidTableAccess } } return } // spectestWasm was generated by the following: // // cd testdata; wat2wasm --debug-names spectest.wat // // This module is required by some test cases, and instantiated before running cases. // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25 // //go:embed testdata/spectest.wasm var spectestWasm []byte // Run runs all the test inside the testDataFS file system where all the cases are described // via JSON files created from wast2json. func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, config wazero.RuntimeConfig) { files, err := testDataFS.ReadDir("testdata") require.NoError(t, err) caseNames := make([]string, 0, len(files)) for _, f := range files { filename := f.Name() if strings.HasSuffix(filename, ".json") { caseNames = append(caseNames, strings.TrimSuffix(filename, ".json")) } } // If the go:embed path resolution was wrong, this fails. // https://github.com/tetratelabs/wazero/issues/247 require.True(t, len(caseNames) > 1, "len(caseNames)=%d (not greater than one)", len(caseNames)) for _, f := range caseNames { RunCase(t, testDataFS, f, ctx, config, -1, 0, math.MaxInt) } } // RunCase runs the test case described by the given spectest file name (without .wast!) in the testDataFS file system. // lineBegin and lineEnd are the line numbers to run. If lineBegin == 0 and lineEnd == math.MaxInt, all the lines are run. // // For example, if you want to run memory_grow.wast:66 to 70, you can do: // // RunCase(t, testDataFS, "memory_grow", ctx, config, mandatoryLine, 66, 70) // // where mandatoryLine is the line number which can be run regardless of the lineBegin and lineEnd. It is useful when // we only want to run specific command while running "module" command to instantiate a module. If you don't need it, // just pass -1. func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, config wazero.RuntimeConfig, mandatoryLine, lineBegin, lineEnd int) { raw, err := testDataFS.ReadFile(testdataPath(f + ".json")) require.NoError(t, err) var base testbase require.NoError(t, json.Unmarshal(raw, &base)) wastName := basename(base.SourceFile) t.Run(wastName, func(t *testing.T) { r := wazero.NewRuntimeWithConfig(ctx, config) defer func() { require.NoError(t, r.Close(ctx)) }() _, err := r.InstantiateWithConfig(ctx, spectestWasm, wazero.NewModuleConfig()) require.NoError(t, err) modules := make(map[string]api.Module) var lastInstantiatedModule api.Module for i := 0; i < len(base.Commands); i++ { c := &base.Commands[i] line := c.Line if mandatoryLine > -1 && c.Line == mandatoryLine { } else if line < lineBegin || line > lineEnd { continue } t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) { msg := fmt.Sprintf("%s:%d %s", wastName, c.Line, c.CommandType) switch c.CommandType { case "module": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) var registeredName string if next := i + 1; next < len(base.Commands) && base.Commands[next].CommandType == "register" { registeredName = base.Commands[next].As i++ // Skip the entire "register" command. } mod, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig().WithName(registeredName)) require.NoError(t, err, msg) if c.Name != "" { modules[c.Name] = mod } lastInstantiatedModule = mod case "assert_return", "action": m := lastInstantiatedModule if c.Action.Module != "" { m = modules[c.Action.Module] } switch c.Action.ActionType { case "invoke": args, exps := c.getAssertReturnArgsExps() msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) if c.Action.Module != "" { msg += " in module " + c.Action.Module } fn := m.ExportedFunction(c.Action.Field) results, err := fn.Call(ctx, args...) require.NoError(t, err, msg) require.Equal(t, len(exps), len(results), msg) laneTypes := map[int]string{} for i, expV := range c.Exps { if expV.ValType == "v128" { laneTypes[i] = expV.LaneType } } matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes) require.True(t, matched, msg+"\n"+valuesMsg) case "get": _, exps := c.getAssertReturnArgsExps() require.Equal(t, 1, len(exps)) msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) if c.Action.Module != "" { msg += " in module " + c.Action.Module } global := m.ExportedGlobal(c.Action.Field) require.NotNil(t, global) require.Equal(t, exps[0], global.Get(), msg) default: t.Fatalf("unsupported action type type: %v", c) } case "assert_malformed": if c.ModuleType != "text" { // We don't support direct loading of wast yet. buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) _, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig()) require.Error(t, err, msg) } case "assert_trap": m := lastInstantiatedModule if c.Action.Module != "" { m = modules[c.Action.Module] } switch c.Action.ActionType { case "invoke": args := c.getAssertReturnArgs() msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) if c.Action.Module != "" { msg += " in module " + c.Action.Module } _, err := m.ExportedFunction(c.Action.Field).Call(ctx, args...) require.ErrorIs(t, err, c.expectedError(), msg) default: t.Fatalf("unsupported action type type: %v", c) } case "assert_invalid": if c.ModuleType == "text" { // We don't support direct loading of wast yet. t.Skip() } buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) _, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig()) require.Error(t, err, msg) case "assert_exhaustion": switch c.Action.ActionType { case "invoke": args := c.getAssertReturnArgs() msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) if c.Action.Module != "" { msg += " in module " + c.Action.Module } _, err := lastInstantiatedModule.ExportedFunction(c.Action.Field).Call(ctx, args...) require.ErrorIs(t, err, wasmruntime.ErrRuntimeStackOverflow, msg) default: t.Fatalf("unsupported action type type: %v", c) } case "assert_unlinkable": if c.ModuleType == "text" { // We don't support direct loading of wast yet. t.Skip() } buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) _, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig()) require.Error(t, err, msg) case "assert_uninstantiable": buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) require.NoError(t, err, msg) _, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig()) if c.Text == "out of bounds table access" { // This is not actually an instantiation error, but assert_trap in the original wast, but wast2json translates it to assert_uninstantiable. // Anyway, this spectest case expects the error due to active element offset ouf of bounds // "after" instantiation while retaining function instances used for elements. // https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L264-L274 // // In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to // retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case. require.NoError(t, err, msg) } else { require.Error(t, err, msg) } default: t.Fatalf("unsupported command type: %s", c) } }) } }) } // basename avoids filepath.Base to ensure a forward slash is used even in Windows. // See https://pkg.go.dev/embed#hdr-Directives func basename(path string) string { lastSlash := strings.LastIndexByte(path, '/') return path[lastSlash+1:] } // testdataPath avoids filepath.Join to ensure a forward slash is used even in Windows. // See https://pkg.go.dev/embed#hdr-Directives func testdataPath(filename string) string { return fmt.Sprintf("testdata/%s", filename) } // valuesEq returns true if all the actual result matches exps which are all expressed as uint64. // - actual,exps: comparison target values which are all represented as uint64, meaning that if valTypes = [V128,I32], then // we have actual/exp = [(lower-64bit of the first V128), (higher-64bit of the first V128), I32]. // - valTypes holds the wasm.ValueType(s) of the original values in Wasm. // - laneTypes maps the index of valueTypes to laneType if valueTypes[i] == wasm.ValueTypeV128. // // Also, if matched == false this returns non-empty valuesMsg which can be used to augment the test failure message. func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType) (matched bool, valuesMsg string) { matched = true var msgExpValuesStrs, msgActualValuesStrs []string var uint64RepPos int // the index to actual and exps slice. for i, tp := range valTypes { switch tp { case wasm.ValueTypeI32: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", uint32(exps[uint64RepPos]))) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", uint32(actual[uint64RepPos]))) matched = matched && uint32(exps[uint64RepPos]) == uint32(actual[uint64RepPos]) uint64RepPos++ case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", exps[uint64RepPos])) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", actual[uint64RepPos])) matched = matched && exps[uint64RepPos] == actual[uint64RepPos] uint64RepPos++ case wasm.ValueTypeF32: a := math.Float32frombits(uint32(actual[uint64RepPos])) e := math.Float32frombits(uint32(exps[uint64RepPos])) msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e)) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a)) matched = matched && f32Equal(e, a) uint64RepPos++ case wasm.ValueTypeF64: e := math.Float64frombits(exps[uint64RepPos]) a := math.Float64frombits(actual[uint64RepPos]) msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e)) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a)) matched = matched && f64Equal(e, a) uint64RepPos++ case wasm.ValueTypeV128: actualLo, actualHi := actual[uint64RepPos], actual[uint64RepPos+1] expLo, expHi := exps[uint64RepPos], exps[uint64RepPos+1] switch laneTypes[i] { case laneTypeI8: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", byte(expLo), byte(expLo>>8), byte(expLo>>16), byte(expLo>>24), byte(expLo>>32), byte(expLo>>40), byte(expLo>>48), byte(expLo>>56), byte(expHi), byte(expHi>>8), byte(expHi>>16), byte(expHi>>24), byte(expHi>>32), byte(expHi>>40), byte(expHi>>48), byte(expHi>>56), ), ) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", byte(actualLo), byte(actualLo>>8), byte(actualLo>>16), byte(actualLo>>24), byte(actualLo>>32), byte(actualLo>>40), byte(actualLo>>48), byte(actualLo>>56), byte(actualHi), byte(actualHi>>8), byte(actualHi>>16), byte(actualHi>>24), byte(actualHi>>32), byte(actualHi>>40), byte(actualHi>>48), byte(actualHi>>56), ), ) matched = matched && (expLo == actualLo) && (expHi == actualHi) case laneTypeI16: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", uint16(expLo), uint16(expLo>>16), uint16(expLo>>32), uint16(expLo>>48), uint16(expHi), uint16(expHi>>16), uint16(expHi>>32), uint16(expHi>>48), ), ) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", uint16(actualLo), uint16(actualLo>>16), uint16(actualLo>>32), uint16(actualLo>>48), uint16(actualHi), uint16(actualHi>>16), uint16(actualHi>>32), uint16(actualHi>>48), ), ) matched = matched && (expLo == actualLo) && (expHi == actualHi) case laneTypeI32: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(expLo), uint32(expLo>>32), uint32(expHi), uint32(expHi>>32)), ) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(actualLo), uint32(actualLo>>32), uint32(actualHi), uint32(actualHi>>32)), ) matched = matched && (expLo == actualLo) && (expHi == actualHi) case laneTypeI64: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("i64x2(%#x, %#x)", expLo, expHi), ) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("i64x2(%#x, %#x)", actualLo, actualHi), ) matched = matched && (expLo == actualLo) && (expHi == actualHi) case laneTypeF32: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("f32x4(%f, %f, %f, %f)", math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(expLo>>32)), math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(expHi>>32)), ), ) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("f32x4(%f, %f, %f, %f)", math.Float32frombits(uint32(actualLo)), math.Float32frombits(uint32(actualLo>>32)), math.Float32frombits(uint32(actualHi)), math.Float32frombits(uint32(actualHi>>32)), ), ) matched = matched && f32Equal(math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(actualLo))) && f32Equal(math.Float32frombits(uint32(expLo>>32)), math.Float32frombits(uint32(actualLo>>32))) && f32Equal(math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(actualHi))) && f32Equal(math.Float32frombits(uint32(expHi>>32)), math.Float32frombits(uint32(actualHi>>32))) case laneTypeF64: msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(expLo), math.Float64frombits(expHi)), ) msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(actualLo), math.Float64frombits(actualHi)), ) matched = matched && f64Equal(math.Float64frombits(expLo), math.Float64frombits(actualLo)) && f64Equal(math.Float64frombits(expHi), math.Float64frombits(actualHi)) default: panic("BUG") } uint64RepPos += 2 default: panic("BUG") } } if !matched { valuesMsg = fmt.Sprintf("\thave [%s]\n\twant [%s]", strings.Join(msgActualValuesStrs, ", "), strings.Join(msgExpValuesStrs, ", ")) } return } func f32Equal(expected, actual float32) (matched bool) { if expBit := math.Float32bits(expected); expBit == moremath.F32CanonicalNaNBits { matched = math.Float32bits(actual)&moremath.F32CanonicalNaNBitsMask == moremath.F32CanonicalNaNBits } else if expBit == moremath.F32ArithmeticNaNBits { b := math.Float32bits(actual) matched = b&moremath.F32ExponentMask == moremath.F32ExponentMask && // Indicates that exponent part equals of NaN. b&moremath.F32ArithmeticNaNPayloadMSB == moremath.F32ArithmeticNaNPayloadMSB } else if math.IsNaN(float64(expected)) { // NaN cannot be compared with themselves, so we have to use IsNaN matched = math.IsNaN(float64(actual)) } else { // Compare the bit patterns directly, rather than == on float32 since in Go, -0 and 0 equals, // but in the Wasm spec, they are treated as different. matched = math.Float32bits(expected) == math.Float32bits(actual) } return } func f64Equal(expected, actual float64) (matched bool) { if expBit := math.Float64bits(expected); expBit == moremath.F64CanonicalNaNBits { matched = math.Float64bits(actual)&moremath.F64CanonicalNaNBitsMask == moremath.F64CanonicalNaNBits } else if expBit == moremath.F64ArithmeticNaNBits { b := math.Float64bits(actual) matched = b&moremath.F64ExponentMask == moremath.F64ExponentMask && // Indicates that exponent part equals of NaN. b&moremath.F64ArithmeticNaNPayloadMSB == moremath.F64ArithmeticNaNPayloadMSB } else if math.IsNaN(expected) { // NaN cannot be compared with themselves, so we have to use IsNaN matched = math.IsNaN(actual) } else { // Compare the bit patterns directly, rather than == on float64 since in Go, -0 and 0 equals, // but in the Wasm spec, they are treated as different. matched = math.Float64bits(expected) == math.Float64bits(actual) } return }