github.com/jhump/golang-x-tools@v0.0.0-20220218190644-4958d6d39439/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  	"github.com/jhump/golang-x-tools/go/ssa"
    17  
    18  	"github.com/jhump/golang-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), nil, 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  //  for every edge x -> y in g, nodeToScc[x] > nodeToScc[y]
   127  func isRevTopSorted(g vtaGraph, nodeToScc map[node]int) bool {
   128  	for n, succs := range g {
   129  		for s := range succs {
   130  			if nodeToScc[n] < nodeToScc[s] {
   131  				return false
   132  			}
   133  		}
   134  	}
   135  	return true
   136  }
   137  
   138  // setName sets name of the function `f` to `name`
   139  // using reflection since setting the name otherwise
   140  // is only possible within the ssa package.
   141  func setName(f *ssa.Function, name string) {
   142  	fi := reflect.ValueOf(f).Elem().FieldByName("name")
   143  	fi = reflect.NewAt(fi.Type(), unsafe.Pointer(fi.UnsafeAddr())).Elem()
   144  	fi.SetString(name)
   145  }
   146  
   147  // testSuite produces a named set of graphs as follows, where
   148  // parentheses contain node types and F nodes stand for function
   149  // nodes whose content is function named F:
   150  //
   151  //  no-cycles:
   152  //	t0 (A) -> t1 (B) -> t2 (C)
   153  //
   154  //  trivial-cycle:
   155  //      <--------    <--------
   156  //      |       |    |       |
   157  //      t0 (A) ->    t1 (B) ->
   158  //
   159  //  circle-cycle:
   160  //	t0 (A) -> t1 (A) -> t2 (B)
   161  //      |                   |
   162  //      <--------------------
   163  //
   164  //  fully-connected:
   165  //	t0 (A) <-> t1 (B)
   166  //           \    /
   167  //            t2(C)
   168  //
   169  //  subsumed-scc:
   170  //	t0 (A) -> t1 (B) -> t2(B) -> t3 (A)
   171  //      |          |         |        |
   172  //      |          <---------         |
   173  //      <-----------------------------
   174  //
   175  //  more-realistic:
   176  //      <--------
   177  //      |        |
   178  //      t0 (A) -->
   179  //                            ---------->
   180  //                           |           |
   181  //      t1 (A) -> t2 (B) -> F1 -> F2 -> F3 -> F4
   182  //       |        |          |           |
   183  //        <-------           <------------
   184  func testSuite() map[string]vtaGraph {
   185  	a := newNamedType("A")
   186  	b := newNamedType("B")
   187  	c := newNamedType("C")
   188  	sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false)
   189  
   190  	f1 := &ssa.Function{Signature: sig}
   191  	setName(f1, "F1")
   192  	f2 := &ssa.Function{Signature: sig}
   193  	setName(f2, "F2")
   194  	f3 := &ssa.Function{Signature: sig}
   195  	setName(f3, "F3")
   196  	f4 := &ssa.Function{Signature: sig}
   197  	setName(f4, "F4")
   198  
   199  	graphs := make(map[string]vtaGraph)
   200  	graphs["no-cycles"] = map[node]map[node]bool{
   201  		newLocal("t0", a): {newLocal("t1", b): true},
   202  		newLocal("t1", b): {newLocal("t2", c): true},
   203  	}
   204  
   205  	graphs["trivial-cycle"] = map[node]map[node]bool{
   206  		newLocal("t0", a): {newLocal("t0", a): true},
   207  		newLocal("t1", b): {newLocal("t1", b): true},
   208  	}
   209  
   210  	graphs["circle-cycle"] = map[node]map[node]bool{
   211  		newLocal("t0", a): {newLocal("t1", a): true},
   212  		newLocal("t1", a): {newLocal("t2", b): true},
   213  		newLocal("t2", b): {newLocal("t0", a): true},
   214  	}
   215  
   216  	graphs["fully-connected"] = map[node]map[node]bool{
   217  		newLocal("t0", a): {newLocal("t1", b): true, newLocal("t2", c): true},
   218  		newLocal("t1", b): {newLocal("t0", a): true, newLocal("t2", c): true},
   219  		newLocal("t2", c): {newLocal("t0", a): true, newLocal("t1", b): true},
   220  	}
   221  
   222  	graphs["subsumed-scc"] = map[node]map[node]bool{
   223  		newLocal("t0", a): {newLocal("t1", b): true},
   224  		newLocal("t1", b): {newLocal("t2", b): true},
   225  		newLocal("t2", b): {newLocal("t1", b): true, newLocal("t3", a): true},
   226  		newLocal("t3", a): {newLocal("t0", a): true},
   227  	}
   228  
   229  	graphs["more-realistic"] = map[node]map[node]bool{
   230  		newLocal("t0", a): {newLocal("t0", a): true},
   231  		newLocal("t1", a): {newLocal("t2", b): true},
   232  		newLocal("t2", b): {newLocal("t1", a): true, function{f1}: true},
   233  		function{f1}:      {function{f2}: true, function{f3}: true},
   234  		function{f2}:      {function{f3}: true},
   235  		function{f3}:      {function{f1}: true, function{f4}: true},
   236  	}
   237  
   238  	return graphs
   239  }
   240  
   241  func TestSCC(t *testing.T) {
   242  	suite := testSuite()
   243  	for _, test := range []struct {
   244  		name  string
   245  		graph vtaGraph
   246  		want  []string
   247  	}{
   248  		// No cycles results in three separate SCCs: {t0}	{t1}	{t2}
   249  		{name: "no-cycles", graph: suite["no-cycles"], want: []string{"Local(t0)", "Local(t1)", "Local(t2)"}},
   250  		// The two trivial self-loop cycles results in: {t0}	{t1}
   251  		{name: "trivial-cycle", graph: suite["trivial-cycle"], want: []string{"Local(t0)", "Local(t1)"}},
   252  		// The circle cycle produce a single SCC: {t0, t1, t2}
   253  		{name: "circle-cycle", graph: suite["circle-cycle"], want: []string{"Local(t0);Local(t1);Local(t2)"}},
   254  		// Similar holds for fully connected SCC: {t0, t1, t2}
   255  		{name: "fully-connected", graph: suite["fully-connected"], want: []string{"Local(t0);Local(t1);Local(t2)"}},
   256  		// Subsumed SCC also has a single SCC: {t0, t1, t2, t3}
   257  		{name: "subsumed-scc", graph: suite["subsumed-scc"], want: []string{"Local(t0);Local(t1);Local(t2);Local(t3)"}},
   258  		// The more realistic example has the following SCCs: {t0}	{t1, t2}	{F1, F2, F3}	{F4}
   259  		{name: "more-realistic", graph: suite["more-realistic"], want: []string{"Local(t0)", "Local(t1);Local(t2)", "Function(F1);Function(F2);Function(F3)", "Function(F4)"}},
   260  	} {
   261  		sccs, _ := scc(test.graph)
   262  		if got := sccString(sccs); !sccEqual(test.want, got) {
   263  			t.Errorf("want %v for graph %v; got %v", test.want, test.name, got)
   264  		}
   265  		if !isRevTopSorted(test.graph, sccs) {
   266  			t.Errorf("%v not topologically sorted", test.name)
   267  		}
   268  	}
   269  }
   270  
   271  func TestPropagation(t *testing.T) {
   272  	suite := testSuite()
   273  	var canon typeutil.Map
   274  	for _, test := range []struct {
   275  		name  string
   276  		graph vtaGraph
   277  		want  map[string]string
   278  	}{
   279  		// No cycles graph pushes type information forward.
   280  		{name: "no-cycles", graph: suite["no-cycles"],
   281  			want: map[string]string{
   282  				"Local(t0)": "A",
   283  				"Local(t1)": "A;B",
   284  				"Local(t2)": "A;B;C",
   285  			},
   286  		},
   287  		// No interesting type flow in trivial cycle graph.
   288  		{name: "trivial-cycle", graph: suite["trivial-cycle"],
   289  			want: map[string]string{
   290  				"Local(t0)": "A",
   291  				"Local(t1)": "B",
   292  			},
   293  		},
   294  		// Circle cycle makes type A and B get propagated everywhere.
   295  		{name: "circle-cycle", graph: suite["circle-cycle"],
   296  			want: map[string]string{
   297  				"Local(t0)": "A;B",
   298  				"Local(t1)": "A;B",
   299  				"Local(t2)": "A;B",
   300  			},
   301  		},
   302  		// Similarly for fully connected graph.
   303  		{name: "fully-connected", graph: suite["fully-connected"],
   304  			want: map[string]string{
   305  				"Local(t0)": "A;B;C",
   306  				"Local(t1)": "A;B;C",
   307  				"Local(t2)": "A;B;C",
   308  			},
   309  		},
   310  		// The outer loop of subsumed-scc pushes A an B through the graph.
   311  		{name: "subsumed-scc", graph: suite["subsumed-scc"],
   312  			want: map[string]string{
   313  				"Local(t0)": "A;B",
   314  				"Local(t1)": "A;B",
   315  				"Local(t2)": "A;B",
   316  				"Local(t3)": "A;B",
   317  			},
   318  		},
   319  		// More realistic graph has a more fine grained flow.
   320  		{name: "more-realistic", graph: suite["more-realistic"],
   321  			want: map[string]string{
   322  				"Local(t0)":    "A",
   323  				"Local(t1)":    "A;B",
   324  				"Local(t2)":    "A;B",
   325  				"Function(F1)": "A;B;F1;F2;F3",
   326  				"Function(F2)": "A;B;F1;F2;F3",
   327  				"Function(F3)": "A;B;F1;F2;F3",
   328  				"Function(F4)": "A;B;F1;F2;F3;F4",
   329  			},
   330  		},
   331  	} {
   332  		if got := nodeToTypeString(propagate(test.graph, &canon)); !reflect.DeepEqual(got, test.want) {
   333  			t.Errorf("want %v for graph %v; got %v", test.want, test.name, got)
   334  		}
   335  	}
   336  }