wa-lang.org/wazero@v1.0.2/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 "wa-lang.org/wazero/api" 14 "wa-lang.org/wazero/internal/leb128" 15 "wa-lang.org/wazero/internal/moremath" 16 "wa-lang.org/wazero/internal/sys" 17 "wa-lang.org/wazero/internal/testing/require" 18 "wa-lang.org/wazero/internal/u64" 19 "wa-lang.org/wazero/internal/wasm" 20 binaryformat "wa-lang.org/wazero/internal/wasm/binary" 21 "wa-lang.org/wazero/internal/wasmruntime" 22 ) 23 24 // TODO: complete porting this to wazero API 25 type ( 26 testbase struct { 27 SourceFile string `json:"source_filename"` 28 Commands []command `json:"commands"` 29 } 30 command struct { 31 CommandType string `json:"type"` 32 Line int `json:"line"` 33 34 // Set when type == "module" || "register" 35 Name string `json:"name,omitempty"` 36 37 // Set when type == "module" || "assert_uninstantiable" || "assert_malformed" 38 Filename string `json:"filename,omitempty"` 39 40 // Set when type == "register" 41 As string `json:"as,omitempty"` 42 43 // Set when type == "assert_return" || "action" 44 Action commandAction `json:"action,omitempty"` 45 Exps []commandActionVal `json:"expected"` 46 47 // Set when type == "assert_malformed" 48 ModuleType string `json:"module_type"` 49 50 // Set when type == "assert_trap" 51 Text string `json:"text"` 52 } 53 54 commandAction struct { 55 ActionType string `json:"type"` 56 Args []commandActionVal `json:"args"` 57 58 // Set when ActionType == "invoke" 59 Field string `json:"field,omitempty"` 60 Module string `json:"module,omitempty"` 61 } 62 63 commandActionVal struct { 64 ValType string `json:"type"` 65 // LaneType is not empty if ValueType == "v128" 66 LaneType laneType `json:"lane_type"` 67 Value interface{} `json:"value"` 68 } 69 ) 70 71 // laneType is a type of each lane of vector value. 72 // 73 // See https://github.com/WebAssembly/wabt/blob/main/docs/wast2json.md#const 74 type laneType = string 75 76 const ( 77 laneTypeI8 laneType = "i8" 78 laneTypeI16 laneType = "i16" 79 laneTypeI32 laneType = "i32" 80 laneTypeI64 laneType = "i64" 81 laneTypeF32 laneType = "f32" 82 laneTypeF64 laneType = "f64" 83 ) 84 85 func (c commandActionVal) String() string { 86 var v string 87 valTypeStr := c.ValType 88 switch c.ValType { 89 case "i32": 90 v = c.Value.(string) 91 case "f32": 92 str := c.Value.(string) 93 if strings.Contains(str, "nan") { 94 v = str 95 } else { 96 ret, _ := strconv.ParseUint(str, 10, 32) 97 v = fmt.Sprintf("%f", math.Float32frombits(uint32(ret))) 98 } 99 case "i64": 100 v = c.Value.(string) 101 case "f64": 102 str := c.Value.(string) 103 if strings.Contains(str, "nan") { 104 v = str 105 } else { 106 ret, _ := strconv.ParseUint(str, 10, 64) 107 v = fmt.Sprintf("%f", math.Float64frombits(ret)) 108 } 109 case "externref": 110 if c.Value == "null" { 111 v = "null" 112 } else { 113 original, _ := strconv.ParseUint(c.Value.(string), 10, 64) 114 // In wazero, externref is opaque pointer, so "0" is considered as null. 115 // So in order to treat "externref 0" in spectest non nullref, we increment the value. 116 v = fmt.Sprintf("%d", original+1) 117 } 118 case "funcref": 119 // All the in and out funcref params are null in spectest (cannot represent non-null as it depends on runtime impl). 120 v = "null" 121 case "v128": 122 simdValues, ok := c.Value.([]interface{}) 123 if !ok { 124 panic("BUG") 125 } 126 var strs []string 127 for _, v := range simdValues { 128 strs = append(strs, v.(string)) 129 } 130 v = strings.Join(strs, ",") 131 valTypeStr = fmt.Sprintf("v128[lane=%s]", c.LaneType) 132 } 133 return fmt.Sprintf("{type: %s, value: %v}", valTypeStr, v) 134 } 135 136 func (c command) String() string { 137 msg := fmt.Sprintf("line: %d, type: %s", c.Line, c.CommandType) 138 switch c.CommandType { 139 case "register": 140 msg += fmt.Sprintf(", name: %s, as: %s", c.Name, c.As) 141 case "module": 142 if c.Name != "" { 143 msg += fmt.Sprintf(", name: %s, filename: %s", c.Name, c.Filename) 144 } else { 145 msg += fmt.Sprintf(", filename: %s", c.Filename) 146 } 147 case "assert_return", "action": 148 msg += fmt.Sprintf(", action type: %s", c.Action.ActionType) 149 if c.Action.Module != "" { 150 msg += fmt.Sprintf(", module: %s", c.Action.Module) 151 } 152 msg += fmt.Sprintf(", field: %s", c.Action.Field) 153 msg += fmt.Sprintf(", args: %v, expected: %v", c.Action.Args, c.Exps) 154 case "assert_malformed": 155 // TODO: 156 case "assert_trap": 157 msg += fmt.Sprintf(", args: %v, error text: %s", c.Action.Args, c.Text) 158 case "assert_invalid": 159 // TODO: 160 case "assert_exhaustion": 161 // TODO: 162 case "assert_unlinkable": 163 // TODO: 164 case "assert_uninstantiable": 165 // TODO: 166 } 167 return "{" + msg + "}" 168 } 169 170 func (c command) getAssertReturnArgs() []uint64 { 171 var args []uint64 172 for _, arg := range c.Action.Args { 173 args = append(args, arg.toUint64s()...) 174 } 175 return args 176 } 177 178 func (c command) getAssertReturnArgsExps() (args []uint64, exps []uint64) { 179 for _, arg := range c.Action.Args { 180 args = append(args, arg.toUint64s()...) 181 } 182 for _, exp := range c.Exps { 183 exps = append(exps, exp.toUint64s()...) 184 } 185 return 186 } 187 188 func (c commandActionVal) toUint64s() (ret []uint64) { 189 if c.ValType == "v128" { 190 strValues, ok := c.Value.([]interface{}) 191 if !ok { 192 panic("BUG") 193 } 194 var width, valNum int 195 switch c.LaneType { 196 case "i8": 197 width, valNum = 8, 16 198 case "i16": 199 width, valNum = 16, 8 200 case "i32": 201 width, valNum = 32, 4 202 case "i64": 203 width, valNum = 64, 2 204 case "f32": 205 width, valNum = 32, 4 206 case "f64": 207 width, valNum = 64, 2 208 default: 209 panic("BUG") 210 } 211 lo, hi := buildLaneUint64(strValues, width, valNum) 212 return []uint64{lo, hi} 213 } else { 214 return []uint64{c.toUint64()} 215 } 216 } 217 218 func buildLaneUint64(raw []interface{}, width, valNum int) (lo, hi uint64) { 219 for i := 0; i < valNum; i++ { 220 str := raw[i].(string) 221 222 var v uint64 223 var err error 224 if strings.Contains(str, "nan") { 225 v = getNaNBits(str, width == 32) 226 } else { 227 v, err = strconv.ParseUint(str, 10, width) 228 if err != nil { 229 panic(err) 230 } 231 } 232 233 if half := valNum / 2; i < half { 234 lo |= v << (i * width) 235 } else { 236 hi |= v << ((i - half) * width) 237 } 238 } 239 return 240 } 241 242 func getNaNBits(strValue string, is32bit bool) (ret uint64) { 243 // Note: nan:canonical, nan:arithmetic only appears on the expected values. 244 if is32bit { 245 switch strValue { 246 case "nan:canonical": 247 ret = uint64(moremath.F32CanonicalNaNBits) 248 case "nan:arithmetic": 249 ret = uint64(moremath.F32ArithmeticNaNBits) 250 default: 251 panic("BUG") 252 } 253 } else { 254 switch strValue { 255 case "nan:canonical": 256 ret = moremath.F64CanonicalNaNBits 257 case "nan:arithmetic": 258 ret = moremath.F64ArithmeticNaNBits 259 default: 260 panic("BUG") 261 } 262 } 263 return 264 } 265 266 func (c commandActionVal) toUint64() (ret uint64) { 267 strValue := c.Value.(string) 268 if strings.Contains(strValue, "nan") { 269 ret = getNaNBits(strValue, c.ValType == "f32") 270 } else if c.ValType == "externref" { 271 if c.Value == "null" { 272 ret = 0 273 } else { 274 original, _ := strconv.ParseUint(strValue, 10, 64) 275 // In wazero, externref is opaque pointer, so "0" is considered as null. 276 // So in order to treat "externref 0" in spectest non nullref, we increment the value. 277 ret = original + 1 278 } 279 } else if strings.Contains(c.ValType, "32") { 280 ret, _ = strconv.ParseUint(strValue, 10, 32) 281 } else { 282 ret, _ = strconv.ParseUint(strValue, 10, 64) 283 } 284 return 285 } 286 287 // expectedError returns the expected runtime error when the command type equals assert_trap 288 // which expects engines to emit the errors corresponding command.Text field. 289 func (c command) expectedError() (err error) { 290 if c.CommandType != "assert_trap" { 291 panic("unreachable") 292 } 293 switch c.Text { 294 case "out of bounds memory access": 295 err = wasmruntime.ErrRuntimeOutOfBoundsMemoryAccess 296 case "indirect call type mismatch", "indirect call": 297 err = wasmruntime.ErrRuntimeIndirectCallTypeMismatch 298 case "undefined element", "undefined", "out of bounds table access": 299 err = wasmruntime.ErrRuntimeInvalidTableAccess 300 case "integer overflow": 301 err = wasmruntime.ErrRuntimeIntegerOverflow 302 case "invalid conversion to integer": 303 err = wasmruntime.ErrRuntimeInvalidConversionToInteger 304 case "integer divide by zero": 305 err = wasmruntime.ErrRuntimeIntegerDivideByZero 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 //go:embed testdata/spectest.wasm 321 var spectestWasm []byte 322 323 // addSpectestModule adds a module that drops inputs and returns globals as 666 per the default test harness. 324 // 325 // See https://github.com/WebAssembly/spec/blob/wg-1.0/test/core/imports.wast 326 // See https://github.com/WebAssembly/spec/blob/wg-1.0/interpreter/script/js.ml#L13-L25 327 func addSpectestModule(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, enabledFeatures api.CoreFeatures) { 328 mod, err := binaryformat.DecodeModule(spectestWasm, api.CoreFeaturesV2, wasm.MemoryLimitPages, false) 329 require.NoError(t, err) 330 331 // (global (export "global_i32") i32 (i32.const 666)) 332 mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{ 333 Type: &wasm.GlobalType{ValType: wasm.ValueTypeI32}, 334 Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI32Const, Data: leb128.EncodeInt32(666)}, 335 }) 336 mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "global_i32", Index: 0, Type: wasm.ExternTypeGlobal}) 337 338 // (global (export "global_i64") i64 (i32.const 666)) 339 mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{ 340 Type: &wasm.GlobalType{ValType: wasm.ValueTypeI64}, 341 Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeI64Const, Data: leb128.EncodeInt32(666)}, 342 }) 343 mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "global_i64", Index: 1, Type: wasm.ExternTypeGlobal}) 344 345 // (global (export "global_f32") f32 (f32.const 666)) 346 mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{ 347 Type: &wasm.GlobalType{ValType: wasm.ValueTypeF32}, 348 Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF32Const, Data: u64.LeBytes(api.EncodeF32(666))}, 349 }) 350 mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "global_f32", Index: 2, Type: wasm.ExternTypeGlobal}) 351 352 // (global (export "global_f64") f64 (f64.const 666)) 353 mod.GlobalSection = append(mod.GlobalSection, &wasm.Global{ 354 Type: &wasm.GlobalType{ValType: wasm.ValueTypeF64}, 355 Init: &wasm.ConstantExpression{Opcode: wasm.OpcodeF64Const, Data: u64.LeBytes(api.EncodeF64(666))}, 356 }) 357 mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "global_f64", Index: 3, Type: wasm.ExternTypeGlobal}) 358 359 // (table (export "table") 10 20 funcref) 360 tableLimitMax := uint32(20) 361 mod.TableSection = []*wasm.Table{{Min: 10, Max: &tableLimitMax, Type: wasm.RefTypeFuncref}} 362 mod.ExportSection = append(mod.ExportSection, &wasm.Export{Name: "table", Index: 0, Type: wasm.ExternTypeTable}) 363 364 maybeSetMemoryCap(mod) 365 mod.BuildFunctionDefinitions() 366 367 err = mod.Validate(enabledFeatures) 368 require.NoError(t, err) 369 370 err = s.Engine.CompileModule(ctx, mod, nil) 371 require.NoError(t, err) 372 373 _, err = s.Instantiate(ctx, ns, mod, mod.NameSection.ModuleName, sys.DefaultContext(nil)) 374 require.NoError(t, err) 375 } 376 377 // maybeSetMemoryCap assigns wasm.Memory Cap to Min, which is what wazero.CompileModule would do. 378 func maybeSetMemoryCap(mod *wasm.Module) { 379 if mem := mod.MemorySection; mem != nil { 380 mem.Cap = mem.Min 381 } 382 } 383 384 // Run runs all the test inside the testDataFS file system where all the cases are described 385 // via JSON files created from wast2json. 386 func Run(t *testing.T, testDataFS embed.FS, ctx context.Context, newEngine func(context.Context, api.CoreFeatures) wasm.Engine, enabledFeatures api.CoreFeatures) { 387 files, err := testDataFS.ReadDir("testdata") 388 require.NoError(t, err) 389 390 jsonfiles := make([]string, 0, len(files)) 391 for _, f := range files { 392 filename := f.Name() 393 if strings.HasSuffix(filename, ".json") { 394 jsonfiles = append(jsonfiles, testdataPath(filename)) 395 } 396 } 397 398 // If the go:embed path resolution was wrong, this fails. 399 // https://github.com/tetratelabs/wazero/issues/247 400 require.True(t, len(jsonfiles) > 1, "len(jsonfiles)=%d (not greater than one)", len(jsonfiles)) 401 402 for _, f := range jsonfiles { 403 raw, err := testDataFS.ReadFile(f) 404 require.NoError(t, err) 405 406 var base testbase 407 require.NoError(t, json.Unmarshal(raw, &base)) 408 409 wastName := basename(base.SourceFile) 410 411 t.Run(wastName, func(t *testing.T) { 412 s, ns := wasm.NewStore(enabledFeatures, newEngine(ctx, enabledFeatures)) 413 addSpectestModule(t, ctx, s, ns, enabledFeatures) 414 415 var lastInstantiatedModuleName string 416 for _, c := range base.Commands { 417 t.Run(fmt.Sprintf("%s/line:%d", c.CommandType, c.Line), func(t *testing.T) { 418 msg := fmt.Sprintf("%s:%d %s", wastName, c.Line, c.CommandType) 419 switch c.CommandType { 420 case "module": 421 buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) 422 require.NoError(t, err, msg) 423 mod, err := binaryformat.DecodeModule(buf, enabledFeatures, wasm.MemoryLimitPages, false) 424 require.NoError(t, err, msg) 425 require.NoError(t, mod.Validate(enabledFeatures)) 426 mod.AssignModuleID(buf) 427 428 moduleName := c.Name 429 if moduleName == "" { 430 // Use the file name as the name. 431 moduleName = c.Filename 432 } 433 434 maybeSetMemoryCap(mod) 435 mod.BuildFunctionDefinitions() 436 err = s.Engine.CompileModule(ctx, mod, nil) 437 require.NoError(t, err, msg) 438 439 _, err = s.Instantiate(ctx, ns, mod, moduleName, nil) 440 lastInstantiatedModuleName = moduleName 441 require.NoError(t, err) 442 case "register": 443 src := c.Name 444 if src == "" { 445 src = lastInstantiatedModuleName 446 } 447 ns.AliasModule(src, c.As) 448 lastInstantiatedModuleName = c.As 449 case "assert_return", "action": 450 moduleName := lastInstantiatedModuleName 451 if c.Action.Module != "" { 452 moduleName = c.Action.Module 453 } 454 switch c.Action.ActionType { 455 case "invoke": 456 args, exps := c.getAssertReturnArgsExps() 457 msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) 458 if c.Action.Module != "" { 459 msg += " in module " + c.Action.Module 460 } 461 vals, types, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...) 462 require.NoError(t, err, msg) 463 require.Equal(t, len(exps), len(vals), msg) 464 laneTypes := map[int]string{} 465 for i, expV := range c.Exps { 466 if expV.ValType == "v128" { 467 laneTypes[i] = expV.LaneType 468 } 469 } 470 matched, valuesMsg := valuesEq(vals, exps, types, laneTypes) 471 require.True(t, matched, msg+"\n"+valuesMsg) 472 case "get": 473 _, exps := c.getAssertReturnArgsExps() 474 require.Equal(t, 1, len(exps)) 475 msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) 476 if c.Action.Module != "" { 477 msg += " in module " + c.Action.Module 478 } 479 module := ns.Module(moduleName) 480 require.NotNil(t, module) 481 global := module.ExportedGlobal(c.Action.Field) 482 require.NotNil(t, global) 483 var expType wasm.ValueType 484 switch c.Exps[0].ValType { 485 case "i32": 486 expType = wasm.ValueTypeI32 487 case "i64": 488 expType = wasm.ValueTypeI64 489 case "f32": 490 expType = wasm.ValueTypeF32 491 case "f64": 492 expType = wasm.ValueTypeF64 493 } 494 require.Equal(t, expType, global.Type(), msg) 495 require.Equal(t, exps[0], global.Get(ctx), msg) 496 default: 497 t.Fatalf("unsupported action type type: %v", c) 498 } 499 case "assert_malformed": 500 if c.ModuleType != "text" { 501 // We don't support direct loading of wast yet. 502 buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) 503 require.NoError(t, err, msg) 504 requireInstantiationError(t, ctx, s, ns, buf, msg) 505 } 506 case "assert_trap": 507 moduleName := lastInstantiatedModuleName 508 if c.Action.Module != "" { 509 moduleName = c.Action.Module 510 } 511 switch c.Action.ActionType { 512 case "invoke": 513 args := c.getAssertReturnArgs() 514 msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) 515 if c.Action.Module != "" { 516 msg += " in module " + c.Action.Module 517 } 518 _, _, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...) 519 require.ErrorIs(t, err, c.expectedError(), msg) 520 default: 521 t.Fatalf("unsupported action type type: %v", c) 522 } 523 case "assert_invalid": 524 if c.ModuleType == "text" { 525 // We don't support direct loading of wast yet. 526 t.Skip() 527 } 528 buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) 529 require.NoError(t, err, msg) 530 requireInstantiationError(t, ctx, s, ns, buf, msg) 531 case "assert_exhaustion": 532 moduleName := lastInstantiatedModuleName 533 switch c.Action.ActionType { 534 case "invoke": 535 args := c.getAssertReturnArgs() 536 msg = fmt.Sprintf("%s invoke %s (%s)", msg, c.Action.Field, c.Action.Args) 537 if c.Action.Module != "" { 538 msg += " in module " + c.Action.Module 539 } 540 _, _, err := callFunction(ns, ctx, moduleName, c.Action.Field, args...) 541 require.ErrorIs(t, err, wasmruntime.ErrRuntimeStackOverflow, msg) 542 default: 543 t.Fatalf("unsupported action type type: %v", c) 544 } 545 case "assert_unlinkable": 546 if c.ModuleType == "text" { 547 // We don't support direct loading of wast yet. 548 t.Skip() 549 } 550 buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) 551 require.NoError(t, err, msg) 552 requireInstantiationError(t, ctx, s, ns, buf, msg) 553 case "assert_uninstantiable": 554 buf, err := testDataFS.ReadFile(testdataPath(c.Filename)) 555 require.NoError(t, err, msg) 556 if c.Text == "out of bounds table access" { 557 // This is not actually an instantiation error, but assert_trap in the original wast, but wast2json translates it to assert_uninstantiable. 558 // Anyway, this spectest case expects the error due to active element offset ouf of bounds 559 // "after" instantiation while retaining function instances used for elements. 560 // https://github.com/WebAssembly/spec/blob/d39195773112a22b245ffbe864bab6d1182ccb06/test/core/linking.wast#L264-L274 561 // 562 // In practice, such a module instance can be used for invoking functions without any issue. In addition, we have to 563 // retain functions after the expected "instantiation" failure, so in wazero we choose to not raise error in that case. 564 mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false) 565 require.NoError(t, err, msg) 566 567 err = mod.Validate(s.EnabledFeatures) 568 require.NoError(t, err, msg) 569 570 mod.AssignModuleID(buf) 571 572 maybeSetMemoryCap(mod) 573 mod.BuildFunctionDefinitions() 574 err = s.Engine.CompileModule(ctx, mod, nil) 575 require.NoError(t, err, msg) 576 577 _, err = s.Instantiate(ctx, ns, mod, t.Name(), nil) 578 require.NoError(t, err, msg) 579 } else { 580 requireInstantiationError(t, ctx, s, ns, buf, msg) 581 } 582 583 default: 584 t.Fatalf("unsupported command type: %s", c) 585 } 586 }) 587 } 588 }) 589 } 590 } 591 592 func requireInstantiationError(t *testing.T, ctx context.Context, s *wasm.Store, ns *wasm.Namespace, buf []byte, msg string) { 593 mod, err := binaryformat.DecodeModule(buf, s.EnabledFeatures, wasm.MemoryLimitPages, false) 594 if err != nil { 595 return 596 } 597 598 err = mod.Validate(s.EnabledFeatures) 599 if err != nil { 600 return 601 } 602 603 mod.AssignModuleID(buf) 604 605 maybeSetMemoryCap(mod) 606 mod.BuildFunctionDefinitions() 607 err = s.Engine.CompileModule(ctx, mod, nil) 608 if err != nil { 609 return 610 } 611 612 _, err = s.Instantiate(ctx, ns, mod, t.Name(), nil) 613 require.Error(t, err, msg) 614 } 615 616 // basename avoids filepath.Base to ensure a forward slash is used even in Windows. 617 // See https://pkg.go.dev/embed#hdr-Directives 618 func basename(path string) string { 619 lastSlash := strings.LastIndexByte(path, '/') 620 return path[lastSlash+1:] 621 } 622 623 // testdataPath avoids filepath.Join to ensure a forward slash is used even in Windows. 624 // See https://pkg.go.dev/embed#hdr-Directives 625 func testdataPath(filename string) string { 626 return fmt.Sprintf("testdata/%s", filename) 627 } 628 629 // valuesEq returns true if all the actual result matches exps which are all expressed as uint64. 630 // - actual,exps: comparison target values which are all represented as uint64, meaning that if valTypes = [V128,I32], then 631 // we have actual/exp = [(lower-64bit of the first V128), (higher-64bit of the first V128), I32]. 632 // - valTypes holds the wasm.ValueType(s) of the original values in Wasm. 633 // - laneTypes maps the index of valueTypes to laneType if valueTypes[i] == wasm.ValueTypeV128. 634 // 635 // Also, if matched == false this returns non-empty valuesMsg which can be used to augment the test failure message. 636 func valuesEq(actual, exps []uint64, valTypes []wasm.ValueType, laneTypes map[int]laneType) (matched bool, valuesMsg string) { 637 matched = true 638 639 var msgExpValuesStrs, msgActualValuesStrs []string 640 var uint64RepPos int // the index to actual and exps slice. 641 for i, tp := range valTypes { 642 switch tp { 643 case wasm.ValueTypeI32: 644 msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", uint32(exps[uint64RepPos]))) 645 msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", uint32(actual[uint64RepPos]))) 646 matched = matched && uint32(exps[uint64RepPos]) == uint32(actual[uint64RepPos]) 647 uint64RepPos++ 648 case wasm.ValueTypeI64, wasm.ValueTypeExternref, wasm.ValueTypeFuncref: 649 msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%d", exps[uint64RepPos])) 650 msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%d", actual[uint64RepPos])) 651 matched = matched && exps[uint64RepPos] == actual[uint64RepPos] 652 uint64RepPos++ 653 case wasm.ValueTypeF32: 654 a := math.Float32frombits(uint32(actual[uint64RepPos])) 655 e := math.Float32frombits(uint32(exps[uint64RepPos])) 656 msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e)) 657 msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a)) 658 matched = matched && f32Equal(e, a) 659 uint64RepPos++ 660 case wasm.ValueTypeF64: 661 e := math.Float64frombits(exps[uint64RepPos]) 662 a := math.Float64frombits(actual[uint64RepPos]) 663 msgExpValuesStrs = append(msgExpValuesStrs, fmt.Sprintf("%f", e)) 664 msgActualValuesStrs = append(msgActualValuesStrs, fmt.Sprintf("%f", a)) 665 matched = matched && f64Equal(e, a) 666 uint64RepPos++ 667 case wasm.ValueTypeV128: 668 actualLo, actualHi := actual[uint64RepPos], actual[uint64RepPos+1] 669 expLo, expHi := exps[uint64RepPos], exps[uint64RepPos+1] 670 switch laneTypes[i] { 671 case laneTypeI8: 672 msgExpValuesStrs = append(msgExpValuesStrs, 673 fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", 674 byte(expLo), byte(expLo>>8), byte(expLo>>16), byte(expLo>>24), 675 byte(expLo>>32), byte(expLo>>40), byte(expLo>>48), byte(expLo>>56), 676 byte(expHi), byte(expHi>>8), byte(expHi>>16), byte(expHi>>24), 677 byte(expHi>>32), byte(expHi>>40), byte(expHi>>48), byte(expHi>>56), 678 ), 679 ) 680 msgActualValuesStrs = append(msgActualValuesStrs, 681 fmt.Sprintf("i8x16(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", 682 byte(actualLo), byte(actualLo>>8), byte(actualLo>>16), byte(actualLo>>24), 683 byte(actualLo>>32), byte(actualLo>>40), byte(actualLo>>48), byte(actualLo>>56), 684 byte(actualHi), byte(actualHi>>8), byte(actualHi>>16), byte(actualHi>>24), 685 byte(actualHi>>32), byte(actualHi>>40), byte(actualHi>>48), byte(actualHi>>56), 686 ), 687 ) 688 matched = matched && (expLo == actualLo) && (expHi == actualHi) 689 case laneTypeI16: 690 msgExpValuesStrs = append(msgExpValuesStrs, 691 fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", 692 uint16(expLo), uint16(expLo>>16), uint16(expLo>>32), uint16(expLo>>48), 693 uint16(expHi), uint16(expHi>>16), uint16(expHi>>32), uint16(expHi>>48), 694 ), 695 ) 696 msgActualValuesStrs = append(msgActualValuesStrs, 697 fmt.Sprintf("i16x8(%#x, %#x, %#x, %#x, %#x, %#x, %#x, %#x)", 698 uint16(actualLo), uint16(actualLo>>16), uint16(actualLo>>32), uint16(actualLo>>48), 699 uint16(actualHi), uint16(actualHi>>16), uint16(actualHi>>32), uint16(actualHi>>48), 700 ), 701 ) 702 matched = matched && (expLo == actualLo) && (expHi == actualHi) 703 case laneTypeI32: 704 msgExpValuesStrs = append(msgExpValuesStrs, 705 fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(expLo), uint32(expLo>>32), uint32(expHi), uint32(expHi>>32)), 706 ) 707 msgActualValuesStrs = append(msgActualValuesStrs, 708 fmt.Sprintf("i32x4(%#x, %#x, %#x, %#x)", uint32(actualLo), uint32(actualLo>>32), uint32(actualHi), uint32(actualHi>>32)), 709 ) 710 matched = matched && (expLo == actualLo) && (expHi == actualHi) 711 case laneTypeI64: 712 msgExpValuesStrs = append(msgExpValuesStrs, 713 fmt.Sprintf("i64x2(%#x, %#x)", expLo, expHi), 714 ) 715 msgActualValuesStrs = append(msgActualValuesStrs, 716 fmt.Sprintf("i64x2(%#x, %#x)", actualLo, actualHi), 717 ) 718 matched = matched && (expLo == actualLo) && (expHi == actualHi) 719 case laneTypeF32: 720 msgExpValuesStrs = append(msgExpValuesStrs, 721 fmt.Sprintf("f32x4(%f, %f, %f, %f)", 722 math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(expLo>>32)), 723 math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(expHi>>32)), 724 ), 725 ) 726 msgActualValuesStrs = append(msgActualValuesStrs, 727 fmt.Sprintf("f32x4(%f, %f, %f, %f)", 728 math.Float32frombits(uint32(actualLo)), math.Float32frombits(uint32(actualLo>>32)), 729 math.Float32frombits(uint32(actualHi)), math.Float32frombits(uint32(actualHi>>32)), 730 ), 731 ) 732 matched = matched && 733 f32Equal(math.Float32frombits(uint32(expLo)), math.Float32frombits(uint32(actualLo))) && 734 f32Equal(math.Float32frombits(uint32(expLo>>32)), math.Float32frombits(uint32(actualLo>>32))) && 735 f32Equal(math.Float32frombits(uint32(expHi)), math.Float32frombits(uint32(actualHi))) && 736 f32Equal(math.Float32frombits(uint32(expHi>>32)), math.Float32frombits(uint32(actualHi>>32))) 737 case laneTypeF64: 738 msgExpValuesStrs = append(msgExpValuesStrs, 739 fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(expLo), math.Float64frombits(expHi)), 740 ) 741 msgActualValuesStrs = append(msgActualValuesStrs, 742 fmt.Sprintf("f64x2(%f, %f)", math.Float64frombits(actualLo), math.Float64frombits(actualHi)), 743 ) 744 matched = matched && 745 f64Equal(math.Float64frombits(expLo), math.Float64frombits(actualLo)) && 746 f64Equal(math.Float64frombits(expHi), math.Float64frombits(actualHi)) 747 default: 748 panic("BUG") 749 } 750 uint64RepPos += 2 751 default: 752 panic("BUG") 753 } 754 } 755 756 if !matched { 757 valuesMsg = fmt.Sprintf("\thave [%s]\n\twant [%s]", 758 strings.Join(msgActualValuesStrs, ", "), 759 strings.Join(msgExpValuesStrs, ", ")) 760 } 761 return 762 } 763 764 func f32Equal(expected, actual float32) (matched bool) { 765 if expBit := math.Float32bits(expected); expBit == moremath.F32CanonicalNaNBits { 766 matched = math.Float32bits(actual)&moremath.F32CanonicalNaNBitsMask == moremath.F32CanonicalNaNBits 767 } else if expBit == moremath.F32ArithmeticNaNBits { 768 b := math.Float32bits(actual) 769 matched = b&moremath.F32ExponentMask == moremath.F32ExponentMask && // Indicates that exponent part equals of NaN. 770 b&moremath.F32ArithmeticNaNPayloadMSB == moremath.F32ArithmeticNaNPayloadMSB 771 } else if math.IsNaN(float64(expected)) { // NaN cannot be compared with themselves, so we have to use IsNaN 772 matched = math.IsNaN(float64(actual)) 773 } else { 774 // Compare the bit patterns directly, rather than == on float32 since in Go, -0 and 0 equals, 775 // but in the Wasm spec, they are treated as different. 776 matched = math.Float32bits(expected) == math.Float32bits(actual) 777 } 778 return 779 } 780 781 func f64Equal(expected, actual float64) (matched bool) { 782 if expBit := math.Float64bits(expected); expBit == moremath.F64CanonicalNaNBits { 783 matched = math.Float64bits(actual)&moremath.F64CanonicalNaNBitsMask == moremath.F64CanonicalNaNBits 784 } else if expBit == moremath.F64ArithmeticNaNBits { 785 b := math.Float64bits(actual) 786 matched = b&moremath.F64ExponentMask == moremath.F64ExponentMask && // Indicates that exponent part equals of NaN. 787 b&moremath.F64ArithmeticNaNPayloadMSB == moremath.F64ArithmeticNaNPayloadMSB 788 } else if math.IsNaN(expected) { // NaN cannot be compared with themselves, so we have to use IsNaN 789 matched = math.IsNaN(actual) 790 } else { 791 // Compare the bit patterns directly, rather than == on float64 since in Go, -0 and 0 equals, 792 // but in the Wasm spec, they are treated as different. 793 matched = math.Float64bits(expected) == math.Float64bits(actual) 794 } 795 return 796 } 797 798 // callFunction is inlined here as the spectest needs to validate the signature was correct 799 // TODO: This is likely already covered with unit tests! 800 func callFunction(ns *wasm.Namespace, ctx context.Context, moduleName, funcName string, params ...uint64) ([]uint64, []wasm.ValueType, error) { 801 fn := ns.Module(moduleName).ExportedFunction(funcName) 802 results, err := fn.Call(ctx, params...) 803 return results, fn.Definition().ResultTypes(), err 804 }