github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/go/callgraph/vta/propagation_test.go (about)

     1  // Copyright 2021 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  package vta
     6  
     7  import (
     8  	"go/token"
     9  	"go/types"
    10  	"reflect"
    11  	"sort"
    12  	"strings"
    13  	"testing"
    14  	"unsafe"
    15  
    16  	"golang.org/x/tools/go/ssa"
    17  
    18  	"golang.org/x/tools/go/types/typeutil"
    19  )
    20  
    21  // val is a test data structure for creating ssa.Value
    22  // outside of the ssa package. Needed for manual creation
    23  // of vta graph nodes in testing.
    24  type val struct {
    25  	name string
    26  	typ  types.Type
    27  }
    28  
    29  func (v val) String() string {
    30  	return v.name
    31  }
    32  
    33  func (v val) Name() string {
    34  	return v.name
    35  }
    36  
    37  func (v val) Type() types.Type {
    38  	return v.typ
    39  }
    40  
    41  func (v val) Parent() *ssa.Function {
    42  	return nil
    43  }
    44  
    45  func (v val) Referrers() *[]ssa.Instruction {
    46  	return nil
    47  }
    48  
    49  func (v val) Pos() token.Pos {
    50  	return token.NoPos
    51  }
    52  
    53  // newLocal creates a new local node with ssa.Value
    54  // named `name` and type `t`.
    55  func newLocal(name string, t types.Type) local {
    56  	return local{val: val{name: name, typ: t}}
    57  }
    58  
    59  // newNamedType creates a bogus type named `name`.
    60  func newNamedType(name string) *types.Named {
    61  	return types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), types.Universe.Lookup("int").Type(), nil)
    62  }
    63  
    64  // sccString is a utility for stringifying `nodeToScc`. Every
    65  // scc is represented as a string where string representation
    66  // of scc nodes are sorted and concatenated using `;`.
    67  func sccString(nodeToScc map[node]int) []string {
    68  	sccs := make(map[int][]node)
    69  	for n, id := range nodeToScc {
    70  		sccs[id] = append(sccs[id], n)
    71  	}
    72  
    73  	var sccsStr []string
    74  	for _, scc := range sccs {
    75  		var nodesStr []string
    76  		for _, node := range scc {
    77  			nodesStr = append(nodesStr, node.String())
    78  		}
    79  		sort.Strings(nodesStr)
    80  		sccsStr = append(sccsStr, strings.Join(nodesStr, ";"))
    81  	}
    82  	return sccsStr
    83  }
    84  
    85  // nodeToTypeString is testing utility for stringifying results
    86  // of type propagation: propTypeMap `pMap` is converted to a map
    87  // from node strings to a string consisting of type stringifications
    88  // concatenated with `;`. We stringify reachable type information
    89  // that also has an accompanying function by the function name.
    90  func nodeToTypeString(pMap propTypeMap) map[string]string {
    91  	// Convert propType to a string. If propType has
    92  	// an attached function, return the function name.
    93  	// Otherwise, return the type name.
    94  	propTypeString := func(p propType) string {
    95  		if p.f != nil {
    96  			return p.f.Name()
    97  		}
    98  		return p.typ.String()
    99  	}
   100  
   101  	nodeToTypeStr := make(map[string]string)
   102  	for node := range pMap.nodeToScc {
   103  		var propStrings []string
   104  		for _, prop := range pMap.propTypes(node) {
   105  			propStrings = append(propStrings, propTypeString(prop))
   106  		}
   107  		sort.Strings(propStrings)
   108  		nodeToTypeStr[node.String()] = strings.Join(propStrings, ";")
   109  	}
   110  
   111  	return nodeToTypeStr
   112  }
   113  
   114  // sccEqual compares two sets of SCC stringifications.
   115  func sccEqual(sccs1 []string, sccs2 []string) bool {
   116  	if len(sccs1) != len(sccs2) {
   117  		return false
   118  	}
   119  	sort.Strings(sccs1)
   120  	sort.Strings(sccs2)
   121  	return reflect.DeepEqual(sccs1, sccs2)
   122  }
   123  
   124  // isRevTopSorted checks if sccs of `g` are sorted in reverse
   125  // topological order:
   126  //
   127  //	for every edge x -> y in g, nodeToScc[x] > nodeToScc[y]
   128  func isRevTopSorted(g vtaGraph, nodeToScc map[node]int) bool {
   129  	for n, succs := range g {
   130  		for s := range succs {
   131  			if nodeToScc[n] < nodeToScc[s] {
   132  				return false
   133  			}
   134  		}
   135  	}
   136  	return true
   137  }
   138  
   139  // setName sets name of the function `f` to `name`
   140  // using reflection since setting the name otherwise
   141  // is only possible within the ssa package.
   142  func setName(f *ssa.Function, name string) {
   143  	fi := reflect.ValueOf(f).Elem().FieldByName("name")
   144  	fi = reflect.NewAt(fi.Type(), unsafe.Pointer(fi.UnsafeAddr())).Elem()
   145  	fi.SetString(name)
   146  }
   147  
   148  // testSuite produces a named set of graphs as follows, where
   149  // parentheses contain node types and F nodes stand for function
   150  // nodes whose content is function named F:
   151  //
   152  //	 no-cycles:
   153  //		t0 (A) -> t1 (B) -> t2 (C)
   154  //
   155  //	 trivial-cycle:
   156  //	     <--------    <--------
   157  //	     |       |    |       |
   158  //	     t0 (A) ->    t1 (B) ->
   159  //
   160  //	 circle-cycle:
   161  //		t0 (A) -> t1 (A) -> t2 (B)
   162  //	     |                   |
   163  //	     <--------------------
   164  //
   165  //	 fully-connected:
   166  //		t0 (A) <-> t1 (B)
   167  //	          \    /
   168  //	           t2(C)
   169  //
   170  //	 subsumed-scc:
   171  //		t0 (A) -> t1 (B) -> t2(B) -> t3 (A)
   172  //	     |          |         |        |
   173  //	     |          <---------         |
   174  //	     <-----------------------------
   175  //
   176  //	 more-realistic:
   177  //	     <--------
   178  //	     |        |
   179  //	     t0 (A) -->
   180  //	                           ---------->
   181  //	                          |           |
   182  //	     t1 (A) -> t2 (B) -> F1 -> F2 -> F3 -> F4
   183  //	      |        |          |           |
   184  //	       <-------           <------------
   185  func testSuite() map[string]vtaGraph {
   186  	a := newNamedType("A")
   187  	b := newNamedType("B")
   188  	c := newNamedType("C")
   189  	sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false)
   190  
   191  	f1 := &ssa.Function{Signature: sig}
   192  	setName(f1, "F1")
   193  	f2 := &ssa.Function{Signature: sig}
   194  	setName(f2, "F2")
   195  	f3 := &ssa.Function{Signature: sig}
   196  	setName(f3, "F3")
   197  	f4 := &ssa.Function{Signature: sig}
   198  	setName(f4, "F4")
   199  
   200  	graphs := make(map[string]vtaGraph)
   201  	graphs["no-cycles"] = map[node]map[node]bool{
   202  		newLocal("t0", a): {newLocal("t1", b): true},
   203  		newLocal("t1", b): {newLocal("t2", c): true},
   204  	}
   205  
   206  	graphs["trivial-cycle"] = map[node]map[node]bool{
   207  		newLocal("t0", a): {newLocal("t0", a): true},
   208  		newLocal("t1", b): {newLocal("t1", b): true},
   209  	}
   210  
   211  	graphs["circle-cycle"] = map[node]map[node]bool{
   212  		newLocal("t0", a): {newLocal("t1", a): true},
   213  		newLocal("t1", a): {newLocal("t2", b): true},
   214  		newLocal("t2", b): {newLocal("t0", a): true},
   215  	}
   216  
   217  	graphs["fully-connected"] = map[node]map[node]bool{
   218  		newLocal("t0", a): {newLocal("t1", b): true, newLocal("t2", c): true},
   219  		newLocal("t1", b): {newLocal("t0", a): true, newLocal("t2", c): true},
   220  		newLocal("t2", c): {newLocal("t0", a): true, newLocal("t1", b): true},
   221  	}
   222  
   223  	graphs["subsumed-scc"] = map[node]map[node]bool{
   224  		newLocal("t0", a): {newLocal("t1", b): true},
   225  		newLocal("t1", b): {newLocal("t2", b): true},
   226  		newLocal("t2", b): {newLocal("t1", b): true, newLocal("t3", a): true},
   227  		newLocal("t3", a): {newLocal("t0", a): true},
   228  	}
   229  
   230  	graphs["more-realistic"] = map[node]map[node]bool{
   231  		newLocal("t0", a): {newLocal("t0", a): true},
   232  		newLocal("t1", a): {newLocal("t2", b): true},
   233  		newLocal("t2", b): {newLocal("t1", a): true, function{f1}: true},
   234  		function{f1}:      {function{f2}: true, function{f3}: true},
   235  		function{f2}:      {function{f3}: true},
   236  		function{f3}:      {function{f1}: true, function{f4}: true},
   237  	}
   238  
   239  	return graphs
   240  }
   241  
   242  func TestSCC(t *testing.T) {
   243  	suite := testSuite()
   244  	for _, test := range []struct {
   245  		name  string
   246  		graph vtaGraph
   247  		want  []string
   248  	}{
   249  		// No cycles results in three separate SCCs: {t0}	{t1}	{t2}
   250  		{name: "no-cycles", graph: suite["no-cycles"], want: []string{"Local(t0)", "Local(t1)", "Local(t2)"}},
   251  		// The two trivial self-loop cycles results in: {t0}	{t1}
   252  		{name: "trivial-cycle", graph: suite["trivial-cycle"], want: []string{"Local(t0)", "Local(t1)"}},
   253  		// The circle cycle produce a single SCC: {t0, t1, t2}
   254  		{name: "circle-cycle", graph: suite["circle-cycle"], want: []string{"Local(t0);Local(t1);Local(t2)"}},
   255  		// Similar holds for fully connected SCC: {t0, t1, t2}
   256  		{name: "fully-connected", graph: suite["fully-connected"], want: []string{"Local(t0);Local(t1);Local(t2)"}},
   257  		// Subsumed SCC also has a single SCC: {t0, t1, t2, t3}
   258  		{name: "subsumed-scc", graph: suite["subsumed-scc"], want: []string{"Local(t0);Local(t1);Local(t2);Local(t3)"}},
   259  		// The more realistic example has the following SCCs: {t0}	{t1, t2}	{F1, F2, F3}	{F4}
   260  		{name: "more-realistic", graph: suite["more-realistic"], want: []string{"Local(t0)", "Local(t1);Local(t2)", "Function(F1);Function(F2);Function(F3)", "Function(F4)"}},
   261  	} {
   262  		sccs, _ := scc(test.graph)
   263  		if got := sccString(sccs); !sccEqual(test.want, got) {
   264  			t.Errorf("want %v for graph %v; got %v", test.want, test.name, got)
   265  		}
   266  		if !isRevTopSorted(test.graph, sccs) {
   267  			t.Errorf("%v not topologically sorted", test.name)
   268  		}
   269  	}
   270  }
   271  
   272  func TestPropagation(t *testing.T) {
   273  	suite := testSuite()
   274  	var canon typeutil.Map
   275  	for _, test := range []struct {
   276  		name  string
   277  		graph vtaGraph
   278  		want  map[string]string
   279  	}{
   280  		// No cycles graph pushes type information forward.
   281  		{name: "no-cycles", graph: suite["no-cycles"],
   282  			want: map[string]string{
   283  				"Local(t0)": "A",
   284  				"Local(t1)": "A;B",
   285  				"Local(t2)": "A;B;C",
   286  			},
   287  		},
   288  		// No interesting type flow in trivial cycle graph.
   289  		{name: "trivial-cycle", graph: suite["trivial-cycle"],
   290  			want: map[string]string{
   291  				"Local(t0)": "A",
   292  				"Local(t1)": "B",
   293  			},
   294  		},
   295  		// Circle cycle makes type A and B get propagated everywhere.
   296  		{name: "circle-cycle", graph: suite["circle-cycle"],
   297  			want: map[string]string{
   298  				"Local(t0)": "A;B",
   299  				"Local(t1)": "A;B",
   300  				"Local(t2)": "A;B",
   301  			},
   302  		},
   303  		// Similarly for fully connected graph.
   304  		{name: "fully-connected", graph: suite["fully-connected"],
   305  			want: map[string]string{
   306  				"Local(t0)": "A;B;C",
   307  				"Local(t1)": "A;B;C",
   308  				"Local(t2)": "A;B;C",
   309  			},
   310  		},
   311  		// The outer loop of subsumed-scc pushes A an B through the graph.
   312  		{name: "subsumed-scc", graph: suite["subsumed-scc"],
   313  			want: map[string]string{
   314  				"Local(t0)": "A;B",
   315  				"Local(t1)": "A;B",
   316  				"Local(t2)": "A;B",
   317  				"Local(t3)": "A;B",
   318  			},
   319  		},
   320  		// More realistic graph has a more fine grained flow.
   321  		{name: "more-realistic", graph: suite["more-realistic"],
   322  			want: map[string]string{
   323  				"Local(t0)":    "A",
   324  				"Local(t1)":    "A;B",
   325  				"Local(t2)":    "A;B",
   326  				"Function(F1)": "A;B;F1;F2;F3",
   327  				"Function(F2)": "A;B;F1;F2;F3",
   328  				"Function(F3)": "A;B;F1;F2;F3",
   329  				"Function(F4)": "A;B;F1;F2;F3;F4",
   330  			},
   331  		},
   332  	} {
   333  		if got := nodeToTypeString(propagate(test.graph, &canon)); !reflect.DeepEqual(got, test.want) {
   334  			t.Errorf("want %v for graph %v; got %v", test.want, test.name, got)
   335  		}
   336  	}
   337  }