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

     1  package ssa
     2  
     3  import (
     4  	"sort"
     5  	"testing"
     6  
     7  	"github.com/wasilibs/wazerox/internal/testing/require"
     8  )
     9  
    10  func TestBuilder_passCalculateImmediateDominators(t *testing.T) {
    11  	for _, tc := range []struct {
    12  		name     string
    13  		edges    edgesCase
    14  		expDoms  map[BasicBlockID]BasicBlockID
    15  		expLoops map[BasicBlockID]struct{}
    16  	}{
    17  		{
    18  			name: "linear",
    19  			// 0 -> 1 -> 2 -> 3 -> 4
    20  			edges: edgesCase{
    21  				0: {1},
    22  				1: {2},
    23  				2: {3},
    24  				3: {4},
    25  			},
    26  			expDoms: map[BasicBlockID]BasicBlockID{
    27  				1: 0,
    28  				2: 1,
    29  				3: 2,
    30  				4: 3,
    31  			},
    32  		},
    33  		{
    34  			name: "diamond",
    35  			//  0
    36  			// / \
    37  			// 1   2
    38  			// \ /
    39  			//  3
    40  			edges: edgesCase{
    41  				0: {1, 2},
    42  				1: {3},
    43  				2: {3},
    44  			},
    45  			expDoms: map[BasicBlockID]BasicBlockID{
    46  				1: 0,
    47  				2: 0,
    48  				3: 0,
    49  			},
    50  		},
    51  		{
    52  			name: "merge",
    53  			// 0 -> 1 -> 3
    54  			// |         ^
    55  			// v         |
    56  			// 2 ---------
    57  			edges: edgesCase{
    58  				0: {1, 2},
    59  				1: {3},
    60  				2: {3},
    61  			},
    62  			expDoms: map[BasicBlockID]BasicBlockID{
    63  				1: 0,
    64  				2: 0,
    65  				3: 0,
    66  			},
    67  		},
    68  		{
    69  			name: "branch",
    70  			//  0
    71  			// / \
    72  			// 1   2
    73  			edges: edgesCase{
    74  				0: {1, 2},
    75  			},
    76  			expDoms: map[BasicBlockID]BasicBlockID{
    77  				1: 0,
    78  				2: 0,
    79  			},
    80  		},
    81  		{
    82  			name: "loop",
    83  			// 0 -> 1 -> 2
    84  			//      ^    |
    85  			//      |    v
    86  			//      |--- 3
    87  			edges: edgesCase{
    88  				0: {1},
    89  				1: {2},
    90  				2: {3},
    91  				3: {1},
    92  			},
    93  			expDoms: map[BasicBlockID]BasicBlockID{
    94  				1: 0,
    95  				2: 1,
    96  				3: 2,
    97  			},
    98  			expLoops: map[BasicBlockID]struct{}{1: {}},
    99  		},
   100  		{
   101  			name: "larger diamond",
   102  			//     0
   103  			//   / | \
   104  			//  1  2  3
   105  			//   \ | /
   106  			//     4
   107  			edges: edgesCase{
   108  				0: {1, 2, 3},
   109  				1: {4},
   110  				2: {4},
   111  				3: {4},
   112  			},
   113  			expDoms: map[BasicBlockID]BasicBlockID{
   114  				1: 0,
   115  				2: 0,
   116  				3: 0,
   117  				4: 0,
   118  			},
   119  		},
   120  		{
   121  			name: "two independent branches",
   122  			//  0
   123  			// / \
   124  			// 1   2
   125  			// |   |
   126  			// 3   4
   127  			edges: edgesCase{
   128  				0: {1, 2},
   129  				1: {3},
   130  				2: {4},
   131  			},
   132  			expDoms: map[BasicBlockID]BasicBlockID{
   133  				1: 0,
   134  				2: 0,
   135  				3: 1,
   136  				4: 2,
   137  			},
   138  		},
   139  		{
   140  			name: "branch",
   141  			// 0 -> 1 -> 2
   142  			//     |    |
   143  			//     v    v
   144  			//     3 <- 4
   145  			edges: edgesCase{
   146  				0: {1},
   147  				1: {2, 3},
   148  				2: {4},
   149  				4: {3},
   150  			},
   151  			expDoms: map[BasicBlockID]BasicBlockID{
   152  				1: 0,
   153  				2: 1,
   154  				3: 1,
   155  				4: 2,
   156  			},
   157  		},
   158  		{
   159  			name: "branches with merge",
   160  			//   0
   161  			// /  \
   162  			// 1   2
   163  			// \   /
   164  			// 3 > 4
   165  			edges: edgesCase{
   166  				0: {1, 2},
   167  				1: {3},
   168  				2: {4},
   169  				3: {4},
   170  			},
   171  			expDoms: map[BasicBlockID]BasicBlockID{
   172  				1: 0,
   173  				2: 0,
   174  				3: 1,
   175  				4: 0,
   176  			},
   177  		},
   178  		{
   179  			name: "cross branches",
   180  			//   0
   181  			//  / \
   182  			// 1   2
   183  			// |\ /|
   184  			// | X |
   185  			// |/ \|
   186  			// 3   4
   187  			edges: edgesCase{
   188  				0: {1, 2},
   189  				1: {3, 4},
   190  				2: {3, 4},
   191  			},
   192  			expDoms: map[BasicBlockID]BasicBlockID{
   193  				1: 0,
   194  				2: 0,
   195  				3: 0,
   196  				4: 0,
   197  			},
   198  		},
   199  		{
   200  			// Loop with multiple entries are not loops in the strict sense.
   201  			// See the comment on basicBlock.loopHeader.
   202  			// Note that WebAssembly program won't produce such CFGs. TODO: proof!
   203  			name: "nested loops with multiple entries",
   204  			//     0
   205  			//    / \
   206  			//   v   v
   207  			//   1 <> 2
   208  			//   ^    |
   209  			//   |    v
   210  			//   4 <- 3
   211  			edges: edgesCase{
   212  				0: {1, 2},
   213  				1: {2},
   214  				2: {1, 3},
   215  				3: {4},
   216  				4: {1},
   217  			},
   218  			expDoms: map[BasicBlockID]BasicBlockID{
   219  				1: 0,
   220  				2: 0,
   221  				3: 2,
   222  				4: 3,
   223  			},
   224  		},
   225  		{
   226  			name: "two intersecting loops",
   227  			//   0
   228  			//   v
   229  			//   1 --> 2 --> 3
   230  			//   ^     |     |
   231  			//   v     v     v
   232  			//   4 <-- 5 <-- 6
   233  			edges: edgesCase{
   234  				0: {1},
   235  				1: {2, 4},
   236  				2: {3, 5},
   237  				3: {6},
   238  				4: {1},
   239  				5: {4},
   240  				6: {5},
   241  			},
   242  			expDoms: map[BasicBlockID]BasicBlockID{
   243  				1: 0,
   244  				2: 1,
   245  				3: 2,
   246  				4: 1,
   247  				5: 2,
   248  				6: 3,
   249  			},
   250  			expLoops: map[BasicBlockID]struct{}{1: {}},
   251  		},
   252  		{
   253  			name: "loop back edges",
   254  			//     0
   255  			//     v
   256  			//     1 --> 2 --> 3 --> 4
   257  			//     ^           |     |
   258  			//     v           v     v
   259  			//     8 <-------- 6 <-- 5
   260  			edges: edgesCase{
   261  				0: {1},
   262  				1: {2, 8},
   263  				2: {3},
   264  				3: {4, 6},
   265  				4: {5},
   266  				5: {6},
   267  				6: {8},
   268  				8: {1},
   269  			},
   270  			expDoms: map[BasicBlockID]BasicBlockID{
   271  				1: 0,
   272  				2: 1,
   273  				3: 2,
   274  				4: 3,
   275  				5: 4,
   276  				6: 3,
   277  				8: 1,
   278  			},
   279  			expLoops: map[BasicBlockID]struct{}{1: {}},
   280  		},
   281  		{
   282  			name: "multiple independent paths",
   283  			//   0
   284  			//   v
   285  			//   1 --> 2 --> 3 --> 4 --> 5
   286  			//   |           ^     ^
   287  			//   v           |     |
   288  			//   6 --> 7 --> 8 --> 9
   289  			edges: edgesCase{
   290  				0: {1},
   291  				1: {2, 6},
   292  				2: {3},
   293  				3: {4},
   294  				4: {5},
   295  				6: {7},
   296  				7: {8},
   297  				8: {3, 9},
   298  				9: {4},
   299  			},
   300  			expDoms: map[BasicBlockID]BasicBlockID{
   301  				1: 0,
   302  				2: 1,
   303  				3: 1,
   304  				4: 1,
   305  				5: 4,
   306  				6: 1,
   307  				7: 6,
   308  				8: 7,
   309  				9: 8,
   310  			},
   311  		},
   312  		{
   313  			name: "double back edges",
   314  			//     0
   315  			//     v
   316  			//     1 --> 2 --> 3 --> 4 -> 5
   317  			//     ^                 |
   318  			//     v                 v
   319  			//     7 <--------------- 6
   320  			edges: edgesCase{
   321  				0: {1},
   322  				1: {2, 7},
   323  				2: {3},
   324  				3: {4},
   325  				4: {5, 6},
   326  				6: {7},
   327  				7: {1},
   328  			},
   329  			expDoms: map[BasicBlockID]BasicBlockID{
   330  				1: 0,
   331  				2: 1,
   332  				3: 2,
   333  				4: 3,
   334  				5: 4,
   335  				6: 4,
   336  				7: 1,
   337  			},
   338  			expLoops: map[BasicBlockID]struct{}{1: {}},
   339  		},
   340  		{
   341  			name: "double nested loops with branches",
   342  			//     0 --> 1 --> 2 --> 3 --> 4 --> 5 --> 6
   343  			//          ^     |            |     |
   344  			//          v     v            v     |
   345  			//          9 <-- 8 <--------- 7 <---|
   346  			edges: edgesCase{
   347  				0: {1},
   348  				1: {2, 9},
   349  				2: {3, 8},
   350  				3: {4},
   351  				4: {5, 7},
   352  				5: {6, 7},
   353  				7: {8},
   354  				8: {9},
   355  				9: {1},
   356  			},
   357  			expDoms: map[BasicBlockID]BasicBlockID{
   358  				1: 0,
   359  				2: 1,
   360  				3: 2,
   361  				4: 3,
   362  				5: 4,
   363  				6: 5,
   364  				7: 4,
   365  				8: 2,
   366  				9: 1,
   367  			},
   368  			expLoops: map[BasicBlockID]struct{}{1: {}},
   369  		},
   370  		{
   371  			name: "split paths with a loop",
   372  			//       0
   373  			//       v
   374  			//       1
   375  			//      / \
   376  			//     v   v
   377  			//     2<--3
   378  			//     ^   |
   379  			//     |   v
   380  			//     6<--4
   381  			//     |
   382  			//     v
   383  			//     5
   384  			edges: edgesCase{
   385  				0: {1},
   386  				1: {2, 3},
   387  				3: {2, 4},
   388  				4: {6},
   389  				6: {2, 5},
   390  			},
   391  			expDoms: map[BasicBlockID]BasicBlockID{
   392  				1: 0,
   393  				2: 1,
   394  				3: 1,
   395  				4: 3,
   396  				5: 6,
   397  				6: 4,
   398  			},
   399  		},
   400  		{
   401  			name: "multiple exits with a loop",
   402  			//     0
   403  			//     v
   404  			//     1
   405  			//    / \
   406  			//   v   v
   407  			//   2<--3
   408  			//   |
   409  			//   v
   410  			//   4<->5
   411  			//   |
   412  			//   v
   413  			//   6
   414  			edges: edgesCase{
   415  				0: {1},
   416  				1: {2, 3},
   417  				2: {4},
   418  				3: {2},
   419  				4: {5, 6},
   420  				5: {4},
   421  			},
   422  			expDoms: map[BasicBlockID]BasicBlockID{
   423  				1: 0,
   424  				2: 1,
   425  				3: 1,
   426  				4: 2,
   427  				5: 4,
   428  				6: 4,
   429  			},
   430  			expLoops: map[BasicBlockID]struct{}{4: {}},
   431  		},
   432  		{
   433  			name: "parallel loops with merge",
   434  			//       0
   435  			//       v
   436  			//       1
   437  			//      / \
   438  			//     v   v
   439  			//     3<--2
   440  			//     |
   441  			//     v
   442  			//     4<->5
   443  			//     |   |
   444  			//     v   v
   445  			//     7<->6
   446  			edges: edgesCase{
   447  				0: {1},
   448  				1: {2, 3},
   449  				2: {3},
   450  				3: {4},
   451  				4: {5, 7},
   452  				5: {4, 6},
   453  				6: {7},
   454  				7: {6},
   455  			},
   456  			expDoms: map[BasicBlockID]BasicBlockID{
   457  				1: 0,
   458  				2: 1,
   459  				3: 1,
   460  				4: 3,
   461  				5: 4,
   462  				6: 4,
   463  				7: 4,
   464  			},
   465  			expLoops: map[BasicBlockID]struct{}{4: {}},
   466  		},
   467  		{
   468  			name: "two independent loops",
   469  			//      0
   470  			//      |
   471  			//      v
   472  			//      1 --> 2 --> 3
   473  			//      ^           |
   474  			//      v           v
   475  			//      4 <---------5
   476  			//      |
   477  			//      v
   478  			//      6 --> 7 --> 8
   479  			//      ^           |
   480  			//      v           v
   481  			//      9 <---------10
   482  			edges: map[BasicBlockID][]BasicBlockID{
   483  				0:  {1},
   484  				1:  {2, 4},
   485  				2:  {3},
   486  				3:  {5},
   487  				4:  {1, 6},
   488  				5:  {4},
   489  				6:  {7, 9},
   490  				7:  {8},
   491  				8:  {10},
   492  				9:  {6},
   493  				10: {9},
   494  			},
   495  			expDoms: map[BasicBlockID]BasicBlockID{
   496  				1:  0,
   497  				2:  1,
   498  				3:  2,
   499  				4:  1,
   500  				5:  3,
   501  				6:  4,
   502  				7:  6,
   503  				8:  7,
   504  				9:  6,
   505  				10: 8,
   506  			},
   507  			expLoops: map[BasicBlockID]struct{}{1: {}, 6: {}},
   508  		},
   509  		{
   510  			name: "merge after loop",
   511  			edges: edgesCase{
   512  				0: {3, 1},
   513  				1: {2},
   514  				2: {1, 3},
   515  				3: {4},
   516  			},
   517  			expDoms: map[BasicBlockID]BasicBlockID{
   518  				1: 0,
   519  				2: 1,
   520  				3: 0,
   521  				4: 3,
   522  			},
   523  			expLoops: map[BasicBlockID]struct{}{1: {}},
   524  		},
   525  	} {
   526  		tc := tc
   527  		t.Run(tc.name, func(t *testing.T) {
   528  			b := constructGraphFromEdges(tc.edges)
   529  			passCalculateImmediateDominators(b)
   530  
   531  			for blockID, expDomID := range tc.expDoms {
   532  				expBlock := b.basicBlocksPool.View(int(expDomID))
   533  				require.Equal(t, expBlock, b.dominators[blockID],
   534  					"block %d expecting %d, but got %s", blockID, expDomID, b.dominators[blockID])
   535  			}
   536  
   537  			for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
   538  				_, expLoop := tc.expLoops[blk.id]
   539  				require.Equal(t, expLoop, blk.loopHeader, blk.String())
   540  			}
   541  		})
   542  	}
   543  }
   544  
   545  func TestBuildLoopNestingForest(t *testing.T) {
   546  	type expLoopNestingForest struct {
   547  		roots    []BasicBlockID
   548  		children map[BasicBlockID][]BasicBlockID
   549  	}
   550  
   551  	for _, tc := range []struct {
   552  		name   string
   553  		edges  edgesCase
   554  		expLNF expLoopNestingForest
   555  	}{
   556  		{
   557  			name: "linear",
   558  			// 0 -> 1 -> 2 -> 3 -> 4
   559  			edges: edgesCase{
   560  				0: {1},
   561  				1: {2},
   562  				2: {3},
   563  				3: {4},
   564  			},
   565  		},
   566  		{
   567  			name: "loop",
   568  			// 0 -> 1 -> 2
   569  			//      ^    |
   570  			//      |    v
   571  			//      |--- 3
   572  			edges: edgesCase{
   573  				0: {1},
   574  				1: {2},
   575  				2: {3},
   576  				3: {1},
   577  			},
   578  			expLNF: expLoopNestingForest{
   579  				roots: []BasicBlockID{1},
   580  				children: map[BasicBlockID][]BasicBlockID{
   581  					1: {2, 3},
   582  				},
   583  			},
   584  		},
   585  		{
   586  			name: "two independent loops",
   587  			//      0
   588  			//      |
   589  			//      v
   590  			//      1 --> 2 --> 3
   591  			//      ^           |
   592  			//      v           v
   593  			//      4 <---------5
   594  			//      |
   595  			//      v
   596  			//      6 --> 7 --> 8
   597  			//      ^           |
   598  			//      v           v
   599  			//      9 <---------10
   600  			edges: map[BasicBlockID][]BasicBlockID{
   601  				0:  {1},
   602  				1:  {2, 4},
   603  				2:  {3},
   604  				3:  {5},
   605  				4:  {1, 6},
   606  				5:  {4},
   607  				6:  {7, 9},
   608  				7:  {8},
   609  				8:  {10},
   610  				9:  {6},
   611  				10: {9},
   612  			},
   613  			expLNF: expLoopNestingForest{
   614  				roots: []BasicBlockID{1},
   615  				children: map[BasicBlockID][]BasicBlockID{
   616  					1: {2, 3, 4, 5, 6},
   617  					6: {7, 8, 9, 10},
   618  				},
   619  			},
   620  		},
   621  		{
   622  			//
   623  			//                  +-----+
   624  			//                  |     |
   625  			//                  v     |
   626  			//    0 ---> 1 ---> 2 --> 3 ---> 4
   627  			//           ^      |
   628  			//           |      |
   629  			//           +------+
   630  			//
   631  			name: "Fig. 9.2", // in "SSA-based Compiler Design".
   632  			edges: map[BasicBlockID][]BasicBlockID{
   633  				0: {1},
   634  				1: {2},
   635  				2: {1, 3},
   636  				3: {2, 4},
   637  			},
   638  			expLNF: expLoopNestingForest{
   639  				roots: []BasicBlockID{1},
   640  				children: map[BasicBlockID][]BasicBlockID{
   641  					1: {2},
   642  					2: {3, 4},
   643  				},
   644  			},
   645  		},
   646  	} {
   647  		tc := tc
   648  		t.Run(tc.name, func(t *testing.T) {
   649  			b := constructGraphFromEdges(tc.edges)
   650  			// buildLoopNestingForest requires passCalculateImmediateDominators to be done.
   651  			passCalculateImmediateDominators(b)
   652  			buildLoopNestingForest(b)
   653  
   654  			blocks := map[BasicBlockID]*basicBlock{}
   655  			for blk := b.blockIteratorBegin(); blk != nil; blk = b.blockIteratorNext() {
   656  				blocks[blk.id] = blk
   657  			}
   658  
   659  			// Check the result of buildLoopNestingForest.
   660  			var forestRoots []BasicBlockID
   661  			for _, root := range b.loopNestingForestRoots {
   662  				forestRoots = append(forestRoots, root.(*basicBlock).id)
   663  			}
   664  			sort.Slice(forestRoots, func(i, j int) bool {
   665  				return forestRoots[i] < forestRoots[j]
   666  			})
   667  			require.Equal(t, tc.expLNF.roots, forestRoots)
   668  
   669  			for expBlkID, blk := range blocks {
   670  				expChildren := tc.expLNF.children[expBlkID]
   671  				var actualChildren []BasicBlockID
   672  				for _, child := range blk.loopNestingForestChildren {
   673  					actualChildren = append(actualChildren, child.(*basicBlock).id)
   674  				}
   675  				sort.Slice(actualChildren, func(i, j int) bool {
   676  					return actualChildren[i] < actualChildren[j]
   677  				})
   678  				require.Equal(t, expChildren, actualChildren, "block %d", expBlkID)
   679  			}
   680  		})
   681  	}
   682  }
   683  
   684  func TestDominatorTree(t *testing.T) {
   685  	type lowestCommonAncestorCase struct {
   686  		a, b BasicBlockID
   687  		exp  BasicBlockID
   688  	}
   689  
   690  	for _, tc := range []struct {
   691  		name  string
   692  		edges edgesCase
   693  		cases []lowestCommonAncestorCase
   694  	}{
   695  		{
   696  			name: "linear",
   697  			// 0 -> 1 -> 2 -> 3 -> 4
   698  			edges: edgesCase{
   699  				0: {1},
   700  				1: {2},
   701  				2: {3},
   702  				3: {4},
   703  			},
   704  			cases: []lowestCommonAncestorCase{
   705  				{a: 0, b: 0, exp: 0},
   706  				{a: 0, b: 1, exp: 0},
   707  				{a: 0, b: 2, exp: 0},
   708  				{a: 0, b: 3, exp: 0},
   709  				{a: 0, b: 4, exp: 0},
   710  				{a: 1, b: 1, exp: 1},
   711  				{a: 1, b: 2, exp: 1},
   712  				{a: 1, b: 3, exp: 1},
   713  				{a: 1, b: 4, exp: 1},
   714  				{a: 2, b: 2, exp: 2},
   715  				{a: 2, b: 3, exp: 2},
   716  				{a: 2, b: 4, exp: 2},
   717  				{a: 3, b: 3, exp: 3},
   718  				{a: 3, b: 4, exp: 3},
   719  				{a: 4, b: 4, exp: 4},
   720  			},
   721  		},
   722  		{
   723  			//
   724  			//                  +-----+
   725  			//                  |     |
   726  			//                  v     |
   727  			//    0 ---> 1 ---> 2 --> 3 ---> 4
   728  			//           ^      |
   729  			//           |      |
   730  			//           +------+
   731  			//
   732  			name: "two loops",
   733  			edges: map[BasicBlockID][]BasicBlockID{
   734  				0: {1},
   735  				1: {2},
   736  				2: {1, 3},
   737  				3: {2, 4},
   738  			},
   739  			cases: []lowestCommonAncestorCase{
   740  				{a: 0, b: 0, exp: 0},
   741  				{a: 0, b: 1, exp: 0},
   742  				{a: 0, b: 2, exp: 0},
   743  				{a: 0, b: 3, exp: 0},
   744  				{a: 0, b: 4, exp: 0},
   745  				{a: 1, b: 1, exp: 1},
   746  				{a: 1, b: 2, exp: 1},
   747  				{a: 1, b: 3, exp: 1},
   748  				{a: 1, b: 4, exp: 1},
   749  				{a: 2, b: 2, exp: 2},
   750  				{a: 2, b: 3, exp: 2},
   751  				{a: 2, b: 4, exp: 2},
   752  				{a: 3, b: 3, exp: 3},
   753  				{a: 3, b: 4, exp: 3},
   754  				{a: 4, b: 4, exp: 4},
   755  			},
   756  		},
   757  		{
   758  			name: "binary",
   759  			edges: edgesCase{
   760  				0: {1, 2},
   761  				1: {3, 4},
   762  				2: {5, 6},
   763  				3: {},
   764  				4: {},
   765  				5: {},
   766  				6: {},
   767  			},
   768  			cases: []lowestCommonAncestorCase{
   769  				{a: 3, b: 4, exp: 1},
   770  				{a: 3, b: 5, exp: 0},
   771  				{a: 3, b: 6, exp: 0},
   772  				{a: 4, b: 5, exp: 0},
   773  				{a: 4, b: 6, exp: 0},
   774  				{a: 5, b: 6, exp: 2},
   775  				{a: 3, b: 1, exp: 1},
   776  				{a: 6, b: 2, exp: 2},
   777  			},
   778  		},
   779  		{
   780  			name: "complex tree",
   781  			edges: edgesCase{
   782  				0:  {1, 2},
   783  				1:  {3, 4, 5},
   784  				2:  {6, 7},
   785  				3:  {8, 9},
   786  				4:  {10},
   787  				6:  {11, 12, 13},
   788  				7:  {14},
   789  				12: {15},
   790  			},
   791  			cases: []lowestCommonAncestorCase{
   792  				{a: 8, b: 9, exp: 3},
   793  				{a: 10, b: 5, exp: 1},
   794  				{a: 11, b: 14, exp: 2},
   795  				{a: 15, b: 13, exp: 6},
   796  				{a: 8, b: 10, exp: 1},
   797  				{a: 9, b: 4, exp: 1},
   798  				{a: 15, b: 7, exp: 2},
   799  			},
   800  		},
   801  		{
   802  			name: "complex tree with single and multiple branching",
   803  			edges: edgesCase{
   804  				0:  {1, 2},
   805  				1:  {3},
   806  				2:  {4, 5},
   807  				3:  {6, 7, 8},
   808  				4:  {9, 10},
   809  				5:  {11},
   810  				8:  {12, 13},
   811  				9:  {14, 15},
   812  				10: {16},
   813  				13: {17, 18},
   814  			},
   815  			cases: []lowestCommonAncestorCase{
   816  				{a: 6, b: 7, exp: 3},
   817  				{a: 14, b: 16, exp: 4},
   818  				{a: 17, b: 18, exp: 13},
   819  				{a: 12, b: 18, exp: 8},
   820  				{a: 6, b: 12, exp: 3},
   821  				{a: 11, b: 9, exp: 2},
   822  				{a: 15, b: 11, exp: 2},
   823  				{a: 3, b: 10, exp: 0},
   824  				{a: 7, b: 13, exp: 3},
   825  				{a: 15, b: 16, exp: 4},
   826  				{a: 0, b: 17, exp: 0},
   827  				{a: 1, b: 18, exp: 1},
   828  			},
   829  		},
   830  	} {
   831  		tc := tc
   832  		t.Run(tc.name, func(t *testing.T) {
   833  			b := constructGraphFromEdges(tc.edges)
   834  			// buildDominatorTree requires passCalculateImmediateDominators to be done.
   835  			passCalculateImmediateDominators(b)
   836  			buildDominatorTree(b)
   837  
   838  			for _, c := range tc.cases {
   839  				exp := b.sparseTree.findLCA(c.a, c.b)
   840  				require.Equal(t, exp.id, c.exp, "LCA(%d, %d)", c.a, c.b)
   841  			}
   842  		})
   843  	}
   844  }