github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/engine/compiler/engine_test.go (about) 1 package compiler 2 3 import ( 4 "context" 5 "errors" 6 "fmt" 7 "runtime" 8 "testing" 9 "unsafe" 10 11 "github.com/wasilibs/wazerox/api" 12 "github.com/wasilibs/wazerox/experimental" 13 "github.com/wasilibs/wazerox/internal/bitpack" 14 "github.com/wasilibs/wazerox/internal/platform" 15 "github.com/wasilibs/wazerox/internal/testing/require" 16 "github.com/wasilibs/wazerox/internal/wasm" 17 ) 18 19 // testCtx is an arbitrary, non-default context. Non-nil also prevents linter errors. 20 var testCtx = context.WithValue(context.Background(), struct{}{}, "arbitrary") 21 22 // requireSupportedOSArch is duplicated also in the platform package to ensure no cyclic dependency. 23 func requireSupportedOSArch(t *testing.T) { 24 if !platform.CompilerSupported() { 25 t.Skip() 26 } 27 } 28 29 type fakeFinalizer map[*compiledModule]func(module *compiledModule) 30 31 func (f fakeFinalizer) setFinalizer(obj interface{}, finalizer interface{}) { 32 cf := obj.(*compiledModule) 33 if _, ok := f[cf]; ok { // easier than adding a field for testing.T 34 panic(fmt.Sprintf("BUG: %v already had its finalizer set", cf)) 35 } 36 f[cf] = finalizer.(func(*compiledModule)) 37 } 38 39 func TestCompiler_CompileModule(t *testing.T) { 40 t.Run("ok", func(t *testing.T) { 41 e := NewEngine(testCtx, api.CoreFeaturesV1, nil).(*engine) 42 ff := fakeFinalizer{} 43 e.setFinalizer = ff.setFinalizer 44 45 okModule := &wasm.Module{ 46 TypeSection: []wasm.FunctionType{{}}, 47 FunctionSection: []wasm.Index{0, 0, 0, 0}, 48 CodeSection: []wasm.Code{ 49 {Body: []byte{wasm.OpcodeEnd}}, 50 {Body: []byte{wasm.OpcodeEnd}}, 51 {Body: []byte{wasm.OpcodeEnd}}, 52 {Body: []byte{wasm.OpcodeEnd}}, 53 }, 54 ID: wasm.ModuleID{}, 55 } 56 57 err := e.CompileModule(testCtx, okModule, nil, false) 58 require.NoError(t, err) 59 60 // Compiling same module shouldn't be compiled again, but instead should be cached. 61 err = e.CompileModule(testCtx, okModule, nil, false) 62 require.NoError(t, err) 63 64 compiled, ok := e.codes[okModule.ID] 65 require.True(t, ok) 66 require.Equal(t, len(okModule.FunctionSection), len(compiled.functions)) 67 68 // Pretend the finalizer executed, by invoking them one-by-one. 69 for k, v := range ff { 70 v(k) 71 } 72 }) 73 74 t.Run("fail", func(t *testing.T) { 75 errModule := &wasm.Module{ 76 TypeSection: []wasm.FunctionType{{}}, 77 FunctionSection: []wasm.Index{0, 0, 0}, 78 CodeSection: []wasm.Code{ 79 {Body: []byte{wasm.OpcodeEnd}}, 80 {Body: []byte{wasm.OpcodeEnd}}, 81 {Body: []byte{wasm.OpcodeCall}}, // Call instruction without immediate for call target index is invalid and should fail to compile. 82 }, 83 ID: wasm.ModuleID{}, 84 } 85 86 e := NewEngine(testCtx, api.CoreFeaturesV1, nil).(*engine) 87 err := e.CompileModule(testCtx, errModule, nil, false) 88 require.EqualError(t, err, "failed to lower func[2]: handling instruction: apply stack failed for call: reading immediates: EOF") 89 90 // On the compilation failure, the compiled functions must not be cached. 91 _, ok := e.codes[errModule.ID] 92 require.False(t, ok) 93 }) 94 } 95 96 func TestCompiler_Releasecode_Panic(t *testing.T) { 97 captured := require.CapturePanic(func() { 98 releaseCompiledModule(&compiledModule{ 99 compiledCode: &compiledCode{ 100 executable: makeCodeSegment(1, 2), 101 }, 102 }) 103 }) 104 require.Contains(t, captured.Error(), "compiler: failed to munmap code segment") 105 } 106 107 // Ensures that value stack and call-frame stack are allocated on heap which 108 // allows us to safely access to their data region from native code. 109 // See comments on initialStackSize and initialCallFrameStackSize. 110 func TestCompiler_SliceAllocatedOnHeap(t *testing.T) { 111 enabledFeatures := api.CoreFeaturesV1 112 e := newEngine(enabledFeatures, nil) 113 s := wasm.NewStore(enabledFeatures, e) 114 115 const hostModuleName = "env" 116 const hostFnName = "grow_and_shrink_goroutine_stack" 117 hostFn := func() { 118 // This function aggressively grow the goroutine stack by recursively 119 // calling the function many times. 120 callNum := 1000 121 var growGoroutineStack func() 122 growGoroutineStack = func() { 123 if callNum != 0 { 124 callNum-- 125 growGoroutineStack() 126 } 127 } 128 growGoroutineStack() 129 130 // Trigger relocation of goroutine stack because at this point we have the majority of 131 // goroutine stack unused after recursive call. 132 runtime.GC() 133 } 134 hm, err := wasm.NewHostModule( 135 hostModuleName, 136 []string{hostFnName}, 137 map[string]*wasm.HostFunc{hostFnName: {ExportName: hostFnName, Code: wasm.Code{GoFunc: hostFn}}}, 138 enabledFeatures, 139 ) 140 require.NoError(t, err) 141 142 err = s.Engine.CompileModule(testCtx, hm, nil, false) 143 require.NoError(t, err) 144 145 typeIDs, err := s.GetFunctionTypeIDs(hm.TypeSection) 146 require.NoError(t, err) 147 148 _, err = s.Instantiate(testCtx, hm, hostModuleName, nil, typeIDs) 149 require.NoError(t, err) 150 151 const stackCorruption = "value_stack_corruption" 152 const callStackCorruption = "call_stack_corruption" 153 const expectedReturnValue = 0x1 154 m := &wasm.Module{ 155 ImportFunctionCount: 1, 156 TypeSection: []wasm.FunctionType{ 157 {Params: []wasm.ValueType{}, Results: []wasm.ValueType{wasm.ValueTypeI32}, ResultNumInUint64: 1}, 158 {Params: []wasm.ValueType{}, Results: []wasm.ValueType{}}, 159 }, 160 FunctionSection: []wasm.Index{ 161 wasm.Index(0), 162 wasm.Index(0), 163 wasm.Index(0), 164 }, 165 CodeSection: []wasm.Code{ 166 { 167 // value_stack_corruption 168 Body: []byte{ 169 wasm.OpcodeCall, 0, // Call host function to shrink Goroutine stack 170 // We expect this value is returned, but if the stack is allocated on 171 // goroutine stack, we write this expected value into the old-location of 172 // stack. 173 wasm.OpcodeI32Const, expectedReturnValue, 174 wasm.OpcodeEnd, 175 }, 176 }, 177 { 178 // call_stack_corruption 179 Body: []byte{ 180 wasm.OpcodeCall, 3, // Call the wasm function below. 181 // At this point, call stack's memory looks like [call_stack_corruption, index3] 182 // With this function call it should end up [call_stack_corruption, host func] 183 // but if the call-frame stack is allocated on goroutine stack, we exit the native code 184 // with [call_stack_corruption, index3] (old call frame stack) with HostCall status code, 185 // and end up trying to call index3 as a host function which results in nil pointer exception. 186 wasm.OpcodeCall, 0, 187 wasm.OpcodeI32Const, expectedReturnValue, 188 wasm.OpcodeEnd, 189 }, 190 }, 191 {Body: []byte{wasm.OpcodeCall, 0, wasm.OpcodeEnd}}, 192 }, 193 ImportSection: []wasm.Import{{Module: hostModuleName, Name: hostFnName, DescFunc: 1}}, 194 ImportPerModule: map[string][]*wasm.Import{ 195 hostModuleName: {{Module: hostModuleName, Name: hostFnName, DescFunc: 1}}, 196 }, 197 ExportSection: []wasm.Export{ 198 {Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption}, 199 {Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption}, 200 }, 201 Exports: map[string]*wasm.Export{ 202 stackCorruption: {Type: wasm.ExternTypeFunc, Index: 1, Name: stackCorruption}, 203 callStackCorruption: {Type: wasm.ExternTypeFunc, Index: 2, Name: callStackCorruption}, 204 }, 205 ID: wasm.ModuleID{1}, 206 } 207 208 err = s.Engine.CompileModule(testCtx, m, nil, false) 209 require.NoError(t, err) 210 211 typeIDs, err = s.GetFunctionTypeIDs(m.TypeSection) 212 require.NoError(t, err) 213 214 mi, err := s.Instantiate(testCtx, m, t.Name(), nil, typeIDs) 215 require.NoError(t, err) 216 217 for _, fnName := range []string{stackCorruption, callStackCorruption} { 218 fnName := fnName 219 t.Run(fnName, func(t *testing.T) { 220 ret, err := mi.ExportedFunction(fnName).Call(testCtx) 221 require.NoError(t, err) 222 223 require.Equal(t, uint32(expectedReturnValue), uint32(ret[0])) 224 }) 225 } 226 } 227 228 func TestCallEngine_builtinFunctionTableGrow(t *testing.T) { 229 ce := &callEngine{ 230 stack: []uint64{ 231 0xff, // pseudo-ref 232 1, // num 233 // Table Index = 0 (lower 32-bits), but the higher bits (32-63) are all sets, 234 // which happens if the previous value on that stack location was 64-bit wide. 235 0xffffffff << 32, 236 }, 237 stackContext: stackContext{stackPointer: 3}, 238 } 239 240 table := &wasm.TableInstance{References: []wasm.Reference{}, Min: 10} 241 ce.builtinFunctionTableGrow([]*wasm.TableInstance{table}) 242 243 require.Equal(t, 1, len(table.References)) 244 require.Equal(t, uintptr(0xff), table.References[0]) 245 } 246 247 func ptrAsUint64(f *function) uint64 { 248 return uint64(uintptr(unsafe.Pointer(f))) 249 } 250 251 func TestCallEngine_deferredOnCall(t *testing.T) { 252 s := &wasm.Module{ 253 FunctionSection: []wasm.Index{0, 1, 2}, 254 CodeSection: []wasm.Code{{}, {}, {}}, 255 TypeSection: []wasm.FunctionType{{}, {}, {}}, 256 } 257 f1 := &function{ 258 funcType: &wasm.FunctionType{ParamNumInUint64: 2}, 259 parent: &compiledFunction{parent: &compiledCode{source: s}, index: 0}, 260 } 261 f2 := &function{ 262 funcType: &wasm.FunctionType{ParamNumInUint64: 2, ResultNumInUint64: 3}, 263 parent: &compiledFunction{parent: &compiledCode{source: s}, index: 1}, 264 } 265 f3 := &function{ 266 funcType: &wasm.FunctionType{ResultNumInUint64: 1}, 267 parent: &compiledFunction{parent: &compiledCode{source: s}, index: 2}, 268 } 269 270 ce := &callEngine{ 271 stack: []uint64{ 272 0xff, 0xff, // dummy argument for f1 273 0, 0, 0, 0, 274 0xcc, 0xcc, // local variable for f1. 275 // <----- stack base point of f2 (top) == index 8. 276 0xaa, 0xaa, 0xdeadbeaf, // dummy argument for f2 (0xaa, 0xaa) and the reserved slot for result 0xdeadbeaf) 277 0, 0, ptrAsUint64(f1), 0, // callFrame 278 0xcc, 0xcc, 0xcc, // local variable for f2. 279 // <----- stack base point of f3 (top) == index 18 280 0xdeadbeaf, // the reserved slot for result 0xdeadbeaf) from f3. 281 0, 8 << 3, ptrAsUint64(f2), 0, // callFrame 282 }, 283 stackContext: stackContext{ 284 stackBasePointerInBytes: 18 << 3, // currently executed function (f3)'s base pointer. 285 stackPointer: 0xff, // dummy supposed to be reset to zero. 286 }, 287 moduleContext: moduleContext{ 288 fn: f3, // currently executed function (f3)! 289 moduleInstance: nil, 290 }, 291 } 292 293 beforeRecoverStack := ce.stack 294 295 err := ce.deferredOnCall(context.Background(), &wasm.ModuleInstance{}, errors.New("some error")) 296 require.EqualError(t, err, `some error (recovered by wazero) 297 wasm stack trace: 298 .$2() 299 .$1() 300 .$0()`) 301 302 // After recover, the state of callEngine must be reset except that the underlying slices must be intact 303 // for the subsequent calls to avoid additional allocations on each call. 304 require.Equal(t, uint64(0), ce.stackBasePointerInBytes) 305 require.Equal(t, uint64(0), ce.stackPointer) 306 require.Equal(t, nil, ce.moduleInstance) 307 require.Equal(t, beforeRecoverStack, ce.stack) 308 309 // Keep f1, f2, and f3 alive until we reach here, as we access these functions from the uint64 raw pointers in the stack. 310 // In practice, they are guaranteed to be alive as they are held by moduleContext. 311 runtime.KeepAlive(f1) 312 runtime.KeepAlive(f2) 313 runtime.KeepAlive(f3) 314 } 315 316 func TestCallEngine_initializeStack(t *testing.T) { 317 const i32 = wasm.ValueTypeI32 318 const stackSize = 10 319 const initialVal = ^uint64(0) 320 tests := []struct { 321 name string 322 funcType *wasm.FunctionType 323 args []uint64 324 expStackPointer uint64 325 expStack [stackSize]uint64 326 }{ 327 { 328 name: "no param/result", 329 funcType: &wasm.FunctionType{}, 330 expStackPointer: callFrameDataSizeInUint64, 331 expStack: [stackSize]uint64{ 332 0, 0, 0, // zeroed call frame 333 initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, initialVal, 334 }, 335 }, 336 { 337 name: "no result", 338 funcType: &wasm.FunctionType{ 339 Params: []wasm.ValueType{i32, i32}, 340 ParamNumInUint64: 2, 341 }, 342 args: []uint64{0xdeadbeaf, 0xdeadbeaf}, 343 expStackPointer: callFrameDataSizeInUint64 + 2, 344 expStack: [stackSize]uint64{ 345 0xdeadbeaf, 0xdeadbeaf, // arguments 346 0, 0, 0, // zeroed call frame 347 initialVal, initialVal, initialVal, initialVal, initialVal, 348 }, 349 }, 350 { 351 name: "no param", 352 funcType: &wasm.FunctionType{ 353 Results: []wasm.ValueType{i32, i32, i32}, 354 ResultNumInUint64: 3, 355 }, 356 expStackPointer: callFrameDataSizeInUint64 + 3, 357 expStack: [stackSize]uint64{ 358 initialVal, initialVal, initialVal, // reserved slots for results 359 0, 0, 0, // zeroed call frame 360 initialVal, initialVal, initialVal, initialVal, 361 }, 362 }, 363 { 364 name: "params > results", 365 funcType: &wasm.FunctionType{ 366 Params: []wasm.ValueType{i32, i32, i32, i32, i32}, 367 ParamNumInUint64: 5, 368 Results: []wasm.ValueType{i32, i32, i32}, 369 ResultNumInUint64: 3, 370 }, 371 args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf}, 372 expStackPointer: callFrameDataSizeInUint64 + 5, 373 expStack: [stackSize]uint64{ 374 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 375 0, 0, 0, // zeroed call frame 376 initialVal, initialVal, 377 }, 378 }, 379 { 380 name: "params == results", 381 funcType: &wasm.FunctionType{ 382 Params: []wasm.ValueType{i32, i32, i32, i32, i32}, 383 ParamNumInUint64: 5, 384 Results: []wasm.ValueType{i32, i32, i32, i32, i32}, 385 ResultNumInUint64: 5, 386 }, 387 args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf}, 388 expStackPointer: callFrameDataSizeInUint64 + 5, 389 expStack: [stackSize]uint64{ 390 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 391 0, 0, 0, // zeroed call frame 392 initialVal, initialVal, 393 }, 394 }, 395 { 396 name: "params < results", 397 funcType: &wasm.FunctionType{ 398 Params: []wasm.ValueType{i32, i32, i32}, 399 ParamNumInUint64: 3, 400 Results: []wasm.ValueType{i32, i32, i32, i32, i32}, 401 ResultNumInUint64: 5, 402 }, 403 args: []uint64{0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf}, 404 expStackPointer: callFrameDataSizeInUint64 + 5, 405 expStack: [stackSize]uint64{ 406 0xdeafbeaf, 0xdeafbeaf, 0xdeafbeaf, 407 initialVal, initialVal, // reserved for results 408 0, 0, 0, // zeroed call frame 409 initialVal, initialVal, 410 }, 411 }, 412 } 413 414 for _, tc := range tests { 415 tc := tc 416 t.Run(tc.name, func(t *testing.T) { 417 initialStack := make([]uint64, stackSize) 418 for i := range initialStack { 419 initialStack[i] = initialVal 420 } 421 ce := &callEngine{stack: initialStack} 422 ce.initializeStack(tc.funcType, tc.args) 423 require.Equal(t, tc.expStackPointer, ce.stackPointer) 424 require.Equal(t, tc.expStack[:], ce.stack) 425 }) 426 } 427 } 428 429 func Test_callFrameOffset(t *testing.T) { 430 require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 0, ResultNumInUint64: 1})) 431 require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 5, ResultNumInUint64: 10})) 432 require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 50, ResultNumInUint64: 100})) 433 require.Equal(t, 1, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 1, ResultNumInUint64: 0})) 434 require.Equal(t, 10, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 10, ResultNumInUint64: 5})) 435 require.Equal(t, 100, callFrameOffset(&wasm.FunctionType{ParamNumInUint64: 100, ResultNumInUint64: 50})) 436 } 437 438 type stackEntry struct { 439 def api.FunctionDefinition 440 } 441 442 func assertStackIterator(t *testing.T, it experimental.StackIterator, expected []stackEntry) { 443 var actual []stackEntry 444 for it.Next() { 445 actual = append(actual, stackEntry{def: it.Function().Definition()}) 446 } 447 require.Equal(t, expected, actual) 448 } 449 450 func TestCallEngine_builtinFunctionFunctionListenerBefore(t *testing.T) { 451 currentContext := context.Background() 452 453 f := &function{ 454 funcType: &wasm.FunctionType{ParamNumInUint64: 3}, 455 parent: &compiledFunction{ 456 listener: mockListener{ 457 before: func(ctx context.Context, _ api.Module, def api.FunctionDefinition, params []uint64, stackIterator experimental.StackIterator) { 458 require.Equal(t, currentContext, ctx) 459 require.Equal(t, []uint64{2, 3, 4}, params) 460 assertStackIterator(t, stackIterator, []stackEntry{{def: def}}) 461 }, 462 }, 463 index: 0, 464 parent: &compiledCode{source: &wasm.Module{ 465 FunctionSection: []wasm.Index{0}, 466 CodeSection: []wasm.Code{{}}, 467 TypeSection: []wasm.FunctionType{{}}, 468 }}, 469 }, 470 } 471 ce := &callEngine{ 472 stack: []uint64{0, 1, 2, 3, 4, 0, 0, 0}, 473 stackContext: stackContext{stackBasePointerInBytes: 16}, 474 } 475 ce.builtinFunctionFunctionListenerBefore(currentContext, &wasm.ModuleInstance{}, f) 476 } 477 478 func TestCallEngine_builtinFunctionFunctionListenerAfter(t *testing.T) { 479 currentContext := context.Background() 480 f := &function{ 481 funcType: &wasm.FunctionType{ResultNumInUint64: 1}, 482 parent: &compiledFunction{ 483 listener: mockListener{ 484 after: func(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) { 485 require.Equal(t, currentContext, ctx) 486 require.Equal(t, []uint64{5}, results) 487 }, 488 }, 489 index: 0, 490 parent: &compiledCode{source: &wasm.Module{ 491 FunctionSection: []wasm.Index{0}, 492 CodeSection: []wasm.Code{{}}, 493 TypeSection: []wasm.FunctionType{{}}, 494 }}, 495 }, 496 } 497 498 ce := &callEngine{ 499 stack: []uint64{0, 1, 2, 3, 4, 5}, 500 stackContext: stackContext{stackBasePointerInBytes: 40}, 501 } 502 ce.builtinFunctionFunctionListenerAfter(currentContext, &wasm.ModuleInstance{}, f) 503 } 504 505 type mockListener struct { 506 before func(context.Context, api.Module, api.FunctionDefinition, []uint64, experimental.StackIterator) 507 after func(context.Context, api.Module, api.FunctionDefinition, []uint64) 508 abort func(context.Context, api.Module, api.FunctionDefinition, error) 509 } 510 511 func (m mockListener) Before(ctx context.Context, mod api.Module, def api.FunctionDefinition, params []uint64, stackIterator experimental.StackIterator) { 512 if m.before != nil { 513 m.before(ctx, mod, def, params, stackIterator) 514 } 515 } 516 517 func (m mockListener) After(ctx context.Context, mod api.Module, def api.FunctionDefinition, results []uint64) { 518 if m.after != nil { 519 m.after(ctx, mod, def, results) 520 } 521 } 522 523 func (m mockListener) Abort(ctx context.Context, mod api.Module, def api.FunctionDefinition, err error) { 524 if m.abort != nil { 525 m.abort(ctx, mod, def, err) 526 } 527 } 528 529 func TestFunction_getSourceOffsetInWasmBinary(t *testing.T) { 530 tests := []struct { 531 name string 532 pc, exp uint64 533 codeInitialAddress uintptr 534 srcMap sourceOffsetMap 535 }{ 536 {name: "not found", srcMap: sourceOffsetMap{}}, 537 { 538 name: "first IR", 539 pc: 4000, 540 codeInitialAddress: 3999, 541 srcMap: sourceOffsetMap{ 542 irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{ 543 0 /*4000-3999=1 exists here*/, 5, 8, 15, 544 }), 545 irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{ 546 10, 100, 800, 12344, 547 }), 548 }, 549 exp: 10, 550 }, 551 { 552 name: "middle", 553 pc: 100, 554 codeInitialAddress: 90, 555 srcMap: sourceOffsetMap{ 556 irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{ 557 0, 5, 8 /*100-90=10 exists here*/, 15, 558 }), 559 irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{ 560 10, 100, 800, 12344, 561 }), 562 }, 563 exp: 800, 564 }, 565 { 566 name: "last", 567 pc: 9999, 568 codeInitialAddress: 8999, 569 srcMap: sourceOffsetMap{ 570 irOperationOffsetsInNativeBinary: bitpack.NewOffsetArray([]uint64{ 571 0, 5, 8, 15, /*9999-8999=1000 exists here*/ 572 }), 573 irOperationSourceOffsetsInWasmBinary: bitpack.NewOffsetArray([]uint64{ 574 10, 100, 800, 12344, 575 }), 576 }, 577 exp: 12344, 578 }, 579 } 580 581 for _, tc := range tests { 582 tc := tc 583 t.Run(tc.name, func(t *testing.T) { 584 f := function{ 585 parent: &compiledFunction{sourceOffsetMap: tc.srcMap}, 586 codeInitialAddress: tc.codeInitialAddress, 587 } 588 589 actual := f.getSourceOffsetInWasmBinary(tc.pc) 590 require.Equal(t, tc.exp, actual) 591 }) 592 } 593 }