...

Source file src/github.com/tetratelabs/wazero/internal/integration_test/spectest/spectest.go

Documentation: github.com/tetratelabs/wazero/internal/integration_test/spectest

     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  	"github.com/tetratelabs/wazero"
    14  	"github.com/tetratelabs/wazero/api"
    15  	"github.com/tetratelabs/wazero/internal/moremath"
    16  	"github.com/tetratelabs/wazero/internal/testing/require"
    17  	"github.com/tetratelabs/wazero/internal/wasm"
    18  	"github.com/tetratelabs/wazero/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 "out of bounds memory access":
   291  		err = wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess
   292  	case "indirect call type mismatch", "indirect call":
   293  		err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch
   294  	case "undefined element", "undefined", "out of bounds table access":
   295  		err = wasmruntime.ErrRuntimeInvalidTableAccess
   296  	case "integer overflow":
   297  		err = wasmruntime.ErrRuntimeIntegerOverflow
   298  	case "invalid conversion to integer":
   299  		err = wasmruntime.ErrRuntimeInvalidConversionToInteger
   300  	case "integer divide by zero":
   301  		err = wasmruntime.ErrRuntimeIntegerDivideByZero
   302  	case "unreachable":
   303  		err = wasmruntime.ErrRuntimeUnreachable
   304  	default:
   305  		if strings.HasPrefix(c.Text, "uninitialized") {
   306  			err = wasmruntime.ErrRuntimeInvalidTableAccess
   307  		}
   308  	}
   309  	return
   310  }
   311  
   312  // spectestWasm was generated by the following:
   313  //
   314  //	cd testdata; wat2wasm --debug-names spectest.wat
   315  //
   316  // This module is required by some test cases, and instantiated before running cases.
   317  // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast
   318  // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25
   319  //
   320  //go:embed testdata/spectest.wasm
   321  var spectestWasm []byte
   322  
   323  // Run runs all the test inside the testDataFS file system where all the cases are described
   324  // via JSON files created from wast2json.
   325  func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, config wazero.RuntimeConfig) {
   326  	files, err := testDataFS.ReadDir("testdata")
   327  	require.NoError(t, err)
   328  
   329  	caseNames := make([]string, 0, len(files))
   330  	for _, f := range files {
   331  		filename := f.Name()
   332  		if strings.HasSuffix(filename, ".json") {
   333  			caseNames = append(caseNames, strings.TrimSuffix(filename, ".json"))
   334  		}
   335  	}
   336  
   337  	// If the go:embed path resolution was wrong, this fails.
   338  	// https://github.com/tetratelabs/wazero/issues/247
   339  	require.True(t, len(caseNames) > 1, "len(caseNames)=%d (not greater than one)", len(caseNames))
   340  
   341  	for _, f := range caseNames {
   342  		RunCase(t, testDataFS, f, ctx, config, -1, 0, math.MaxInt)
   343  	}
   344  }
   345  
   346  // RunCase runs the test case described by the given spectest file name (without .wast!) in the testDataFS file system.
   347  // lineBegin and lineEnd are the line numbers to run. If lineBegin == 0 and lineEnd == math.MaxInt, all the lines are run.
   348  //
   349  // For example, if you want to run memory_grow.wast:66 to 70, you can do:
   350  //
   351  //	RunCase(t, testDataFS, "memory_grow", ctx, config, mandatoryLine, 66, 70)
   352  //
   353  // where mandatoryLine is the line number which can be run regardless of the lineBegin and lineEnd. It is useful when
   354  // we only want to run specific command while running "module" command to instantiate a module. If you don't need it,
   355  // just pass -1.
   356  func RunCase(t *testing.T, testDataFS embed.FS, f string, ctx context.Context, config wazero.RuntimeConfig, mandatoryLine, lineBegin, lineEnd int) {
   357  	raw, err := testDataFS.ReadFile(testdataPath(f + ".json"))
   358  	require.NoError(t, err)
   359  
   360  	var base testbase
   361  	require.NoError(t, json.Unmarshal(raw, &base))
   362  
   363  	wastName := basename(base.SourceFile)
   364  
   365  	t.Run(wastName, func(t *testing.T) {
   366  		r := wazero.NewRuntimeWithConfig(ctx, config)
   367  		defer func() {
   368  			require.NoError(t, r.Close(ctx))
   369  		}()
   370  
   371  		_, err := r.InstantiateWithConfig(ctx, spectestWasm, wazero.NewModuleConfig())
   372  		require.NoError(t, err)
   373  
   374  		modules := make(map[string]api.Module)
   375  		var lastInstantiatedModule api.Module
   376  		for i := 0; i < len(base.Commands); i++ {
   377  			c := &base.Commands[i]
   378  			line := c.Line
   379  			if mandatoryLine > -1 && c.Line == mandatoryLine {
   380  			} else if line < lineBegin || line > lineEnd {
   381  				continue
   382  			}
   383  			t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) {
   384  				msg := fmt.Sprintf("%s:%d %s", wastName, c.Line, c.CommandType)
   385  				switch c.CommandType {
   386  				case "module":
   387  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   388  					require.NoError(t, err, msg)
   389  
   390  					var registeredName string
   391  					if next := i + 1; next < len(base.Commands) && base.Commands[next].CommandType == "register" {
   392  						registeredName = base.Commands[next].As
   393  						i++ // Skip the entire "register" command.
   394  					}
   395  					mod, err := r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig().WithName(registeredName))
   396  					require.NoError(t, err, msg)
   397  					if c.Name != "" {
   398  						modules[c.Name] = mod
   399  					}
   400  					lastInstantiatedModule = mod
   401  				case "assert_return", "action":
   402  					m := lastInstantiatedModule
   403  					if c.Action.Module != "" {
   404  						m = modules[c.Action.Module]
   405  					}
   406  					switch c.Action.ActionType {
   407  					case "invoke":
   408  						args, exps := c.getAssertReturnArgsExps()
   409  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   410  						if c.Action.Module != "" {
   411  							msg += " in module " + c.Action.Module
   412  						}
   413  						fn := m.ExportedFunction(c.Action.Field)
   414  						results, err := fn.Call(ctx, args...)
   415  						require.NoError(t, err, msg)
   416  						require.Equal(t, len(exps), len(results), msg)
   417  						laneTypes := map[int]string{}
   418  						for i, expV := range c.Exps {
   419  							if expV.ValType == "v128" {
   420  								laneTypes[i] = expV.LaneType
   421  							}
   422  						}
   423  						matched, valuesMsg := valuesEq(results, exps, fn.Definition().ResultTypes(), laneTypes)
   424  						require.True(t, matched, msg+"\n"+valuesMsg)
   425  					case "get":
   426  						_, exps := c.getAssertReturnArgsExps()
   427  						require.Equal(t, 1, len(exps))
   428  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   429  						if c.Action.Module != "" {
   430  							msg += " in module " + c.Action.Module
   431  						}
   432  						global := m.ExportedGlobal(c.Action.Field)
   433  						require.NotNil(t, global)
   434  						require.Equal(t, exps[0], global.Get(), msg)
   435  					default:
   436  						t.Fatalf("unsupported action type type: %v", c)
   437  					}
   438  				case "assert_malformed":
   439  					if c.ModuleType != "text" {
   440  						// We don't support direct loading of wast yet.
   441  						buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   442  						require.NoError(t, err, msg)
   443  						_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   444  						require.Error(t, err, msg)
   445  					}
   446  				case "assert_trap":
   447  					m := lastInstantiatedModule
   448  					if c.Action.Module != "" {
   449  						m = modules[c.Action.Module]
   450  					}
   451  					switch c.Action.ActionType {
   452  					case "invoke":
   453  						args := c.getAssertReturnArgs()
   454  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   455  						if c.Action.Module != "" {
   456  							msg += " in module " + c.Action.Module
   457  						}
   458  						_, err := m.ExportedFunction(c.Action.Field).Call(ctx, args...)
   459  						require.ErrorIs(t, err, c.expectedError(), msg)
   460  					default:
   461  						t.Fatalf("unsupported action type type: %v", c)
   462  					}
   463  				case "assert_invalid":
   464  					if c.ModuleType == "text" {
   465  						// We don't support direct loading of wast yet.
   466  						t.Skip()
   467  					}
   468  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   469  					require.NoError(t, err, msg)
   470  					_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   471  					require.Error(t, err, msg)
   472  				case "assert_exhaustion":
   473  					switch c.Action.ActionType {
   474  					case "invoke":
   475  						args := c.getAssertReturnArgs()
   476  						msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args)
   477  						if c.Action.Module != "" {
   478  							msg += " in module " + c.Action.Module
   479  						}
   480  						_, err := lastInstantiatedModule.ExportedFunction(c.Action.Field).Call(ctx, args...)
   481  						require.ErrorIs(t, err, wasmruntime.ErrRuntimeStackOverflow, msg)
   482  					default:
   483  						t.Fatalf("unsupported action type type: %v", c)
   484  					}
   485  				case "assert_unlinkable":
   486  					if c.ModuleType == "text" {
   487  						// We don't support direct loading of wast yet.
   488  						t.Skip()
   489  					}
   490  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   491  					require.NoError(t, err, msg)
   492  					_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   493  					require.Error(t, err, msg)
   494  				case "assert_uninstantiable":
   495  					buf, err := testDataFS.ReadFile(testdataPath(c.Filename))
   496  					require.NoError(t, err, msg)
   497  					_, err = r.InstantiateWithConfig(ctx, buf, wazero.NewModuleConfig())
   498  					if c.Text == "out of bounds table access" {
   499  						// This is not actually an instantiation error, but assert_trap in the original wast, but wast2json translates it to assert_uninstantiable.
   500  						// Anyway, this spectest case expects the error due to active element offset ouf of bounds
   501  						// "after" instantiation while retaining function instances used for elements.
   502  						// https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L264-L274
   503  						//
   504  						// In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to
   505  						// retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case.
   506  						require.NoError(t, err, msg)
   507  					} else {
   508  						require.Error(t, err, msg)
   509  					}
   510  				default:
   511  					t.Fatalf("unsupported command type: %s", c)
   512  				}
   513  			})
   514  		}
   515  	})
   516  }
   517  
   518  // basename avoids filepath.Base to ensure a forward slash is used even in Windows.
   519  // See https://pkg.go.dev/embed#hdr-Directives
   520  func basename(path string) string {
   521  	lastSlash := strings.LastIndexByte(path, '/')
   522  	return path[lastSlash+1:]
   523  }
   524  
   525  // testdataPath avoids filepath.Join to ensure a forward slash is used even in Windows.
   526  // See https://pkg.go.dev/embed#hdr-Directives
   527  func testdataPath(filename string) string {
   528  	return fmt.Sprintf("testdata/%s", filename)
   529  }
   530  
   531  // valuesEq returns true if all the actual result matches exps which are all expressed as uint64.
   532  //   - actual,exps: comparison target values which are all represented as uint64, meaning that if valTypes = [V128,I32], then
   533  //     we have actual/exp = [(lower-64bit of the first V128), (higher-64bit of the first V128), I32].
   534  //   - valTypes holds the wasm.ValueType(s) of the original values in Wasm.
   535  //   - laneTypes maps the index of valueTypes to laneType if valueTypes[i] == wasm.ValueTypeV128.
   536  //
   537  // Also, if matched == false this returns non-empty valuesMsg which can be used to augment the test failure message.
   538  func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType) (matched bool, valuesMsg string) {
   539  	matched = true
   540  
   541  	var msgExpValuesStrs, msgActualValuesStrs []string
   542  	var uint64RepPos int // the index to actual and exps slice.
   543  	for i, tp := range valTypes {
   544  		switch tp {
   545  		case wasm.ValueTypeI32:
   546  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", uint32(exps[uint64RepPos])))
   547  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", uint32(actual[uint64RepPos])))
   548  			matched = matched && uint32(exps[uint64RepPos]) == uint32(actual[uint64RepPos])
   549  			uint64RepPos++
   550  		case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref:
   551  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", exps[uint64RepPos]))
   552  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", actual[uint64RepPos]))
   553  			matched = matched && exps[uint64RepPos] == actual[uint64RepPos]
   554  			uint64RepPos++
   555  		case wasm.ValueTypeF32:
   556  			a := math.Float32frombits(uint32(actual[uint64RepPos]))
   557  			e := math.Float32frombits(uint32(exps[uint64RepPos]))
   558  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e))
   559  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a))
   560  			matched = matched && f32Equal(e, a)
   561  			uint64RepPos++
   562  		case wasm.ValueTypeF64:
   563  			e := math.Float64frombits(exps[uint64RepPos])
   564  			a := math.Float64frombits(actual[uint64RepPos])
   565  			msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e))
   566  			msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a))
   567  			matched = matched && f64Equal(e, a)
   568  			uint64RepPos++
   569  		case wasm.ValueTypeV128:
   570  			actualLo, actualHi := actual[uint64RepPos], actual[uint64RepPos+1]
   571  			expLo, expHi := exps[uint64RepPos], exps[uint64RepPos+1]
   572  			switch laneTypes[i] {
   573  			case laneTypeI8:
   574  				msgExpValuesStrs = append(msgExpValuesStrs,
   575  					fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   576  						byte(expLo), byte(expLo>>8), byte(expLo>>16), byte(expLo>>24),
   577  						byte(expLo>>32), byte(expLo>>40), byte(expLo>>48), byte(expLo>>56),
   578  						byte(expHi), byte(expHi>>8), byte(expHi>>16), byte(expHi>>24),
   579  						byte(expHi>>32), byte(expHi>>40), byte(expHi>>48), byte(expHi>>56),
   580  					),
   581  				)
   582  				msgActualValuesStrs = append(msgActualValuesStrs,
   583  					fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   584  						byte(actualLo), byte(actualLo>>8), byte(actualLo>>16), byte(actualLo>>24),
   585  						byte(actualLo>>32), byte(actualLo>>40), byte(actualLo>>48), byte(actualLo>>56),
   586  						byte(actualHi), byte(actualHi>>8), byte(actualHi>>16), byte(actualHi>>24),
   587  						byte(actualHi>>32), byte(actualHi>>40), byte(actualHi>>48), byte(actualHi>>56),
   588  					),
   589  				)
   590  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   591  			case laneTypeI16:
   592  				msgExpValuesStrs = append(msgExpValuesStrs,
   593  					fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   594  						uint16(expLo), uint16(expLo>>16), uint16(expLo>>32), uint16(expLo>>48),
   595  						uint16(expHi), uint16(expHi>>16), uint16(expHi>>32), uint16(expHi>>48),
   596  					),
   597  				)
   598  				msgActualValuesStrs = append(msgActualValuesStrs,
   599  					fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)",
   600  						uint16(actualLo), uint16(actualLo>>16), uint16(actualLo>>32), uint16(actualLo>>48),
   601  						uint16(actualHi), uint16(actualHi>>16), uint16(actualHi>>32), uint16(actualHi>>48),
   602  					),
   603  				)
   604  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   605  			case laneTypeI32:
   606  				msgExpValuesStrs = append(msgExpValuesStrs,
   607  					fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(expLo), uint32(expLo>>32), uint32(expHi), uint32(expHi>>32)),
   608  				)
   609  				msgActualValuesStrs = append(msgActualValuesStrs,
   610  					fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(actualLo), uint32(actualLo>>32), uint32(actualHi), uint32(actualHi>>32)),
   611  				)
   612  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   613  			case laneTypeI64:
   614  				msgExpValuesStrs = append(msgExpValuesStrs,
   615  					fmt.Sprintf("i64x2(%#x, %#x)", expLo, expHi),
   616  				)
   617  				msgActualValuesStrs = append(msgActualValuesStrs,
   618  					fmt.Sprintf("i64x2(%#x, %#x)", actualLo, actualHi),
   619  				)
   620  				matched = matched && (expLo == actualLo) && (expHi == actualHi)
   621  			case laneTypeF32:
   622  				msgExpValuesStrs = append(msgExpValuesStrs,
   623  					fmt.Sprintf("f32x4(%f, %f, %f, %f)",
   624  						math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(expLo>>32)),
   625  						math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(expHi>>32)),
   626  					),
   627  				)
   628  				msgActualValuesStrs = append(msgActualValuesStrs,
   629  					fmt.Sprintf("f32x4(%f, %f, %f, %f)",
   630  						math.Float32frombits(uint32(actualLo)), math.Float32frombits(uint32(actualLo>>32)),
   631  						math.Float32frombits(uint32(actualHi)), math.Float32frombits(uint32(actualHi>>32)),
   632  					),
   633  				)
   634  				matched = matched &&
   635  					f32Equal(math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(actualLo))) &&
   636  					f32Equal(math.Float32frombits(uint32(expLo>>32)), math.Float32frombits(uint32(actualLo>>32))) &&
   637  					f32Equal(math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(actualHi))) &&
   638  					f32Equal(math.Float32frombits(uint32(expHi>>32)), math.Float32frombits(uint32(actualHi>>32)))
   639  			case laneTypeF64:
   640  				msgExpValuesStrs = append(msgExpValuesStrs,
   641  					fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(expLo), math.Float64frombits(expHi)),
   642  				)
   643  				msgActualValuesStrs = append(msgActualValuesStrs,
   644  					fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(actualLo), math.Float64frombits(actualHi)),
   645  				)
   646  				matched = matched &&
   647  					f64Equal(math.Float64frombits(expLo), math.Float64frombits(actualLo)) &&
   648  					f64Equal(math.Float64frombits(expHi), math.Float64frombits(actualHi))
   649  			default:
   650  				panic("BUG")
   651  			}
   652  			uint64RepPos += 2
   653  		default:
   654  			panic("BUG")
   655  		}
   656  	}
   657  
   658  	if !matched {
   659  		valuesMsg = fmt.Sprintf("\thave [%s]\n\twant [%s]",
   660  			strings.Join(msgActualValuesStrs, ", "),
   661  			strings.Join(msgExpValuesStrs, ", "))
   662  	}
   663  	return
   664  }
   665  
   666  func f32Equal(expected, actual float32) (matched bool) {
   667  	if expBit := math.Float32bits(expected); expBit == moremath.F32CanonicalNaNBits {
   668  		matched = math.Float32bits(actual)&moremath.F32CanonicalNaNBitsMask == moremath.F32CanonicalNaNBits
   669  	} else if expBit == moremath.F32ArithmeticNaNBits {
   670  		b := math.Float32bits(actual)
   671  		matched = b&moremath.F32ExponentMask == moremath.F32ExponentMask && // Indicates that exponent part equals of NaN.
   672  			b&moremath.F32ArithmeticNaNPayloadMSB == moremath.F32ArithmeticNaNPayloadMSB
   673  	} else if math.IsNaN(float64(expected)) { // NaN cannot be compared with themselves, so we have to use IsNaN
   674  		matched = math.IsNaN(float64(actual))
   675  	} else {
   676  		// Compare the bit patterns directly, rather than == on float32 since in Go, -0 and 0 equals,
   677  		// but in the Wasm spec, they are treated as different.
   678  		matched = math.Float32bits(expected) == math.Float32bits(actual)
   679  	}
   680  	return
   681  }
   682  
   683  func f64Equal(expected, actual float64) (matched bool) {
   684  	if expBit := math.Float64bits(expected); expBit == moremath.F64CanonicalNaNBits {
   685  		matched = math.Float64bits(actual)&moremath.F64CanonicalNaNBitsMask == moremath.F64CanonicalNaNBits
   686  	} else if expBit == moremath.F64ArithmeticNaNBits {
   687  		b := math.Float64bits(actual)
   688  		matched = b&moremath.F64ExponentMask == moremath.F64ExponentMask && // Indicates that exponent part equals of NaN.
   689  			b&moremath.F64ArithmeticNaNPayloadMSB == moremath.F64ArithmeticNaNPayloadMSB
   690  	} else if math.IsNaN(expected) { // NaN cannot be compared with themselves, so we have to use IsNaN
   691  		matched = math.IsNaN(actual)
   692  	} else {
   693  		// Compare the bit patterns directly, rather than == on float64 since in Go, -0 and 0 equals,
   694  		// but in the Wasm spec, they are treated as different.
   695  		matched = math.Float64bits(expected) == math.Float64bits(actual)
   696  	}
   697  	return
   698  }
   699  

View as plain text