github.com/bananabytelabs/wazero@v0.0.0-20240105073314-54b22a776da8/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 "github.com/bananabytelabs/wazero" 14 "github.com/bananabytelabs/wazero/api" 15 "github.com/bananabytelabs/wazero/internal/moremath" 16 "github.com/bananabytelabs/wazero/internal/testing/require" 17 "github.com/bananabytelabs/wazero/internal/wasm" 18 "github.com/bananabytelabs/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/bananabytelabs/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 }