github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/engine/wazevo/ssa/builder_test.go (about) 1 package ssa 2 3 import ( 4 "testing" 5 6 "github.com/wasilibs/wazerox/internal/testing/require" 7 ) 8 9 func Test_maybeInvertBranch(t *testing.T) { 10 insertJump := func(b *builder, src, dst *basicBlock) { 11 b.SetCurrentBlock(src) 12 jump := b.AllocateInstruction() 13 jump.AsJump(nil, dst) 14 b.InsertInstruction(jump) 15 } 16 17 insertBrz := func(b *builder, src, dst *basicBlock) { 18 b.SetCurrentBlock(src) 19 vinst := b.AllocateInstruction() 20 vinst.AsIconst32(0) 21 b.InsertInstruction(vinst) 22 v := vinst.Return() 23 brz := b.AllocateInstruction() 24 brz.AsBrz(v, nil, dst) 25 b.InsertInstruction(brz) 26 } 27 28 for _, tc := range []struct { 29 name string 30 setup func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) 31 exp bool 32 }{ 33 { 34 name: "ends with br_table", 35 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 36 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 37 inst := b.AllocateInstruction() 38 // TODO: we haven't implemented AsBrTable on Instruction. 39 inst.opcode = OpcodeBrTable 40 now.currentInstr = inst 41 verify = func(t *testing.T) { require.Equal(t, OpcodeBrTable, inst.opcode) } 42 return 43 }, 44 }, 45 { 46 name: "no conditional branch without previous instruction", 47 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 48 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 49 insertJump(b, now, next) 50 verify = func(t *testing.T) { 51 tail := now.currentInstr 52 require.Equal(t, OpcodeJump, tail.opcode) 53 } 54 return 55 }, 56 }, 57 { 58 name: "no conditional branch with previous instruction", 59 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 60 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 61 b.SetCurrentBlock(now) 62 prev := b.AllocateInstruction() 63 prev.AsIconst64(1) 64 b.InsertInstruction(prev) 65 insertJump(b, now, next) 66 verify = func(t *testing.T) { 67 tail := now.currentInstr 68 require.Equal(t, OpcodeJump, tail.opcode) 69 require.Equal(t, prev, tail.prev) 70 } 71 return 72 }, 73 }, 74 { 75 name: "tail target is already loop", 76 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 77 now, next, loopHeader, dummy := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 78 loopHeader.loopHeader = true 79 insertBrz(b, now, dummy) 80 insertJump(b, now, loopHeader) 81 verify = func(t *testing.T) { 82 tail := now.currentInstr 83 conditionalBr := tail.prev 84 require.Equal(t, OpcodeJump, tail.opcode) 85 require.Equal(t, OpcodeBrz, conditionalBr.opcode) // intact. 86 require.Equal(t, conditionalBr, tail.prev) 87 } 88 return 89 }, 90 }, 91 { 92 name: "tail target is already the next block", 93 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 94 now, next, dummy := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 95 insertBrz(b, now, dummy) 96 insertJump(b, now, next) 97 verify = func(t *testing.T) { 98 tail := now.currentInstr 99 conditionalBr := tail.prev 100 require.Equal(t, OpcodeJump, tail.opcode) 101 require.Equal(t, OpcodeBrz, conditionalBr.opcode) // intact. 102 require.Equal(t, conditionalBr, tail.prev) 103 } 104 return 105 }, 106 }, 107 { 108 name: "conditional target is loop", 109 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 110 now, next, loopHeader := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 111 loopHeader.loopHeader = true 112 insertBrz(b, now, loopHeader) // jump to loop, which needs inversion. 113 insertJump(b, now, next) 114 115 tail := now.currentInstr 116 conditionalBr := tail.prev 117 118 // Sanity check before inversion. 119 require.Equal(t, conditionalBr, loopHeader.preds[0].branch) 120 require.Equal(t, tail, next.preds[0].branch) 121 verify = func(t *testing.T) { 122 require.Equal(t, OpcodeJump, tail.opcode) 123 require.Equal(t, OpcodeBrnz, conditionalBr.opcode) // inversion. 124 require.Equal(t, loopHeader, tail.blk) // swapped. 125 require.Equal(t, next, conditionalBr.blk) // swapped. 126 require.Equal(t, conditionalBr, tail.prev) 127 128 // Predecessor info should correctly point to the inverted jump instruction. 129 require.Equal(t, tail, loopHeader.preds[0].branch) 130 require.Equal(t, conditionalBr, next.preds[0].branch) 131 } 132 return 133 }, 134 exp: true, 135 }, 136 { 137 name: "conditional target is the next block", 138 setup: func(b *builder) (now, next *basicBlock, verify func(t *testing.T)) { 139 now, next = b.allocateBasicBlock(), b.allocateBasicBlock() 140 nowTarget := b.allocateBasicBlock() 141 insertBrz(b, now, next) // jump to the next block in conditional, which needs inversion. 142 insertJump(b, now, nowTarget) 143 144 tail := now.currentInstr 145 conditionalBr := tail.prev 146 147 // Sanity check before inversion. 148 require.Equal(t, tail, nowTarget.preds[0].branch) 149 require.Equal(t, conditionalBr, next.preds[0].branch) 150 151 verify = func(t *testing.T) { 152 require.Equal(t, OpcodeJump, tail.opcode) 153 require.Equal(t, OpcodeBrnz, conditionalBr.opcode) // inversion. 154 require.Equal(t, next, tail.blk) // swapped. 155 require.Equal(t, nowTarget, conditionalBr.blk) // swapped. 156 require.Equal(t, conditionalBr, tail.prev) 157 158 require.Equal(t, conditionalBr, nowTarget.preds[0].branch) 159 require.Equal(t, tail, next.preds[0].branch) 160 } 161 return 162 }, 163 exp: true, 164 }, 165 } { 166 t.Run(tc.name, func(t *testing.T) { 167 b := NewBuilder().(*builder) 168 now, next, verify := tc.setup(b) 169 actual := maybeInvertBranches(now, next) 170 verify(t) 171 require.Equal(t, tc.exp, actual) 172 }) 173 } 174 } 175 176 func TestBuilder_splitCriticalEdge(t *testing.T) { 177 b := NewBuilder().(*builder) 178 predBlk, dummyBlk, dummyBlk2 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 179 predBlk.reversePostOrder = 100 180 b.SetCurrentBlock(predBlk) 181 inst := b.AllocateInstruction() 182 inst.AsIconst32(1) 183 b.InsertInstruction(inst) 184 v := inst.Return() 185 originalBrz := b.AllocateInstruction() // This is the split edge. 186 originalBrz.AsBrz(v, nil, dummyBlk) 187 b.InsertInstruction(originalBrz) 188 dummyJump := b.AllocateInstruction() 189 dummyJump.AsJump(nil, dummyBlk2) 190 b.InsertInstruction(dummyJump) 191 192 predInfo := &basicBlockPredecessorInfo{blk: predBlk, branch: originalBrz} 193 trampoline := b.splitCriticalEdge(predBlk, dummyBlk, predInfo) 194 require.NotNil(t, trampoline) 195 require.Equal(t, 100, trampoline.reversePostOrder) 196 197 require.Equal(t, trampoline, predInfo.blk) 198 require.Equal(t, originalBrz, predInfo.branch) 199 require.Equal(t, trampoline.rootInstr, predInfo.branch) 200 require.Equal(t, trampoline.currentInstr, predInfo.branch) 201 require.Equal(t, trampoline.success[0], dummyBlk) 202 203 replacedBrz := predBlk.rootInstr.next 204 require.Equal(t, OpcodeBrz, replacedBrz.opcode) 205 require.Equal(t, trampoline, replacedBrz.blk) 206 } 207 208 func Test_swapInstruction(t *testing.T) { 209 t.Run("swap root", func(t *testing.T) { 210 b := NewBuilder().(*builder) 211 blk := b.allocateBasicBlock() 212 213 dummy := b.AllocateInstruction() 214 215 old := b.AllocateInstruction() 216 old.next, dummy.prev = dummy, old 217 newi := b.AllocateInstruction() 218 blk.rootInstr = old 219 swapInstruction(blk, old, newi) 220 221 require.Equal(t, newi, blk.rootInstr) 222 require.Equal(t, dummy, newi.next) 223 require.Equal(t, dummy.prev, newi) 224 require.Nil(t, old.next) 225 require.Nil(t, old.prev) 226 }) 227 t.Run("swap middle", func(t *testing.T) { 228 b := NewBuilder().(*builder) 229 blk := b.allocateBasicBlock() 230 b.SetCurrentBlock(blk) 231 i1, i2, i3 := b.AllocateInstruction(), b.AllocateInstruction(), b.AllocateInstruction() 232 i1.AsIconst32(1) 233 i2.AsIconst32(2) 234 i3.AsIconst32(3) 235 b.InsertInstruction(i1) 236 b.InsertInstruction(i2) 237 b.InsertInstruction(i3) 238 239 newi := b.AllocateInstruction() 240 newi.AsIconst32(100) 241 swapInstruction(blk, i2, newi) 242 243 require.Equal(t, i1, blk.rootInstr) 244 require.Equal(t, newi, i1.next) 245 require.Equal(t, i3, newi.next) 246 require.Equal(t, i1, newi.prev) 247 require.Equal(t, newi, i3.prev) 248 require.Nil(t, i2.next) 249 require.Nil(t, i2.prev) 250 }) 251 t.Run("swap tail", func(t *testing.T) { 252 b := NewBuilder().(*builder) 253 blk := b.allocateBasicBlock() 254 b.SetCurrentBlock(blk) 255 i1, i2 := b.AllocateInstruction(), b.AllocateInstruction() 256 i1.AsIconst32(1) 257 i2.AsIconst32(2) 258 b.InsertInstruction(i1) 259 b.InsertInstruction(i2) 260 261 newi := b.AllocateInstruction() 262 newi.AsIconst32(100) 263 swapInstruction(blk, i2, newi) 264 265 require.Equal(t, i1, blk.rootInstr) 266 require.Equal(t, newi, blk.currentInstr) 267 require.Equal(t, newi, i1.next) 268 require.Equal(t, i1, newi.prev) 269 require.Nil(t, newi.next) 270 require.Nil(t, i2.next) 271 require.Nil(t, i2.prev) 272 }) 273 } 274 275 func TestBuilder_LayoutBlocks(t *testing.T) { 276 insertJump := func(b *builder, src, dst *basicBlock, vs ...Value) { 277 b.SetCurrentBlock(src) 278 jump := b.AllocateInstruction() 279 jump.AsJump(vs, dst) 280 b.InsertInstruction(jump) 281 } 282 283 insertBrz := func(b *builder, src, dst *basicBlock, condVal Value, vs ...Value) { 284 b.SetCurrentBlock(src) 285 vinst := b.AllocateInstruction().AsIconst32(0) 286 b.InsertInstruction(vinst) 287 brz := b.AllocateInstruction() 288 brz.AsBrz(condVal, vs, dst) 289 b.InsertInstruction(brz) 290 } 291 292 for _, tc := range []struct { 293 name string 294 setup func(b *builder) 295 exp []BasicBlockID 296 }{ 297 { 298 name: "sequential - no critical edge", 299 setup: func(b *builder) { 300 b1, b2, b3, b4 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 301 insertJump(b, b1, b2) 302 insertJump(b, b2, b3) 303 insertJump(b, b3, b4) 304 b.Seal(b1) 305 b.Seal(b2) 306 b.Seal(b3) 307 b.Seal(b4) 308 }, 309 exp: []BasicBlockID{0, 1, 2, 3}, 310 }, 311 { 312 name: "sequential with unreachable predecessor", 313 setup: func(b *builder) { 314 b0, unreachable, b2 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 315 insertJump(b, b0, b2) 316 insertJump(b, unreachable, b2) 317 unreachable.invalid = true 318 b.Seal(b0) 319 b.Seal(unreachable) 320 b.Seal(b2) 321 }, 322 exp: []BasicBlockID{0, 2}, 323 }, 324 { 325 name: "merge - no critical edge", 326 // 0 -> 1 -> 3 327 // | ^ 328 // v | 329 // 2 --------- 330 setup: func(b *builder) { 331 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 332 b.SetCurrentBlock(b0) 333 c := b.AllocateInstruction().AsIconst32(0) 334 b.InsertInstruction(c) 335 insertBrz(b, b0, b2, c.Return()) 336 insertJump(b, b0, b1) 337 insertJump(b, b1, b3) 338 insertJump(b, b2, b3) 339 b.Seal(b0) 340 b.Seal(b1) 341 b.Seal(b2) 342 b.Seal(b3) 343 }, 344 exp: []BasicBlockID{0, 1, 2, 3}, 345 }, 346 { 347 name: "loop towards loop header in fallthrough", 348 // 0 349 // v 350 // 1<--+ 351 // | | <---- critical 352 // 2---+ 353 // v 354 // 3 355 // 356 // ==> 357 // 358 // 0 359 // v 360 // 1<---+ 361 // | | 362 // 2--->4 363 // v 364 // 3 365 setup: func(b *builder) { 366 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 367 insertJump(b, b0, b1) 368 insertJump(b, b1, b2) 369 b.SetCurrentBlock(b2) 370 c := b.AllocateInstruction().AsIconst32(0) 371 b.InsertInstruction(c) 372 insertBrz(b, b2, b1, c.Return()) 373 insertJump(b, b2, b3) 374 b.Seal(b0) 375 b.Seal(b1) 376 b.Seal(b2) 377 b.Seal(b3) 378 }, 379 // The trampoline 4 is placed right after 2, which is the hot path of the loop. 380 exp: []BasicBlockID{0, 1, 2, 4, 3}, 381 }, 382 { 383 name: "loop - towards loop header in conditional branch", 384 // 0 385 // v 386 // 1<--+ 387 // | | <---- critical 388 // 2---+ 389 // v 390 // 3 391 // 392 // ==> 393 // 394 // 0 395 // v 396 // 1<---+ 397 // | | 398 // 2--->4 399 // v 400 // 3 401 setup: func(b *builder) { 402 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 403 insertJump(b, b0, b1) 404 insertJump(b, b1, b2) 405 b.SetCurrentBlock(b2) 406 c := b.AllocateInstruction().AsIconst32(0) 407 b.InsertInstruction(c) 408 insertBrz(b, b2, b3, c.Return()) 409 insertJump(b, b2, b1) 410 b.Seal(b0) 411 b.Seal(b1) 412 b.Seal(b2) 413 b.Seal(b3) 414 }, 415 // The trampoline 4 is placed right after 2, which is the hot path of the loop. 416 exp: []BasicBlockID{0, 1, 2, 4, 3}, 417 }, 418 { 419 name: "loop with header is critical backward edge", 420 // 0 421 // v 422 // 1<--+ 423 // / | | 424 // 3 2 | <--- critical 425 // \ | | 426 // 4---+ 427 // v 428 // 5 429 // 430 // ==> 431 // 432 // 0 433 // v 434 // 1<----+ 435 // / | | 436 // 3 2 | 437 // \ | | 438 // 4---->6 439 // v 440 // 5 441 setup: func(b *builder) { 442 b0, b1, b2, b3, b4, b5 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), 443 b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 444 insertJump(b, b0, b1) 445 b.SetCurrentBlock(b0) 446 c1 := b.AllocateInstruction().AsIconst32(0) 447 b.InsertInstruction(c1) 448 insertBrz(b, b1, b2, c1.Return()) 449 insertJump(b, b1, b3) 450 insertJump(b, b3, b4) 451 insertJump(b, b2, b4) 452 b.SetCurrentBlock(b4) 453 c2 := b.AllocateInstruction().AsIconst32(0) 454 b.InsertInstruction(c2) 455 insertBrz(b, b4, b1, c2.Return()) 456 insertJump(b, b4, b5) 457 b.Seal(b0) 458 b.Seal(b1) 459 b.Seal(b2) 460 b.Seal(b3) 461 b.Seal(b4) 462 b.Seal(b5) 463 }, 464 // The trampoline 6 is placed right after 4, which is the hot path of the loop. 465 exp: []BasicBlockID{0, 1, 3, 2, 4, 6, 5}, 466 }, 467 { 468 name: "multiple critical edges", 469 // 0 470 // v 471 // +---1<--+ 472 // | v | <---- critical 473 // critical ---->| 2 --+ 474 // | | <-------- critical 475 // | v 476 // +-->3--->4 477 // 478 // ==> 479 // 480 // 0 481 // v 482 // +---1<---+ 483 // | v | 484 // 5 2 -->6 485 // | v 486 // | 7 487 // | v 488 // +-->3--->4 489 setup: func(b *builder) { 490 b0, b1, b2, b3, b4 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), 491 b.allocateBasicBlock(), b.allocateBasicBlock() 492 insertJump(b, b0, b1) 493 b.SetCurrentBlock(b1) 494 c1 := b.AllocateInstruction().AsIconst32(0) 495 b.InsertInstruction(c1) 496 insertBrz(b, b1, b2, c1.Return()) 497 insertJump(b, b1, b3) 498 499 b.SetCurrentBlock(b2) 500 c2 := b.AllocateInstruction().AsIconst32(0) 501 b.InsertInstruction(c2) 502 insertBrz(b, b2, b1, c2.Return()) 503 insertJump(b, b2, b3) 504 insertJump(b, b3, b4) 505 506 b.Seal(b0) 507 b.Seal(b1) 508 b.Seal(b2) 509 b.Seal(b3) 510 b.Seal(b4) 511 }, 512 exp: []BasicBlockID{ 513 0, 1, 514 // block 2 has loop header (1) as the conditional branch target, so it's inverted, 515 // and the split edge trampoline is placed right after 2 which is the hot path of the loop. 516 2, 6, 517 // Then the placement iteration goes to 3, which has two (5, 7) unplaced trampolines as predecessors, 518 // so they are placed before 3. 519 5, 7, 3, 520 // Then the final block. 521 4, 522 }, 523 }, 524 { 525 name: "brz with arg", 526 setup: func(b *builder) { 527 b0, b1, b2 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 528 p := b0.AddParam(b, TypeI32) 529 retval := b1.AddParam(b, TypeI32) 530 531 b.SetCurrentBlock(b0) 532 { 533 arg := b.AllocateInstruction().AsIconst32(1000).Insert(b).Return() 534 insertBrz(b, b0, b1, p, arg) 535 insertJump(b, b0, b2) 536 } 537 b.SetCurrentBlock(b1) 538 { 539 b.AllocateInstruction().AsReturn([]Value{retval}).Insert(b) 540 } 541 b.SetCurrentBlock(b2) 542 { 543 arg := b.AllocateInstruction().AsIconst32(1).Insert(b).Return() 544 insertJump(b, b2, b1, arg) 545 } 546 547 b.Seal(b0) 548 b.Seal(b1) 549 b.Seal(b2) 550 }, 551 exp: []BasicBlockID{0x0, 0x3, 0x1, 0x2}, 552 }, 553 { 554 name: "loop with output", 555 exp: []BasicBlockID{0x0, 0x2, 0x4, 0x1, 0x3, 0x6, 0x5}, 556 setup: func(b *builder) { 557 b.currentSignature = &Signature{Results: []Type{TypeI32}} 558 b0, b1, b2, b3 := b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock(), b.allocateBasicBlock() 559 560 b.SetCurrentBlock(b0) 561 funcParam := b0.AddParam(b, TypeI32) 562 b2Param := b2.AddParam(b, TypeI32) 563 insertJump(b, b0, b2, funcParam) 564 565 b.SetCurrentBlock(b1) 566 { 567 returnParam := b1.AddParam(b, TypeI32) 568 insertJump(b, b1, b.returnBlk, returnParam) 569 } 570 571 b.SetCurrentBlock(b2) 572 { 573 c := b.AllocateInstruction().AsIconst32(100).Insert(b) 574 cmp := b.AllocateInstruction(). 575 AsIcmp(b2Param, c.Return(), IntegerCmpCondUnsignedLessThan). 576 Insert(b) 577 insertBrz(b, b2, b1, cmp.Return(), b2Param) 578 insertJump(b, b2, b3) 579 } 580 581 b.SetCurrentBlock(b3) 582 { 583 one := b.AllocateInstruction().AsIconst32(1).Insert(b) 584 minusOned := b.AllocateInstruction().AsIsub(b2Param, one.Return()).Insert(b) 585 c := b.AllocateInstruction().AsIconst32(150).Insert(b) 586 cmp := b.AllocateInstruction(). 587 AsIcmp(b2Param, c.Return(), IntegerCmpCondEqual). 588 Insert(b) 589 insertBrz(b, b3, b1, cmp.Return(), minusOned.Return()) 590 insertJump(b, b3, b2, minusOned.Return()) 591 } 592 593 b.Seal(b0) 594 b.Seal(b1) 595 b.Seal(b2) 596 b.Seal(b3) 597 }, 598 }, 599 } { 600 tc := tc 601 t.Run(tc.name, func(t *testing.T) { 602 b := NewBuilder().(*builder) 603 tc.setup(b) 604 605 b.RunPasses() // LayoutBlocks() must be called after RunPasses(). 606 b.LayoutBlocks() 607 608 var actual []BasicBlockID 609 for blk := b.BlockIteratorReversePostOrderBegin(); blk != nil; blk = b.BlockIteratorReversePostOrderNext() { 610 actual = append(actual, blk.(*basicBlock).id) 611 } 612 require.Equal(t, tc.exp, actual) 613 }) 614 } 615 }