github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/integration_test/spectest/spectest.go (about)

     1  package spectest
     2  
     3  import (
     4  	"context"
     5  	"embed"
     6  	"encoding/json"
     7  	"fmt"
     8  	"math"
     9  	"strconv"
    10  	"strings"
    11  	"testing"
    12  
    13  	wazero "github.com/wasilibs/wazerox"
    14  	"github.com/wasilibs/wazerox/api"
    15  	"github.com/wasilibs/wazerox/internal/moremath"
    16  	"github.com/wasilibs/wazerox/internal/testing/require"
    17  	"github.com/wasilibs/wazerox/internal/wasm"
    18  	"github.com/wasilibs/wazerox/internal/wasmruntime"
    19  )
    20  
    21  type (
    22  	testbase struct {
    23  		SourceFile string    `json:"source_filename"`
    24  		Commands   []command `json:"commands"`
    25  	}
    26  	command struct {
    27  		CommandType string `json:"type"`
    28  		Line        int    `json:"line"`
    29  
    30  		// Set when type == "module" || "register"
    31  		Name string `json:"name,omitempty"`
    32  
    33  		// Set when type == "module" || "assert_uninstantiable" || "assert_malformed"
    34  		Filename string `json:"filename,omitempty"`
    35  
    36  		// Set when type == "register"
    37  		As string `json:"as,omitempty"`
    38  
    39  		// Set when type == "assert_return" || "action"
    40  		Action commandAction      `json:"action,omitempty"`
    41  		Exps   []commandActionVal `json:"expected"`
    42  
    43  		// Set when type == "assert_malformed"
    44  		ModuleType string `json:"module_type"`
    45  
    46  		// Set when type == "assert_trap"
    47  		Text string `json:"text"`
    48  	}
    49  
    50  	commandAction struct {
    51  		ActionType string             `json:"type"`
    52  		Args       []commandActionVal `json:"args"`
    53  
    54  		// Set when ActionType == "invoke"
    55  		Field  string `json:"field,omitempty"`
    56  		Module string `json:"module,omitempty"`
    57  	}
    58  
    59  	commandActionVal struct {
    60  		ValType string `json:"type"`
    61  		// LaneType is not empty if ValueType == "v128"
    62  		LaneType laneType    `json:"lane_type"`
    63  		Value    interface{} `json:"value"`
    64  	}
    65  )
    66  
    67  // laneType is a type of each lane of vector value.
    68  //
    69  // See https://github.com/WebAssembly/wabt/blob/main/docs/wast2json.md#const
    70  type laneType = string
    71  
    72  const (
    73  	laneTypeI8  laneType = "i8"
    74  	laneTypeI16 laneType = "i16"
    75  	laneTypeI32 laneType = "i32"
    76  	laneTypeI64 laneType = "i64"
    77  	laneTypeF32 laneType = "f32"
    78  	laneTypeF64 laneType = "f64"
    79  )
    80  
    81  func (c commandActionVal) String() string {
    82  	var v string
    83  	valTypeStr := c.ValType
    84  	switch c.ValType {
    85  	case "i32":
    86  		v = c.Value.(string)
    87  	case "f32":
    88  		str := c.Value.(string)
    89  		if strings.Contains(str, "nan") {
    90  			v = str
    91  		} else {
    92  			ret, _ := strconv.ParseUint(str, 10, 32)
    93  			v = fmt.Sprintf("%f", math.Float32frombits(uint32(ret)))
    94  		}
    95  	case "i64":
    96  		v = c.Value.(string)
    97  	case "f64":
    98  		str := c.Value.(string)
    99  		if strings.Contains(str, "nan") {
   100  			v = str
   101  		} else {
   102  			ret, _ := strconv.ParseUint(str, 10, 64)
   103  			v = fmt.Sprintf("%f", math.Float64frombits(ret))
   104  		}
   105  	case "externref":
   106  		if c.Value == "null" {
   107  			v = "null"
   108  		} else {
   109  			original, _ := strconv.ParseUint(c.Value.(string), 10, 64)
   110  			// In wazero, externref is opaque pointer, so "0" is considered as null.
   111  			// So in order to treat "externref 0" in spectest non nullref, we increment the value.
   112  			v = fmt.Sprintf("%d", original+1)
   113  		}
   114  	case "funcref":
   115  		// All the in and out funcref params are null in spectest (cannot represent non-null as it depends on runtime impl).
   116  		v = "null"
   117  	case "v128":
   118  		simdValues, ok := c.Value.([]interface{})
   119  		if !ok {
   120  			panic("BUG")
   121  		}
   122  		var strs []string
   123  		for _, v := range simdValues {
   124  			strs = append(strs, v.(string))
   125  		}
   126  		v = strings.Join(strs, ",")
   127  		valTypeStr = fmt.Sprintf("v128[lane=%s]", c.LaneType)
   128  	}
   129  	return fmt.Sprintf("{type: %s, value: %v}", valTypeStr, v)
   130  }
   131  
   132  func (c command) String() string {
   133  	msg := fmt.Sprintf("line: %d, type: %s", c.Line, c.CommandType)
   134  	switch c.CommandType {
   135  	case "register":
   136  		msg += fmt.Sprintf(", name: %s, as: %s", c.Name, c.As)
   137  	case "module":
   138  		if c.Name != "" {
   139  			msg += fmt.Sprintf(", name: %s, filename: %s", c.Name, c.Filename)
   140  		} else {
   141  			msg += fmt.Sprintf(", filename: %s", c.Filename)
   142  		}
   143  	case "assert_return", "action":
   144  		msg += fmt.Sprintf(", action type: %s", c.Action.ActionType)
   145  		if c.Action.Module != "" {
   146  			msg += fmt.Sprintf(", module: %s", c.Action.Module)
   147  		}
   148  		msg += fmt.Sprintf(", field: %s", c.Action.Field)
   149  		msg += fmt.Sprintf(", args: %v, expected: %v", c.Action.Args, c.Exps)
   150  	case "assert_malformed":
   151  		// TODO:
   152  	case "assert_trap":
   153  		msg += fmt.Sprintf(", args: %v, error text:  %s", c.Action.Args, c.Text)
   154  	case "assert_invalid":
   155  		// TODO:
   156  	case "assert_exhaustion":
   157  		// TODO:
   158  	case "assert_unlinkable":
   159  		// TODO:
   160  	case "assert_uninstantiable":
   161  		// TODO:
   162  	}
   163  	return "{" + msg + "}"
   164  }
   165  
   166  func (c command) getAssertReturnArgs() []uint64 {
   167  	var args []uint64
   168  	for _, arg := range c.Action.Args {
   169  		args = append(args, arg.toUint64s()...)
   170  	}
   171  	return args
   172  }
   173  
   174  func (c command) getAssertReturnArgsExps() (args []uint64, exps []uint64) {
   175  	for _, arg := range c.Action.Args {
   176  		args = append(args, arg.toUint64s()...)
   177  	}
   178  	for _, exp := range c.Exps {
   179  		exps = append(exps, exp.toUint64s()...)
   180  	}
   181  	return
   182  }
   183  
   184  func (c commandActionVal) toUint64s() (ret []uint64) {
   185  	if c.ValType == "v128" {
   186  		strValues, ok := c.Value.([]interface{})
   187  		if !ok {
   188  			panic("BUG")
   189  		}
   190  		var width, valNum int
   191  		switch c.LaneType {
   192  		case "i8":
   193  			width, valNum = 8, 16
   194  		case "i16":
   195  			width, valNum = 16, 8
   196  		case "i32":
   197  			width, valNum = 32, 4
   198  		case "i64":
   199  			width, valNum = 64, 2
   200  		case "f32":
   201  			width, valNum = 32, 4
   202  		case "f64":
   203  			width, valNum = 64, 2
   204  		default:
   205  			panic("BUG")
   206  		}
   207  		lo, hi := buildLaneUint64(strValues, width, valNum)
   208  		return []uint64{lo, hi}
   209  	} else {
   210  		return []uint64{c.toUint64()}
   211  	}
   212  }
   213  
   214  func buildLaneUint64(raw []interface{}, width, valNum int) (lo, hi uint64) {
   215  	for i := 0; i < valNum; i++ {
   216  		str := raw[i].(string)
   217  
   218  		var v uint64
   219  		var err error
   220  		if strings.Contains(str, "nan") {
   221  			v = getNaNBits(str, width == 32)
   222  		} else {
   223  			v, err = strconv.ParseUint(str, 10, width)
   224  			if err != nil {
   225  				panic(err)
   226  			}
   227  		}
   228  
   229  		if half := valNum / 2; i < half {
   230  			lo |= v << (i * width)
   231  		} else {
   232  			hi |= v << ((i - half) * width)
   233  		}
   234  	}
   235  	return
   236  }
   237  
   238  func getNaNBits(strValue string, is32bit bool) (ret uint64) {
   239  	// Note: nan:canonical, nan:arithmetic only appears on the expected values.
   240  	if is32bit {
   241  		switch strValue {
   242  		case "nan:canonical":
   243  			ret = uint64(moremath.F32CanonicalNaNBits)
   244  		case "nan:arithmetic":
   245  			ret = uint64(moremath.F32ArithmeticNaNBits)
   246  		default:
   247  			panic("BUG")
   248  		}
   249  	} else {
   250  		switch strValue {
   251  		case "nan:canonical":
   252  			ret = moremath.F64CanonicalNaNBits
   253  		case "nan:arithmetic":
   254  			ret = moremath.F64ArithmeticNaNBits
   255  		default:
   256  			panic("BUG")
   257  		}
   258  	}
   259  	return
   260  }
   261  
   262  func (c commandActionVal) toUint64() (ret uint64) {
   263  	strValue := c.Value.(string)
   264  	if strings.Contains(strValue, "nan") {
   265  		ret = getNaNBits(strValue, c.ValType == "f32")
   266  	} else if c.ValType == "externref" {
   267  		if c.Value == "null" {
   268  			ret = 0
   269  		} else {
   270  			original, _ := strconv.ParseUint(strValue, 10, 64)
   271  			// In wazero, externref is opaque pointer, so "0" is considered as null.
   272  			// So in order to treat "externref 0" in spectest non nullref, we increment the value.
   273  			ret = original + 1
   274  		}
   275  	} else if strings.Contains(c.ValType, "32") {
   276  		ret, _ = strconv.ParseUint(strValue, 10, 32)
   277  	} else {
   278  		ret, _ = strconv.ParseUint(strValue, 10, 64)
   279  	}
   280  	return
   281  }
   282  
   283  // expectedError returns the expected runtime error when the command type equals assert_trap
   284  // which expects engines to emit the errors corresponding command.Text field.
   285  func (c command) expectedError() (err error) {
   286  	if c.CommandType != "assert_trap" {
   287  		panic("unreachable")
   288  	}
   289  	switch c.Text {
   290  	case "expected shared memory":
   291  		err = wasmruntime.ErrRuntimeExpectedSharedMemory
   292  	case "out of bounds memory access":
   293  		err = wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess
   294  	case "indirect call type mismatch", "indirect call":
   295  		err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch
   296  	case "undefined element", "undefined", "out of bounds table access":
   297  		err = wasmruntime.ErrRuntimeInvalidTableAccess
   298  	case "integer overflow":
   299  		err = wasmruntime.ErrRuntimeIntegerOverflow
   300  	case "invalid conversion to integer":
   301  		err = wasmruntime.ErrRuntimeInvalidConversionToInteger
   302  	case "integer divide by zero":
   303  		err = wasmruntime.ErrRuntimeIntegerDivideByZero
   304  	case "unaligned atomic":
   305  		err = wasmruntime.ErrRuntimeUnalignedAtomic
   306  	case "unreachable":
   307  		err = wasmruntime.ErrRuntimeUnreachable
   308  	default:
   309  		if strings.HasPrefix(c.Text, "uninitialized") {
   310  			err = wasmruntime.ErrRuntimeInvalidTableAccess
   311  		}
   312  	}
   313  	return
   314  }
   315  
   316  // spectestWasm was generated by the following:
   317  //
   318  //	cd testdata; wat2wasm --debug-names spectest.wat
   319  //
   320  // This module is required by some test cases, and instantiated before running cases.
   321  // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast
   322  // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25
   323  //
   324  //go:embed testdata/spectest.wasm
   325  var spectestWasm []byte
   326  
   327  // Run runs all the test inside the testDataFS file system where all the cases are described
   328  // via JSON files created from wast2json.
   329  func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, config wazero.RuntimeConfig) {
   330  	files, err := testDataFS.ReadDir("testdata")
   331  	require.NoError(t, err)
   332  
   333  	caseNames := make([]string, 0, len(files))
   334  	for _, f := range files {
   335  		filename := f.Name()
   336  		if strings.HasSuffix(filename, ".json") {
   337  			caseNames = append(caseNames, strings.TrimSuffix(filename, ".json"))
   338  		}
   339  	}
   340  
   341  	// If the go:embed path resolution was wrong, this fails.
   342  	// https://github.com/tetratelabs/wazero/issues/247
   343  	require.True(t, len(caseNames) > 0, "len(caseNames)=%d (not greater than zero)", len(caseNames))
   344  
   345  	for _, f := range caseNames {
   346  		RunCase(t, testDataFS, f, ctx, config, -1, 0, math.MaxInt)
   347  	}
   348  }
   349  
   350  // RunCase runs the test case described by the given spectest file name (without .wast!) in the testDataFS file system.
   351  // lineBegin and lineEnd are the line numbers to run. If lineBegin == 0 and lineEnd == math.MaxInt, all the lines are run.
   352  //
   353  // For example, if you want to run memory_grow.wast:66 to 70, you can do:
   354  //
   355  //	RunCase(t, testDataFS, "memory_grow", ctx, config, mandatoryLine, 66, 70)
   356  //
   357  // where mandatoryLine is the line number which can be run regardless of the lineBegin and lineEnd. It is useful when
   358  // we only want to run specific command while running "module" command to instantiate a module. If you don't need it,
   359  // just pass -1.
   360  func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, config wazero.RuntimeConfig, mandatoryLine, lineBegin, lineEnd int) {
   361  	raw, err := testDataFS.ReadFile(testdataPath(f + ".json"))
   362  	require.NoError(t, err)
   363  
   364  	var base testbase
   365  	require.NoError(t, json.Unmarshal(raw, &base))
   366  
   367  	wastName := basename(base.SourceFile)
   368  
   369  	t.Run(wastName, func(t *testing.T) {
   370  		r := wazero.NewRuntimeWithConfig(ctx, config)
   371  		defer func() {
   372  			require.NoError(t, r.Close(ctx))
   373  		}()
   374  
   375  		_, err := r.InstantiateWithConfig(ctx, spectestWasm, wazero.NewModuleConfig())
   376  		require.NoError(t, err)
   377  
   378  		modules := make(map[string]api.Module)
   379  		var lastInstantiatedModule api.Module
   380  		for i := 0; i < len(base.Commands); i++ {
   381  			c := &base.Commands[i]
   382  			line := c.Line
   383  			if mandatoryLine > -1 && c.Line == mandatoryLine {
   384  			} else if line < lineBegin || line > lineEnd {
   385  				continue
   386  			}
   387  			t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) {
   388  				msg := fmt.Sprintf("%s:%d %s", wastName, c.Line, c.CommandType)
   389  				switch c.CommandType {
   390  				case "module":
   391  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   392  					require.NoError(t, err, msg)
   393  
   394  					var registeredName string
   395  					if next := i + 1; next < len(base.Commands) && base.Commands[next].CommandType == "register" {
   396  						registeredName = base.Commands[next].As
   397  						i++ // Skip the entire "register" command.
   398  					}
   399  					mod, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig().WithName(registeredName))
   400  					require.NoError(t, err, msg)
   401  					if c.Name != "" {
   402  						modules[c.Name] = mod
   403  					}
   404  					lastInstantiatedModule = mod
   405  				case "assert_return", "action":
   406  					m := lastInstantiatedModule
   407  					if c.Action.Module != "" {
   408  						m = modules[c.Action.Module]
   409  					}
   410  					switch c.Action.ActionType {
   411  					case "invoke":
   412  						args, exps := c.getAssertReturnArgsExps()
   413  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   414  						if c.Action.Module != "" {
   415  							msg += " in module " + c.Action.Module
   416  						}
   417  						fn := m.ExportedFunction(c.Action.Field)
   418  						results, err := fn.Call(ctx, args...)
   419  						require.NoError(t, err, msg)
   420  						require.Equal(t, len(exps), len(results), msg)
   421  						laneTypes := map[int]string{}
   422  						for i, expV := range c.Exps {
   423  							if expV.ValType == "v128" {
   424  								laneTypes[i] = expV.LaneType
   425  							}
   426  						}
   427  						matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes)
   428  						require.True(t, matched, msg+"\n"+valuesMsg)
   429  					case "get":
   430  						_, exps := c.getAssertReturnArgsExps()
   431  						require.Equal(t, 1, len(exps))
   432  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   433  						if c.Action.Module != "" {
   434  							msg += " in module " + c.Action.Module
   435  						}
   436  						global := m.ExportedGlobal(c.Action.Field)
   437  						require.NotNil(t, global)
   438  						require.Equal(t, exps[0], global.Get(), msg)
   439  					default:
   440  						t.Fatalf("unsupported action type type: %v", c)
   441  					}
   442  				case "assert_malformed":
   443  					if c.ModuleType != "text" {
   444  						// We don't support direct loading of wast yet.
   445  						buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   446  						require.NoError(t, err, msg)
   447  						_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   448  						require.Error(t, err, msg)
   449  					}
   450  				case "assert_trap":
   451  					m := lastInstantiatedModule
   452  					if c.Action.Module != "" {
   453  						m = modules[c.Action.Module]
   454  					}
   455  					switch c.Action.ActionType {
   456  					case "invoke":
   457  						args := c.getAssertReturnArgs()
   458  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   459  						if c.Action.Module != "" {
   460  							msg += " in module " + c.Action.Module
   461  						}
   462  						_, err := m.ExportedFunction(c.Action.Field).Call(ctx, args...)
   463  						require.ErrorIs(t, err, c.expectedError(), msg)
   464  					default:
   465  						t.Fatalf("unsupported action type type: %v", c)
   466  					}
   467  				case "assert_invalid":
   468  					if c.ModuleType == "text" {
   469  						// We don't support direct loading of wast yet.
   470  						t.Skip()
   471  					}
   472  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   473  					require.NoError(t, err, msg)
   474  					_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   475  					require.Error(t, err, msg)
   476  				case "assert_exhaustion":
   477  					switch c.Action.ActionType {
   478  					case "invoke":
   479  						args := c.getAssertReturnArgs()
   480  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   481  						if c.Action.Module != "" {
   482  							msg += " in module " + c.Action.Module
   483  						}
   484  						_, err := lastInstantiatedModule.ExportedFunction(c.Action.Field).Call(ctx, args...)
   485  						require.ErrorIs(t, err, wasmruntime.ErrRuntimeStackOverflow, msg)
   486  					default:
   487  						t.Fatalf("unsupported action type type: %v", c)
   488  					}
   489  				case "assert_unlinkable":
   490  					if c.ModuleType == "text" {
   491  						// We don't support direct loading of wast yet.
   492  						t.Skip()
   493  					}
   494  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   495  					require.NoError(t, err, msg)
   496  					_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   497  					require.Error(t, err, msg)
   498  				case "assert_uninstantiable":
   499  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   500  					require.NoError(t, err, msg)
   501  					_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   502  					if c.Text == "out of bounds table access" {
   503  						// This is not actually an instantiation error, but assert_trap in the original wast, but wast2json translates it to assert_uninstantiable.
   504  						// Anyway, this spectest case expects the error due to active element offset ouf of bounds
   505  						// "after" instantiation while retaining function instances used for elements.
   506  						// https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L264-L274
   507  						//
   508  						// In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to
   509  						// retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case.
   510  						require.NoError(t, err, msg)
   511  					} else {
   512  						require.Error(t, err, msg)
   513  					}
   514  				default:
   515  					t.Fatalf("unsupported command type: %s", c)
   516  				}
   517  			})
   518  		}
   519  	})
   520  }
   521  
   522  // basename avoids filepath.Base to ensure a forward slash is used even in Windows.
   523  // See https://pkg.go.dev/embed#hdr-Directives
   524  func basename(path string) string {
   525  	lastSlash := strings.LastIndexByte(path, '/')
   526  	return path[lastSlash+1:]
   527  }
   528  
   529  // testdataPath avoids filepath.Join to ensure a forward slash is used even in Windows.
   530  // See https://pkg.go.dev/embed#hdr-Directives
   531  func testdataPath(filename string) string {
   532  	return fmt.Sprintf("testdata/%s", filename)
   533  }
   534  
   535  // valuesEq returns true if all the actual result matches exps which are all expressed as uint64.
   536  //   - actual,exps: comparison target values which are all represented as uint64, meaning that if valTypes = [V128,I32], then
   537  //     we have actual/exp = [(lower-64bit of the first V128), (higher-64bit of the first V128), I32].
   538  //   - valTypes holds the wasm.ValueType(s) of the original values in Wasm.
   539  //   - laneTypes maps the index of valueTypes to laneType if valueTypes[i] == wasm.ValueTypeV128.
   540  //
   541  // Also, if matched == false this returns non-empty valuesMsg which can be used to augment the test failure message.
   542  func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType) (matched bool, valuesMsg string) {
   543  	matched = true
   544  
   545  	var msgExpValuesStrs, msgActualValuesStrs []string
   546  	var uint64RepPos int // the index to actual and exps slice.
   547  	for i, tp := range valTypes {
   548  		switch tp {
   549  		case wasm.ValueTypeI32:
   550  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", uint32(exps[uint64RepPos])))
   551  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", uint32(actual[uint64RepPos])))
   552  			matched = matched && uint32(exps[uint64RepPos]) == uint32(actual[uint64RepPos])
   553  			uint64RepPos++
   554  		case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
   555  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", exps[uint64RepPos]))
   556  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", actual[uint64RepPos]))
   557  			matched = matched && exps[uint64RepPos] == actual[uint64RepPos]
   558  			uint64RepPos++
   559  		case wasm.ValueTypeF32:
   560  			a := math.Float32frombits(uint32(actual[uint64RepPos]))
   561  			e := math.Float32frombits(uint32(exps[uint64RepPos]))
   562  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e))
   563  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a))
   564  			matched = matched && f32Equal(e, a)
   565  			uint64RepPos++
   566  		case wasm.ValueTypeF64:
   567  			e := math.Float64frombits(exps[uint64RepPos])
   568  			a := math.Float64frombits(actual[uint64RepPos])
   569  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e))
   570  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a))
   571  			matched = matched && f64Equal(e, a)
   572  			uint64RepPos++
   573  		case wasm.ValueTypeV128:
   574  			actualLo, actualHi := actual[uint64RepPos], actual[uint64RepPos+1]
   575  			expLo, expHi := exps[uint64RepPos], exps[uint64RepPos+1]
   576  			switch laneTypes[i] {
   577  			case laneTypeI8:
   578  				msgExpValuesStrs = append(msgExpValuesStrs,
   579  					fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   580  						byte(expLo), byte(expLo>>8), byte(expLo>>16), byte(expLo>>24),
   581  						byte(expLo>>32), byte(expLo>>40), byte(expLo>>48), byte(expLo>>56),
   582  						byte(expHi), byte(expHi>>8), byte(expHi>>16), byte(expHi>>24),
   583  						byte(expHi>>32), byte(expHi>>40), byte(expHi>>48), byte(expHi>>56),
   584  					),
   585  				)
   586  				msgActualValuesStrs = append(msgActualValuesStrs,
   587  					fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   588  						byte(actualLo), byte(actualLo>>8), byte(actualLo>>16), byte(actualLo>>24),
   589  						byte(actualLo>>32), byte(actualLo>>40), byte(actualLo>>48), byte(actualLo>>56),
   590  						byte(actualHi), byte(actualHi>>8), byte(actualHi>>16), byte(actualHi>>24),
   591  						byte(actualHi>>32), byte(actualHi>>40), byte(actualHi>>48), byte(actualHi>>56),
   592  					),
   593  				)
   594  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   595  			case laneTypeI16:
   596  				msgExpValuesStrs = append(msgExpValuesStrs,
   597  					fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   598  						uint16(expLo), uint16(expLo>>16), uint16(expLo>>32), uint16(expLo>>48),
   599  						uint16(expHi), uint16(expHi>>16), uint16(expHi>>32), uint16(expHi>>48),
   600  					),
   601  				)
   602  				msgActualValuesStrs = append(msgActualValuesStrs,
   603  					fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   604  						uint16(actualLo), uint16(actualLo>>16), uint16(actualLo>>32), uint16(actualLo>>48),
   605  						uint16(actualHi), uint16(actualHi>>16), uint16(actualHi>>32), uint16(actualHi>>48),
   606  					),
   607  				)
   608  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   609  			case laneTypeI32:
   610  				msgExpValuesStrs = append(msgExpValuesStrs,
   611  					fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(expLo), uint32(expLo>>32), uint32(expHi), uint32(expHi>>32)),
   612  				)
   613  				msgActualValuesStrs = append(msgActualValuesStrs,
   614  					fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(actualLo), uint32(actualLo>>32), uint32(actualHi), uint32(actualHi>>32)),
   615  				)
   616  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   617  			case laneTypeI64:
   618  				msgExpValuesStrs = append(msgExpValuesStrs,
   619  					fmt.Sprintf("i64x2(%#x, %#x)", expLo, expHi),
   620  				)
   621  				msgActualValuesStrs = append(msgActualValuesStrs,
   622  					fmt.Sprintf("i64x2(%#x, %#x)", actualLo, actualHi),
   623  				)
   624  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   625  			case laneTypeF32:
   626  				msgExpValuesStrs = append(msgExpValuesStrs,
   627  					fmt.Sprintf("f32x4(%f, %f, %f, %f)",
   628  						math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(expLo>>32)),
   629  						math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(expHi>>32)),
   630  					),
   631  				)
   632  				msgActualValuesStrs = append(msgActualValuesStrs,
   633  					fmt.Sprintf("f32x4(%f, %f, %f, %f)",
   634  						math.Float32frombits(uint32(actualLo)), math.Float32frombits(uint32(actualLo>>32)),
   635  						math.Float32frombits(uint32(actualHi)), math.Float32frombits(uint32(actualHi>>32)),
   636  					),
   637  				)
   638  				matched = matched &&
   639  					f32Equal(math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(actualLo))) &&
   640  					f32Equal(math.Float32frombits(uint32(expLo>>32)), math.Float32frombits(uint32(actualLo>>32))) &&
   641  					f32Equal(math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(actualHi))) &&
   642  					f32Equal(math.Float32frombits(uint32(expHi>>32)), math.Float32frombits(uint32(actualHi>>32)))
   643  			case laneTypeF64:
   644  				msgExpValuesStrs = append(msgExpValuesStrs,
   645  					fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(expLo), math.Float64frombits(expHi)),
   646  				)
   647  				msgActualValuesStrs = append(msgActualValuesStrs,
   648  					fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(actualLo), math.Float64frombits(actualHi)),
   649  				)
   650  				matched = matched &&
   651  					f64Equal(math.Float64frombits(expLo), math.Float64frombits(actualLo)) &&
   652  					f64Equal(math.Float64frombits(expHi), math.Float64frombits(actualHi))
   653  			default:
   654  				panic("BUG")
   655  			}
   656  			uint64RepPos += 2
   657  		default:
   658  			panic("BUG")
   659  		}
   660  	}
   661  
   662  	if !matched {
   663  		valuesMsg = fmt.Sprintf("\thave [%s]\n\twant [%s]",
   664  			strings.Join(msgActualValuesStrs, ", "),
   665  			strings.Join(msgExpValuesStrs, ", "))
   666  	}
   667  	return
   668  }
   669  
   670  func f32Equal(expected, actual float32) (matched bool) {
   671  	if expBit := math.Float32bits(expected); expBit == moremath.F32CanonicalNaNBits {
   672  		matched = math.Float32bits(actual)&moremath.F32CanonicalNaNBitsMask == moremath.F32CanonicalNaNBits
   673  	} else if expBit == moremath.F32ArithmeticNaNBits {
   674  		b := math.Float32bits(actual)
   675  		matched = b&moremath.F32ExponentMask == moremath.F32ExponentMask && // Indicates that exponent part equals of NaN.
   676  			b&moremath.F32ArithmeticNaNPayloadMSB == moremath.F32ArithmeticNaNPayloadMSB
   677  	} else if math.IsNaN(float64(expected)) { // NaN cannot be compared with themselves, so we have to use IsNaN
   678  		matched = math.IsNaN(float64(actual))
   679  	} else {
   680  		// Compare the bit patterns directly, rather than == on float32 since in Go, -0 and 0 equals,
   681  		// but in the Wasm spec, they are treated as different.
   682  		matched = math.Float32bits(expected) == math.Float32bits(actual)
   683  	}
   684  	return
   685  }
   686  
   687  func f64Equal(expected, actual float64) (matched bool) {
   688  	if expBit := math.Float64bits(expected); expBit == moremath.F64CanonicalNaNBits {
   689  		matched = math.Float64bits(actual)&moremath.F64CanonicalNaNBitsMask == moremath.F64CanonicalNaNBits
   690  	} else if expBit == moremath.F64ArithmeticNaNBits {
   691  		b := math.Float64bits(actual)
   692  		matched = b&moremath.F64ExponentMask == moremath.F64ExponentMask && // Indicates that exponent part equals of NaN.
   693  			b&moremath.F64ArithmeticNaNPayloadMSB == moremath.F64ArithmeticNaNPayloadMSB
   694  	} else if math.IsNaN(expected) { // NaN cannot be compared with themselves, so we have to use IsNaN
   695  		matched = math.IsNaN(actual)
   696  	} else {
   697  		// Compare the bit patterns directly, rather than == on float64 since in Go, -0 and 0 equals,
   698  		// but in the Wasm spec, they are treated as different.
   699  		matched = math.Float64bits(expected) == math.Float64bits(actual)
   700  	}
   701  	return
   702  }