wa-lang.org/wazero@v1.0.2/internal/engine/compiler/compiler_controlflow_test.go (about) 1 package compiler 2 3 import ( 4 "fmt" 5 "testing" 6 "unsafe" 7 8 "wa-lang.org/wazero/internal/testing/require" 9 "wa-lang.org/wazero/internal/wasm" 10 "wa-lang.org/wazero/internal/wazeroir" 11 ) 12 13 func TestCompiler_compileHostFunction(t *testing.T) { 14 env := newCompilerEnvironment() 15 compiler := env.requireNewCompiler(t, newCompiler, nil) 16 17 err := compiler.compileGoDefinedHostFunction() 18 require.NoError(t, err) 19 20 // Generate and run the code under test. 21 code, _, err := compiler.compile() 22 require.NoError(t, err) 23 env.exec(code) 24 25 // On the return, the code must exit with the host call status. 26 require.Equal(t, nativeCallStatusCodeCallGoHostFunction, env.compilerStatus()) 27 28 // Re-enter the return address. 29 require.NotEqual(t, uintptr(0), uintptr(env.ce.returnAddress)) 30 nativecall(env.ce.returnAddress, 31 uintptr(unsafe.Pointer(env.callEngine())), 32 uintptr(unsafe.Pointer(env.module())), 33 ) 34 35 // After that, the code must exit with returned status. 36 require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) 37 } 38 39 func TestCompiler_compileLabel(t *testing.T) { 40 label := &wazeroir.Label{FrameID: 100, Kind: wazeroir.LabelKindContinuation} 41 for _, expectSkip := range []bool{false, true} { 42 expectSkip := expectSkip 43 t.Run(fmt.Sprintf("expect skip=%v", expectSkip), func(t *testing.T) { 44 env := newCompilerEnvironment() 45 compiler := env.requireNewCompiler(t, newCompiler, nil) 46 47 if expectSkip { 48 // If the initial stack is not set, compileLabel must return skip=true. 49 actual := compiler.compileLabel(&wazeroir.OperationLabel{Label: label}) 50 require.True(t, actual) 51 } else { 52 err := compiler.compileBr(&wazeroir.OperationBr{Target: &wazeroir.BranchTarget{Label: label}}) 53 require.NoError(t, err) 54 actual := compiler.compileLabel(&wazeroir.OperationLabel{Label: label}) 55 require.False(t, actual) 56 } 57 }) 58 } 59 } 60 61 func TestCompiler_compileBrIf(t *testing.T) { 62 unreachableStatus, thenLabelExitStatus, elseLabelExitStatus := nativeCallStatusCodeUnreachable, nativeCallStatusCodeUnreachable+1, nativeCallStatusCodeUnreachable+2 63 thenBranchTarget := &wazeroir.BranchTargetDrop{Target: &wazeroir.BranchTarget{Label: &wazeroir.Label{Kind: wazeroir.LabelKindHeader, FrameID: 1}}} 64 elseBranchTarget := &wazeroir.BranchTargetDrop{Target: &wazeroir.BranchTarget{Label: &wazeroir.Label{Kind: wazeroir.LabelKindHeader, FrameID: 2}}} 65 66 tests := []struct { 67 name string 68 setupFunc func(t *testing.T, compiler compilerImpl, shouldGoElse bool) 69 }{ 70 { 71 name: "cond on register", 72 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 73 val := uint32(1) 74 if shouldGoElse { 75 val = 0 76 } 77 err := compiler.compileConstI32(&wazeroir.OperationConstI32{Value: val}) 78 require.NoError(t, err) 79 }, 80 }, 81 { 82 name: "LS", 83 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 84 x1, x2 := uint32(1), uint32(2) 85 if shouldGoElse { 86 x2, x1 = x1, x2 87 } 88 requirePushTwoInt32Consts(t, x1, x2, compiler) 89 // Le on unsigned integer produces the value on COND_LS register. 90 err := compiler.compileLe(&wazeroir.OperationLe{Type: wazeroir.SignedTypeUint32}) 91 require.NoError(t, err) 92 }, 93 }, 94 { 95 name: "LE", 96 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 97 x1, x2 := uint32(1), uint32(2) 98 if shouldGoElse { 99 x2, x1 = x1, x2 100 } 101 requirePushTwoInt32Consts(t, x1, x2, compiler) 102 // Le on signed integer produces the value on COND_LE register. 103 err := compiler.compileLe(&wazeroir.OperationLe{Type: wazeroir.SignedTypeInt32}) 104 require.NoError(t, err) 105 }, 106 }, 107 { 108 name: "HS", 109 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 110 x1, x2 := uint32(2), uint32(1) 111 if shouldGoElse { 112 x2, x1 = x1, x2 113 } 114 requirePushTwoInt32Consts(t, x1, x2, compiler) 115 // Ge on unsigned integer produces the value on COND_HS register. 116 err := compiler.compileGe(&wazeroir.OperationGe{Type: wazeroir.SignedTypeUint32}) 117 require.NoError(t, err) 118 }, 119 }, 120 { 121 name: "GE", 122 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 123 x1, x2 := uint32(2), uint32(1) 124 if shouldGoElse { 125 x2, x1 = x1, x2 126 } 127 requirePushTwoInt32Consts(t, x1, x2, compiler) 128 // Ge on signed integer produces the value on COND_GE register. 129 err := compiler.compileGe(&wazeroir.OperationGe{Type: wazeroir.SignedTypeInt32}) 130 require.NoError(t, err) 131 }, 132 }, 133 { 134 name: "HI", 135 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 136 x1, x2 := uint32(2), uint32(1) 137 if shouldGoElse { 138 x2, x1 = x1, x2 139 } 140 requirePushTwoInt32Consts(t, x1, x2, compiler) 141 // Gt on unsigned integer produces the value on COND_HI register. 142 err := compiler.compileGt(&wazeroir.OperationGt{Type: wazeroir.SignedTypeUint32}) 143 require.NoError(t, err) 144 }, 145 }, 146 { 147 name: "GT", 148 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 149 x1, x2 := uint32(2), uint32(1) 150 if shouldGoElse { 151 x2, x1 = x1, x2 152 } 153 requirePushTwoInt32Consts(t, x1, x2, compiler) 154 // Gt on signed integer produces the value on COND_GT register. 155 err := compiler.compileGt(&wazeroir.OperationGt{Type: wazeroir.SignedTypeInt32}) 156 require.NoError(t, err) 157 }, 158 }, 159 { 160 name: "LO", 161 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 162 x1, x2 := uint32(1), uint32(2) 163 if shouldGoElse { 164 x2, x1 = x1, x2 165 } 166 requirePushTwoInt32Consts(t, x1, x2, compiler) 167 // Lt on unsigned integer produces the value on COND_LO register. 168 err := compiler.compileLt(&wazeroir.OperationLt{Type: wazeroir.SignedTypeUint32}) 169 require.NoError(t, err) 170 }, 171 }, 172 { 173 name: "LT", 174 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 175 x1, x2 := uint32(1), uint32(2) 176 if shouldGoElse { 177 x2, x1 = x1, x2 178 } 179 requirePushTwoInt32Consts(t, x1, x2, compiler) 180 // Lt on signed integer produces the value on COND_LT register. 181 err := compiler.compileLt(&wazeroir.OperationLt{Type: wazeroir.SignedTypeInt32}) 182 require.NoError(t, err) 183 }, 184 }, 185 { 186 name: "MI", 187 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 188 x1, x2 := float32(1), float32(2) 189 if shouldGoElse { 190 x2, x1 = x1, x2 191 } 192 requirePushTwoFloat32Consts(t, x1, x2, compiler) 193 // Lt on floats produces the value on COND_MI register. 194 err := compiler.compileLt(&wazeroir.OperationLt{Type: wazeroir.SignedTypeFloat32}) 195 require.NoError(t, err) 196 }, 197 }, 198 { 199 name: "EQ", 200 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 201 x1, x2 := uint32(1), uint32(1) 202 if shouldGoElse { 203 x2++ 204 } 205 requirePushTwoInt32Consts(t, x1, x2, compiler) 206 err := compiler.compileEq(&wazeroir.OperationEq{Type: wazeroir.UnsignedTypeI32}) 207 require.NoError(t, err) 208 }, 209 }, 210 { 211 name: "NE", 212 setupFunc: func(t *testing.T, compiler compilerImpl, shouldGoElse bool) { 213 x1, x2 := uint32(1), uint32(2) 214 if shouldGoElse { 215 x2 = x1 216 } 217 requirePushTwoInt32Consts(t, x1, x2, compiler) 218 err := compiler.compileNe(&wazeroir.OperationNe{Type: wazeroir.UnsignedTypeI32}) 219 require.NoError(t, err) 220 }, 221 }, 222 } 223 224 for _, tt := range tests { 225 tc := tt 226 t.Run(tc.name, func(t *testing.T) { 227 for _, shouldGoToElse := range []bool{false, true} { 228 shouldGoToElse := shouldGoToElse 229 t.Run(fmt.Sprintf("should_goto_else=%v", shouldGoToElse), func(t *testing.T) { 230 env := newCompilerEnvironment() 231 compiler := env.requireNewCompiler(t, newCompiler, nil) 232 err := compiler.compilePreamble() 233 require.NoError(t, err) 234 235 tc.setupFunc(t, compiler, shouldGoToElse) 236 requireRuntimeLocationStackPointerEqual(t, uint64(1), compiler) 237 238 err = compiler.compileBrIf(&wazeroir.OperationBrIf{Then: thenBranchTarget, Else: elseBranchTarget}) 239 require.NoError(t, err) 240 compiler.compileExitFromNativeCode(unreachableStatus) 241 242 // Emit code for .then label. 243 skip := compiler.compileLabel(&wazeroir.OperationLabel{Label: thenBranchTarget.Target.Label}) 244 require.False(t, skip) 245 compiler.compileExitFromNativeCode(thenLabelExitStatus) 246 247 // Emit code for .else label. 248 skip = compiler.compileLabel(&wazeroir.OperationLabel{Label: elseBranchTarget.Target.Label}) 249 require.False(t, skip) 250 compiler.compileExitFromNativeCode(elseLabelExitStatus) 251 252 code, _, err := compiler.compile() 253 require.NoError(t, err) 254 255 // The generated code looks like this: 256 // 257 // ... code from compilePreamble() 258 // ... code from tc.setupFunc() 259 // br_if .then, .else 260 // exit $unreachableStatus 261 // .then: 262 // exit $thenLabelExitStatus 263 // .else: 264 // exit $elseLabelExitStatus 265 // 266 // Therefore, if we start executing from the top, we must end up exiting with an appropriate status. 267 env.exec(code) 268 require.NotEqual(t, unreachableStatus, env.compilerStatus()) 269 if shouldGoToElse { 270 require.Equal(t, elseLabelExitStatus, env.compilerStatus()) 271 } else { 272 require.Equal(t, thenLabelExitStatus, env.compilerStatus()) 273 } 274 }) 275 } 276 }) 277 } 278 } 279 280 func TestCompiler_compileBrTable(t *testing.T) { 281 requireRunAndExpectedValueReturned := func(t *testing.T, env *compilerEnv, c compilerImpl, expValue uint32) { 282 // Emit code for each label which returns the frame ID. 283 for returnValue := uint32(0); returnValue < 7; returnValue++ { 284 label := &wazeroir.Label{Kind: wazeroir.LabelKindHeader, FrameID: returnValue} 285 err := c.compileBr(&wazeroir.OperationBr{Target: &wazeroir.BranchTarget{Label: label}}) 286 require.NoError(t, err) 287 _ = c.compileLabel(&wazeroir.OperationLabel{Label: label}) 288 _ = c.compileConstI32(&wazeroir.OperationConstI32{Value: label.FrameID}) 289 err = c.compileReturnFunction() 290 require.NoError(t, err) 291 } 292 293 // Generate the code under test and run. 294 code, _, err := c.compile() 295 require.NoError(t, err) 296 env.exec(code) 297 298 // Check the returned value. 299 require.Equal(t, uint64(1), env.stackPointer()) 300 require.Equal(t, expValue, env.stackTopAsUint32()) 301 } 302 303 getBranchTargetDropFromFrameID := func(frameid uint32) *wazeroir.BranchTargetDrop { 304 return &wazeroir.BranchTargetDrop{ 305 Target: &wazeroir.BranchTarget{ 306 Label: &wazeroir.Label{FrameID: frameid, Kind: wazeroir.LabelKindHeader}, 307 }, 308 } 309 } 310 311 tests := []struct { 312 name string 313 index int64 314 o *wazeroir.OperationBrTable 315 expectedValue uint32 316 }{ 317 { 318 name: "only default with index 0", 319 o: &wazeroir.OperationBrTable{Default: getBranchTargetDropFromFrameID(6)}, 320 index: 0, 321 expectedValue: 6, 322 }, 323 { 324 name: "only default with index 100", 325 o: &wazeroir.OperationBrTable{Default: getBranchTargetDropFromFrameID(6)}, 326 index: 100, 327 expectedValue: 6, 328 }, 329 { 330 name: "select default with targets and good index", 331 o: &wazeroir.OperationBrTable{ 332 Targets: []*wazeroir.BranchTargetDrop{ 333 getBranchTargetDropFromFrameID(1), 334 getBranchTargetDropFromFrameID(2), 335 }, 336 Default: getBranchTargetDropFromFrameID(6), 337 }, 338 index: 3, 339 expectedValue: 6, 340 }, 341 { 342 name: "select default with targets and huge index", 343 o: &wazeroir.OperationBrTable{ 344 Targets: []*wazeroir.BranchTargetDrop{ 345 getBranchTargetDropFromFrameID(1), 346 getBranchTargetDropFromFrameID(2), 347 }, 348 Default: getBranchTargetDropFromFrameID(6), 349 }, 350 index: 100000, 351 expectedValue: 6, 352 }, 353 { 354 name: "select first with two targets", 355 o: &wazeroir.OperationBrTable{ 356 Targets: []*wazeroir.BranchTargetDrop{ 357 getBranchTargetDropFromFrameID(1), 358 getBranchTargetDropFromFrameID(2), 359 }, 360 Default: getBranchTargetDropFromFrameID(5), 361 }, 362 index: 0, 363 expectedValue: 1, 364 }, 365 { 366 name: "select last with two targets", 367 o: &wazeroir.OperationBrTable{ 368 Targets: []*wazeroir.BranchTargetDrop{ 369 getBranchTargetDropFromFrameID(1), 370 getBranchTargetDropFromFrameID(2), 371 }, 372 Default: getBranchTargetDropFromFrameID(6), 373 }, 374 index: 1, 375 expectedValue: 2, 376 }, 377 { 378 name: "select first with five targets", 379 o: &wazeroir.OperationBrTable{ 380 Targets: []*wazeroir.BranchTargetDrop{ 381 getBranchTargetDropFromFrameID(1), 382 getBranchTargetDropFromFrameID(2), 383 getBranchTargetDropFromFrameID(3), 384 getBranchTargetDropFromFrameID(4), 385 getBranchTargetDropFromFrameID(5), 386 }, 387 Default: getBranchTargetDropFromFrameID(5), 388 }, 389 index: 0, 390 expectedValue: 1, 391 }, 392 { 393 name: "select middle with five targets", 394 o: &wazeroir.OperationBrTable{ 395 Targets: []*wazeroir.BranchTargetDrop{ 396 getBranchTargetDropFromFrameID(1), 397 getBranchTargetDropFromFrameID(2), 398 getBranchTargetDropFromFrameID(3), 399 getBranchTargetDropFromFrameID(4), 400 getBranchTargetDropFromFrameID(5), 401 }, 402 Default: getBranchTargetDropFromFrameID(5), 403 }, 404 index: 2, 405 expectedValue: 3, 406 }, 407 { 408 name: "select last with five targets", 409 o: &wazeroir.OperationBrTable{ 410 Targets: []*wazeroir.BranchTargetDrop{ 411 getBranchTargetDropFromFrameID(1), 412 getBranchTargetDropFromFrameID(2), 413 getBranchTargetDropFromFrameID(3), 414 getBranchTargetDropFromFrameID(4), 415 getBranchTargetDropFromFrameID(5), 416 }, 417 Default: getBranchTargetDropFromFrameID(5), 418 }, 419 index: 4, 420 expectedValue: 5, 421 }, 422 } 423 424 for _, tt := range tests { 425 tc := tt 426 t.Run(tc.name, func(t *testing.T) { 427 env := newCompilerEnvironment() 428 compiler := env.requireNewCompiler(t, newCompiler, nil) 429 430 err := compiler.compilePreamble() 431 require.NoError(t, err) 432 433 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(tc.index)}) 434 require.NoError(t, err) 435 436 err = compiler.compileBrTable(tc.o) 437 require.NoError(t, err) 438 439 require.Zero(t, len(compiler.runtimeValueLocationStack().usedRegisters)) 440 441 requireRunAndExpectedValueReturned(t, env, compiler, tc.expectedValue) 442 }) 443 } 444 } 445 446 func requirePushTwoInt32Consts(t *testing.T, x1, x2 uint32, compiler compilerImpl) { 447 err := compiler.compileConstI32(&wazeroir.OperationConstI32{Value: x1}) 448 require.NoError(t, err) 449 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: x2}) 450 require.NoError(t, err) 451 } 452 453 func requirePushTwoFloat32Consts(t *testing.T, x1, x2 float32, compiler compilerImpl) { 454 err := compiler.compileConstF32(&wazeroir.OperationConstF32{Value: x1}) 455 require.NoError(t, err) 456 err = compiler.compileConstF32(&wazeroir.OperationConstF32{Value: x2}) 457 require.NoError(t, err) 458 } 459 460 func TestCompiler_compileBr(t *testing.T) { 461 t.Run("return", func(t *testing.T) { 462 env := newCompilerEnvironment() 463 compiler := env.requireNewCompiler(t, newCompiler, nil) 464 err := compiler.compilePreamble() 465 require.NoError(t, err) 466 467 // Branch into nil label is interpreted as return. See BranchTarget.IsReturnTarget 468 err = compiler.compileBr(&wazeroir.OperationBr{Target: &wazeroir.BranchTarget{Label: nil}}) 469 require.NoError(t, err) 470 471 // Compile and execute the code under test. 472 // Note: we don't invoke "compiler.return()" as the code emitted by compilerBr is enough to exit. 473 code, _, err := compiler.compile() 474 require.NoError(t, err) 475 env.exec(code) 476 477 require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) 478 }) 479 t.Run("back-and-forth br", func(t *testing.T) { 480 env := newCompilerEnvironment() 481 compiler := env.requireNewCompiler(t, newCompiler, nil) 482 err := compiler.compilePreamble() 483 require.NoError(t, err) 484 485 // Emit the forward br, meaning that handle Br instruction where the target label hasn't been compiled yet. 486 forwardLabel := &wazeroir.Label{Kind: wazeroir.LabelKindHeader, FrameID: 0} 487 err = compiler.compileBr(&wazeroir.OperationBr{Target: &wazeroir.BranchTarget{Label: forwardLabel}}) 488 require.NoError(t, err) 489 490 // We must not reach the code after Br, so emit the code exiting with Unreachable status. 491 compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 492 require.NoError(t, err) 493 494 exitLabel := &wazeroir.Label{Kind: wazeroir.LabelKindHeader, FrameID: 1} 495 err = compiler.compileBr(&wazeroir.OperationBr{Target: &wazeroir.BranchTarget{Label: exitLabel}}) 496 require.NoError(t, err) 497 498 // Emit code for the exitLabel. 499 skip := compiler.compileLabel(&wazeroir.OperationLabel{Label: exitLabel}) 500 require.False(t, skip) 501 compiler.compileExitFromNativeCode(nativeCallStatusCodeReturned) 502 require.NoError(t, err) 503 504 // Emit code for the forwardLabel. 505 skip = compiler.compileLabel(&wazeroir.OperationLabel{Label: forwardLabel}) 506 require.False(t, skip) 507 err = compiler.compileBr(&wazeroir.OperationBr{Target: &wazeroir.BranchTarget{Label: exitLabel}}) 508 require.NoError(t, err) 509 510 code, _, err := compiler.compile() 511 require.NoError(t, err) 512 513 // The generated code looks like this: 514 // 515 // ... code from compilePreamble() 516 // br .forwardLabel 517 // exit nativeCallStatusCodeUnreachable // must not be reached 518 // br .exitLabel // must not be reached 519 // .exitLabel: 520 // exit nativeCallStatusCodeReturned 521 // .forwardLabel: 522 // br .exitLabel 523 // 524 // Therefore, if we start executing from the top, we must end up exiting nativeCallStatusCodeReturned. 525 env.exec(code) 526 require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) 527 }) 528 } 529 530 func TestCompiler_compileCallIndirect(t *testing.T) { 531 t.Run("out of bounds", func(t *testing.T) { 532 env := newCompilerEnvironment() 533 env.addTable(&wasm.TableInstance{References: make([]wasm.Reference, 10)}) 534 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 535 Signature: &wasm.FunctionType{}, 536 Types: []*wasm.FunctionType{{}}, 537 HasTable: true, 538 }) 539 err := compiler.compilePreamble() 540 require.NoError(t, err) 541 542 targetOperation := &wazeroir.OperationCallIndirect{} 543 544 // Place the offset value. 545 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 10}) 546 require.NoError(t, err) 547 548 err = compiler.compileCallIndirect(targetOperation) 549 require.NoError(t, err) 550 551 // We expect to exit from the code in callIndirect so the subsequent code must be unreachable. 552 compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 553 554 // Generate the code under test and run. 555 code, _, err := compiler.compile() 556 require.NoError(t, err) 557 env.exec(code) 558 559 require.Equal(t, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) 560 }) 561 562 t.Run("uninitialized", func(t *testing.T) { 563 env := newCompilerEnvironment() 564 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 565 Signature: &wasm.FunctionType{}, 566 Types: []*wasm.FunctionType{{}}, 567 HasTable: true, 568 }) 569 err := compiler.compilePreamble() 570 require.NoError(t, err) 571 572 targetOperation := &wazeroir.OperationCallIndirect{} 573 targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)} 574 575 // and the typeID doesn't match the table[targetOffset]'s type ID. 576 table := make([]wasm.Reference, 10) 577 env.addTable(&wasm.TableInstance{References: table}) 578 env.module().TypeIDs = make([]wasm.FunctionTypeID, 10) 579 580 // Place the offset value. 581 err = compiler.compileConstI32(targetOffset) 582 require.NoError(t, err) 583 err = compiler.compileCallIndirect(targetOperation) 584 require.NoError(t, err) 585 586 // We expect to exit from the code in callIndirect so the subsequent code must be unreachable. 587 compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 588 require.NoError(t, err) 589 590 // Generate the code under test and run. 591 code, _, err := compiler.compile() 592 require.NoError(t, err) 593 env.exec(code) 594 595 require.Equal(t, nativeCallStatusCodeInvalidTableAccess, env.compilerStatus()) 596 }) 597 598 t.Run("type not match", func(t *testing.T) { 599 env := newCompilerEnvironment() 600 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 601 Signature: &wasm.FunctionType{}, 602 Types: []*wasm.FunctionType{{}}, 603 HasTable: true, 604 }) 605 err := compiler.compilePreamble() 606 require.NoError(t, err) 607 608 targetOperation := &wazeroir.OperationCallIndirect{} 609 targetOffset := &wazeroir.OperationConstI32{Value: uint32(0)} 610 env.module().TypeIDs = []wasm.FunctionTypeID{1000} 611 // Ensure that the module instance has the type information for targetOperation.TypeIndex, 612 // and the typeID doesn't match the table[targetOffset]'s type ID. 613 table := make([]wasm.Reference, 10) 614 env.addTable(&wasm.TableInstance{References: table}) 615 616 cf := &function{source: &wasm.FunctionInstance{TypeID: 50}} 617 table[0] = uintptr(unsafe.Pointer(cf)) 618 619 // Place the offset value. 620 err = compiler.compileConstI32(targetOffset) 621 require.NoError(t, err) 622 623 // Now emit the code. 624 require.NoError(t, compiler.compileCallIndirect(targetOperation)) 625 626 // We expect to exit from the code in callIndirect so the subsequent code must be unreachable. 627 compiler.compileExitFromNativeCode(nativeCallStatusCodeUnreachable) 628 require.NoError(t, err) 629 630 // Generate the code under test and run. 631 code, _, err := compiler.compile() 632 require.NoError(t, err) 633 env.exec(code) 634 635 require.Equal(t, nativeCallStatusCodeTypeMismatchOnIndirectCall.String(), env.compilerStatus().String()) 636 }) 637 638 t.Run("ok", func(t *testing.T) { 639 targetType := &wasm.FunctionType{ 640 Results: []wasm.ValueType{wasm.ValueTypeI32}, 641 ResultNumInUint64: 1, 642 } 643 targetTypeID := wasm.FunctionTypeID(10) 644 operation := &wazeroir.OperationCallIndirect{TypeIndex: 0} 645 646 table := make([]wasm.Reference, 10) 647 env := newCompilerEnvironment() 648 env.addTable(&wasm.TableInstance{References: table}) 649 650 // Ensure that the module instance has the type information for targetOperation.TypeIndex, 651 // and the typeID matches the table[targetOffset]'s type ID. 652 env.module().TypeIDs = make([]wasm.FunctionTypeID, 100) 653 env.module().TypeIDs[operation.TypeIndex] = targetTypeID 654 env.module().Engine = &moduleEngine{functions: []*function{}} 655 656 me := env.moduleEngine() 657 for i := 0; i < len(table); i++ { 658 // First, we create the call target function for the table element i. 659 // To match its function type, it must return one value. 660 expectedReturnValue := uint32(i * 1000) 661 662 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 663 Signature: targetType, 664 }) 665 err := compiler.compilePreamble() 666 require.NoError(t, err) 667 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: expectedReturnValue}) 668 require.NoError(t, err) 669 670 requireRuntimeLocationStackPointerEqual(t, uint64(2), compiler) 671 // The function result value must be set at the bottom of the stack. 672 err = compiler.compileSet(&wazeroir.OperationSet{Depth: int(compiler.runtimeValueLocationStack().sp - 1)}) 673 require.NoError(t, err) 674 err = compiler.compileReturnFunction() 675 require.NoError(t, err) 676 677 c, _, err := compiler.compile() 678 require.NoError(t, err) 679 680 // Now that we've generated the code for this function, 681 // add it to the module engine and assign its pointer to the table index. 682 f := &function{ 683 parent: &code{codeSegment: c}, 684 codeInitialAddress: uintptr(unsafe.Pointer(&c[0])), 685 moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)), 686 source: &wasm.FunctionInstance{ 687 TypeID: targetTypeID, 688 }, 689 } 690 me.functions = append(me.functions, f) 691 table[i] = uintptr(unsafe.Pointer(f)) 692 } 693 694 // Test to ensure that we can call all the functions stored in the table. 695 for i := 1; i < len(table); i++ { 696 expectedReturnValue := uint32(i * 1000) 697 t.Run(fmt.Sprintf("%d", i), func(t *testing.T) { 698 compiler := env.requireNewCompiler(t, newCompiler, 699 &wazeroir.CompilationResult{ 700 Signature: &wasm.FunctionType{}, 701 Types: []*wasm.FunctionType{targetType}, 702 HasTable: true, 703 }, 704 ) 705 err := compiler.compilePreamble() 706 require.NoError(t, err) 707 708 // Place the offset value. Here we try calling a function of functionaddr == table[i].FunctionIndex. 709 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: uint32(i)}) 710 require.NoError(t, err) 711 712 // At this point, we should have one item (offset value) on the stack. 713 requireRuntimeLocationStackPointerEqual(t, 1, compiler) 714 715 require.NoError(t, compiler.compileCallIndirect(operation)) 716 717 // At this point, we consumed the offset value, but the function returns one value, 718 // so the stack pointer results in the same. 719 requireRuntimeLocationStackPointerEqual(t, 1, compiler) 720 721 err = compiler.compileReturnFunction() 722 require.NoError(t, err) 723 724 // Generate the code under test and run. 725 code, _, err := compiler.compile() 726 require.NoError(t, err) 727 env.exec(code) 728 729 require.Equal(t, nativeCallStatusCodeReturned.String(), env.compilerStatus().String()) 730 require.Equal(t, uint64(1), env.stackPointer()) 731 require.Equal(t, expectedReturnValue, uint32(env.ce.popValue())) 732 }) 733 } 734 }) 735 } 736 737 // TestCompiler_callIndirect_largeTypeIndex ensures that non-trivial large type index works well during call_indirect. 738 // Note: any index larger than 8-bit range is considered as large for arm64 compiler. 739 func TestCompiler_callIndirect_largeTypeIndex(t *testing.T) { 740 env := newCompilerEnvironment() 741 table := make([]wasm.Reference, 1) 742 env.addTable(&wasm.TableInstance{References: table}) 743 // Ensure that the module instance has the type information for targetOperation.TypeIndex, 744 // and the typeID matches the table[targetOffset]'s type ID. 745 const typeIndex, typeID = 12345, 0 746 operation := &wazeroir.OperationCallIndirect{TypeIndex: typeIndex} 747 env.module().TypeIDs = make([]wasm.FunctionTypeID, typeIndex+1) 748 env.module().TypeIDs[typeIndex] = typeID 749 env.module().Engine = &moduleEngine{functions: []*function{}} 750 751 types := make([]*wasm.FunctionType, typeIndex+1) 752 types[typeIndex] = &wasm.FunctionType{} 753 754 me := env.moduleEngine() 755 { // Compiling call target. 756 compiler := env.requireNewCompiler(t, newCompiler, nil) 757 err := compiler.compilePreamble() 758 require.NoError(t, err) 759 err = compiler.compileReturnFunction() 760 require.NoError(t, err) 761 762 c, _, err := compiler.compile() 763 require.NoError(t, err) 764 765 f := &function{ 766 parent: &code{codeSegment: c}, 767 codeInitialAddress: uintptr(unsafe.Pointer(&c[0])), 768 moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)), 769 source: &wasm.FunctionInstance{TypeID: 0}, 770 } 771 me.functions = append(me.functions, f) 772 table[0] = uintptr(unsafe.Pointer(f)) 773 } 774 775 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 776 Signature: &wasm.FunctionType{}, 777 Types: types, 778 HasTable: true, 779 }) 780 err := compiler.compilePreamble() 781 require.NoError(t, err) 782 783 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 0}) 784 require.NoError(t, err) 785 786 require.NoError(t, compiler.compileCallIndirect(operation)) 787 788 err = compiler.compileReturnFunction() 789 require.NoError(t, err) 790 791 // Generate the code under test and run. 792 code, _, err := compiler.compile() 793 require.NoError(t, err) 794 env.exec(code) 795 } 796 797 func TestCompiler_compileCall(t *testing.T) { 798 env := newCompilerEnvironment() 799 me := env.moduleEngine() 800 expectedValue := uint32(0) 801 802 // Emit the call target function. 803 const numCalls = 3 804 targetFunctionType := &wasm.FunctionType{ 805 Params: []wasm.ValueType{wasm.ValueTypeI32}, 806 Results: []wasm.ValueType{wasm.ValueTypeI32}, 807 ParamNumInUint64: 1, ResultNumInUint64: 1, 808 } 809 for i := 0; i < numCalls; i++ { 810 // Each function takes one argument, adds the value with 100 + i and returns the result. 811 addTargetValue := uint32(100 + i) 812 expectedValue += addTargetValue 813 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 814 Signature: targetFunctionType, 815 }) 816 817 err := compiler.compilePreamble() 818 require.NoError(t, err) 819 820 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: addTargetValue}) 821 require.NoError(t, err) 822 // Picks the function argument placed at the bottom of the stack. 823 err = compiler.compilePick(&wazeroir.OperationPick{Depth: int(compiler.runtimeValueLocationStack().sp - 1)}) 824 require.NoError(t, err) 825 // Adds the const to the picked value. 826 err = compiler.compileAdd(&wazeroir.OperationAdd{Type: wazeroir.UnsignedTypeI32}) 827 require.NoError(t, err) 828 // Then store the added result into the bottom of the stack (which is treated as the result of the function). 829 err = compiler.compileSet(&wazeroir.OperationSet{Depth: int(compiler.runtimeValueLocationStack().sp - 1)}) 830 require.NoError(t, err) 831 832 err = compiler.compileReturnFunction() 833 require.NoError(t, err) 834 835 c, _, err := compiler.compile() 836 require.NoError(t, err) 837 index := wasm.Index(i) 838 me.functions = append(me.functions, &function{ 839 parent: &code{codeSegment: c}, 840 codeInitialAddress: uintptr(unsafe.Pointer(&c[0])), 841 moduleInstanceAddress: uintptr(unsafe.Pointer(env.moduleInstance)), 842 }) 843 env.module().Functions = append(env.module().Functions, 844 &wasm.FunctionInstance{Type: targetFunctionType, Idx: index}) 845 } 846 847 // Now we start building the caller's code. 848 compiler := env.requireNewCompiler(t, newCompiler, &wazeroir.CompilationResult{ 849 Signature: &wasm.FunctionType{}, 850 Functions: make([]uint32, numCalls), 851 Types: []*wasm.FunctionType{targetFunctionType}, 852 }) 853 854 err := compiler.compilePreamble() 855 require.NoError(t, err) 856 857 const initialValue = 100 858 expectedValue += initialValue 859 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: 1234}) // Dummy value so the base pointer would be non-trivial for callees. 860 require.NoError(t, err) 861 err = compiler.compileConstI32(&wazeroir.OperationConstI32{Value: initialValue}) 862 require.NoError(t, err) 863 864 // Call all the built functions. 865 for i := 0; i < numCalls; i++ { 866 err = compiler.compileCall(&wazeroir.OperationCall{FunctionIndex: uint32(i)}) 867 require.NoError(t, err) 868 } 869 870 // Set the result slot 871 err = compiler.compileReturnFunction() 872 require.NoError(t, err) 873 874 code, _, err := compiler.compile() 875 require.NoError(t, err) 876 env.exec(code) 877 878 // Check status and returned values. 879 require.Equal(t, nativeCallStatusCodeReturned, env.compilerStatus()) 880 require.Equal(t, uint64(0), env.stackBasePointer()) 881 require.Equal(t, uint64(2), env.stackPointer()) // Must be 2 (dummy value + the calculation results) 882 require.Equal(t, expectedValue, env.stackTopAsUint32()) 883 }