github.com/wasilibs/wazerox@v0.0.0-20240124024944-4923be63ab5f/internal/engine/wazevo/ssa/pass_test.go (about)

     1  package ssa
     2  
     3  import (
     4  	"testing"
     5  
     6  	"github.com/wasilibs/wazerox/internal/testing/require"
     7  )
     8  
     9  func TestBuilder_passes(t *testing.T) {
    10  	for _, tc := range []struct {
    11  		name string
    12  		// pass is the optimization pass to run.
    13  		pass,
    14  		// postPass is run after the pass is executed, and can be used to test a pass that depends on another pass.
    15  		postPass func(b *builder)
    16  		// setup creates the SSA function in the given *builder.
    17  		// TODO: when we have the text SSA IR parser, we can eliminate this `setup`,
    18  		// 	we could directly decode the *builder from the `before` string. I am still
    19  		//  constantly changing the format, so let's keep setup for now.
    20  		// `verifier` is executed after executing pass, and can be used to
    21  		// do the additional verification of the state of SSA function in addition to `after` text result.
    22  		setup func(*builder) (verifier func(t *testing.T))
    23  		// before is the expected SSA function after `setup` is executed.
    24  		before,
    25  		// after is the expected output after optimization pass.
    26  		after string
    27  	}{
    28  		{
    29  			name: "dead block",
    30  			pass: passDeadBlockEliminationOpt,
    31  			setup: func(b *builder) func(*testing.T) {
    32  				entry := b.AllocateBasicBlock()
    33  				value := entry.AddParam(b, TypeI32)
    34  
    35  				middle1, middle2 := b.AllocateBasicBlock(), b.AllocateBasicBlock()
    36  				end := b.AllocateBasicBlock()
    37  
    38  				b.SetCurrentBlock(entry)
    39  				{
    40  					brz := b.AllocateInstruction()
    41  					brz.AsBrz(value, nil, middle1)
    42  					b.InsertInstruction(brz)
    43  
    44  					jmp := b.AllocateInstruction()
    45  					jmp.AsJump(nil, middle2)
    46  					b.InsertInstruction(jmp)
    47  				}
    48  
    49  				b.SetCurrentBlock(middle1)
    50  				{
    51  					jmp := b.AllocateInstruction()
    52  					jmp.AsJump(nil, end)
    53  					b.InsertInstruction(jmp)
    54  				}
    55  
    56  				b.SetCurrentBlock(middle2)
    57  				{
    58  					jmp := b.AllocateInstruction()
    59  					jmp.AsJump(nil, end)
    60  					b.InsertInstruction(jmp)
    61  				}
    62  
    63  				{
    64  					unreachable := b.AllocateBasicBlock()
    65  					b.SetCurrentBlock(unreachable)
    66  					jmp := b.AllocateInstruction()
    67  					jmp.AsJump(nil, end)
    68  					b.InsertInstruction(jmp)
    69  				}
    70  
    71  				b.SetCurrentBlock(end)
    72  				{
    73  					jmp := b.AllocateInstruction()
    74  					jmp.AsJump(nil, middle1)
    75  					b.InsertInstruction(jmp)
    76  				}
    77  
    78  				b.Seal(entry)
    79  				b.Seal(middle1)
    80  				b.Seal(middle2)
    81  				b.Seal(end)
    82  				return nil
    83  			},
    84  			before: `
    85  blk0: (v0:i32)
    86  	Brz v0, blk1
    87  	Jump blk2
    88  
    89  blk1: () <-- (blk0,blk3)
    90  	Jump blk3
    91  
    92  blk2: () <-- (blk0)
    93  	Jump blk3
    94  
    95  blk3: () <-- (blk1,blk2,blk4)
    96  	Jump blk1
    97  
    98  blk4: ()
    99  	Jump blk3
   100  `,
   101  			after: `
   102  blk0: (v0:i32)
   103  	Brz v0, blk1
   104  	Jump blk2
   105  
   106  blk1: () <-- (blk0,blk3)
   107  	Jump blk3
   108  
   109  blk2: () <-- (blk0)
   110  	Jump blk3
   111  
   112  blk3: () <-- (blk1,blk2)
   113  	Jump blk1
   114  `,
   115  		},
   116  		{
   117  			name: "redundant phis",
   118  			pass: passRedundantPhiEliminationOpt,
   119  			setup: func(b *builder) func(*testing.T) {
   120  				entry, loopHeader, end := b.AllocateBasicBlock(), b.AllocateBasicBlock(), b.AllocateBasicBlock()
   121  
   122  				loopHeader.AddParam(b, TypeI32)
   123  				var1 := b.DeclareVariable(TypeI32)
   124  
   125  				b.SetCurrentBlock(entry)
   126  				{
   127  					constInst := b.AllocateInstruction()
   128  					constInst.AsIconst32(0xff)
   129  					b.InsertInstruction(constInst)
   130  					iConst := constInst.Return()
   131  					b.DefineVariable(var1, iConst, entry)
   132  
   133  					jmp := b.AllocateInstruction()
   134  					jmp.AsJump([]Value{iConst}, loopHeader)
   135  					b.InsertInstruction(jmp)
   136  				}
   137  				b.Seal(entry)
   138  
   139  				b.SetCurrentBlock(loopHeader)
   140  				{
   141  					// At this point, loop is not sealed, so PHI will be added to this header. However, the only
   142  					// input to the PHI is iConst above, so there must be an alias to iConst from the PHI value.
   143  					value := b.MustFindValue(var1)
   144  
   145  					tmpInst := b.AllocateInstruction()
   146  					tmpInst.AsIconst32(0xff)
   147  					b.InsertInstruction(tmpInst)
   148  					tmp := tmpInst.Return()
   149  
   150  					brz := b.AllocateInstruction()
   151  					brz.AsBrz(value, []Value{tmp}, loopHeader) // Loop to itself.
   152  					b.InsertInstruction(brz)
   153  
   154  					jmp := b.AllocateInstruction()
   155  					jmp.AsJump(nil, end)
   156  					b.InsertInstruction(jmp)
   157  				}
   158  				b.Seal(loopHeader)
   159  
   160  				b.SetCurrentBlock(end)
   161  				{
   162  					ret := b.AllocateInstruction()
   163  					ret.AsReturn(nil)
   164  					b.InsertInstruction(ret)
   165  				}
   166  				return nil
   167  			},
   168  			before: `
   169  blk0: ()
   170  	v1:i32 = Iconst_32 0xff
   171  	Jump blk1, v1, v1
   172  
   173  blk1: (v0:i32,v2:i32) <-- (blk0,blk1)
   174  	v3:i32 = Iconst_32 0xff
   175  	Brz v2, blk1, v3, v2
   176  	Jump blk2
   177  
   178  blk2: () <-- (blk1)
   179  	Return
   180  `,
   181  			after: `
   182  blk0: ()
   183  	v1:i32 = Iconst_32 0xff
   184  	Jump blk1, v1
   185  
   186  blk1: (v0:i32) <-- (blk0,blk1)
   187  	v3:i32 = Iconst_32 0xff
   188  	Brz v2, blk1, v3
   189  	Jump blk2
   190  
   191  blk2: () <-- (blk1)
   192  	Return
   193  `,
   194  		},
   195  		{
   196  			name: "dead code",
   197  			pass: passDeadCodeEliminationOpt,
   198  			setup: func(b *builder) func(*testing.T) {
   199  				entry, end := b.AllocateBasicBlock(), b.AllocateBasicBlock()
   200  
   201  				b.SetCurrentBlock(entry)
   202  				iconstRefThriceInst := b.AllocateInstruction()
   203  				iconstRefThriceInst.AsIconst32(3)
   204  				b.InsertInstruction(iconstRefThriceInst)
   205  				refThriceVal := iconstRefThriceInst.Return()
   206  
   207  				// This has side effect.
   208  				store := b.AllocateInstruction()
   209  				store.AsStore(OpcodeStore, refThriceVal, refThriceVal, 0)
   210  				b.InsertInstruction(store)
   211  
   212  				iconstDeadInst := b.AllocateInstruction()
   213  				iconstDeadInst.AsIconst32(0)
   214  				b.InsertInstruction(iconstDeadInst)
   215  
   216  				iconstRefOnceInst := b.AllocateInstruction()
   217  				iconstRefOnceInst.AsIconst32(1)
   218  				b.InsertInstruction(iconstRefOnceInst)
   219  				refOnceVal := iconstRefOnceInst.Return()
   220  
   221  				jmp := b.AllocateInstruction()
   222  				jmp.AsJump(nil, end)
   223  				b.InsertInstruction(jmp)
   224  
   225  				b.SetCurrentBlock(end)
   226  				aliasedRefOnceVal := b.allocateValue(refOnceVal.Type())
   227  				b.alias(aliasedRefOnceVal, refOnceVal)
   228  
   229  				add := b.AllocateInstruction()
   230  				add.AsIadd(aliasedRefOnceVal, refThriceVal)
   231  				b.InsertInstruction(add)
   232  
   233  				addRes := add.Return()
   234  
   235  				ret := b.AllocateInstruction()
   236  				ret.AsReturn([]Value{addRes})
   237  				b.InsertInstruction(ret)
   238  				return func(t *testing.T) {
   239  					// Group IDs.
   240  					const gid0, gid1, gid2 InstructionGroupID = 0, 1, 2
   241  					require.Equal(t, gid0, iconstRefThriceInst.gid)
   242  					require.Equal(t, gid0, store.gid)
   243  					require.Equal(t, gid1, iconstDeadInst.gid)
   244  					require.Equal(t, gid1, iconstRefOnceInst.gid)
   245  					require.Equal(t, gid1, jmp.gid)
   246  					// Different blocks have different gids.
   247  					require.Equal(t, gid2, add.gid)
   248  					require.Equal(t, gid2, ret.gid)
   249  
   250  					// Dead or Alive...
   251  					require.False(t, iconstDeadInst.live)
   252  					require.True(t, iconstRefOnceInst.live)
   253  					require.True(t, iconstRefThriceInst.live)
   254  					require.True(t, add.live)
   255  					require.True(t, jmp.live)
   256  					require.True(t, ret.live)
   257  
   258  					require.Equal(t, 1, b.valueRefCounts[refOnceVal.ID()])
   259  					require.Equal(t, 1, b.valueRefCounts[addRes.ID()])
   260  					require.Equal(t, 3, b.valueRefCounts[refThriceVal.ID()])
   261  				}
   262  			},
   263  			before: `
   264  blk0: ()
   265  	v0:i32 = Iconst_32 0x3
   266  	Store v0, v0, 0x0
   267  	v1:i32 = Iconst_32 0x0
   268  	v2:i32 = Iconst_32 0x1
   269  	Jump blk1
   270  
   271  blk1: () <-- (blk0)
   272  	v4:i32 = Iadd v3, v0
   273  	Return v4
   274  `,
   275  			after: `
   276  blk0: ()
   277  	v0:i32 = Iconst_32 0x3
   278  	Store v0, v0, 0x0
   279  	v2:i32 = Iconst_32 0x1
   280  	Jump blk1
   281  
   282  blk1: () <-- (blk0)
   283  	v4:i32 = Iadd v2, v0
   284  	Return v4
   285  `,
   286  		},
   287  		{
   288  			name:     "nop elimination",
   289  			pass:     passNopInstElimination,
   290  			postPass: passDeadCodeEliminationOpt,
   291  			setup: func(b *builder) (verifier func(t *testing.T)) {
   292  				entry := b.AllocateBasicBlock()
   293  				b.SetCurrentBlock(entry)
   294  
   295  				i32Param := entry.AddParam(b, TypeI32)
   296  				i64Param := entry.AddParam(b, TypeI64)
   297  
   298  				// 32-bit shift.
   299  				moduleZeroI32 := b.AllocateInstruction().AsIconst32(32 * 245).Insert(b).Return()
   300  				nopIshl := b.AllocateInstruction().AsIshl(i32Param, moduleZeroI32).Insert(b).Return()
   301  
   302  				// 64-bit shift.
   303  				moduleZeroI64 := b.AllocateInstruction().AsIconst64(64 * 245).Insert(b).Return()
   304  				nopUshr := b.AllocateInstruction().AsUshr(i64Param, moduleZeroI64).Insert(b).Return()
   305  
   306  				// Non zero shift amount should not be eliminated.
   307  				nonZeroI32 := b.AllocateInstruction().AsIconst32(32*245 + 1).Insert(b).Return()
   308  				nonZeroIshl := b.AllocateInstruction().AsIshl(i32Param, nonZeroI32).Insert(b).Return()
   309  
   310  				nonZeroI64 := b.AllocateInstruction().AsIconst64(64*245 + 1).Insert(b).Return()
   311  				nonZeroSshr := b.AllocateInstruction().AsSshr(i64Param, nonZeroI64).Insert(b).Return()
   312  
   313  				ret := b.AllocateInstruction()
   314  				ret.AsReturn([]Value{nopIshl, nopUshr, nonZeroIshl, nonZeroSshr})
   315  				b.InsertInstruction(ret)
   316  				return nil
   317  			},
   318  			before: `
   319  blk0: (v0:i32, v1:i64)
   320  	v2:i32 = Iconst_32 0x1ea0
   321  	v3:i32 = Ishl v0, v2
   322  	v4:i64 = Iconst_64 0x3d40
   323  	v5:i64 = Ushr v1, v4
   324  	v6:i32 = Iconst_32 0x1ea1
   325  	v7:i32 = Ishl v0, v6
   326  	v8:i64 = Iconst_64 0x3d41
   327  	v9:i64 = Sshr v1, v8
   328  	Return v3, v5, v7, v9
   329  `,
   330  			after: `
   331  blk0: (v0:i32, v1:i64)
   332  	v6:i32 = Iconst_32 0x1ea1
   333  	v7:i32 = Ishl v0, v6
   334  	v8:i64 = Iconst_64 0x3d41
   335  	v9:i64 = Sshr v1, v8
   336  	Return v0, v1, v7, v9
   337  `,
   338  		},
   339  	} {
   340  		tc := tc
   341  		t.Run(tc.name, func(t *testing.T) {
   342  			b := NewBuilder().(*builder)
   343  			verifier := tc.setup(b)
   344  			require.Equal(t, tc.before, b.Format())
   345  			tc.pass(b)
   346  			if verifier != nil {
   347  				verifier(t)
   348  			}
   349  			if tc.postPass != nil {
   350  				tc.postPass(b)
   351  			}
   352  			require.Equal(t, tc.after, b.Format())
   353  		})
   354  	}
   355  }