blob: 219fd70fc0fa736f0d66200ba56783bbe718c49e [file] [log] [blame]
// Copyright 2021 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
package vta
import (
"go/token"
"go/types"
"reflect"
"sort"
"strings"
"testing"
"unsafe"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/types/typeutil"
)
// val is a test data structure for creating ssa.Value
// outside of the ssa package. Needed for manual creation
// of vta graph nodes in testing.
type val struct {
name string
typ types.Type
}
func (v val) String() string {
return v.name
}
func (v val) Name() string {
return v.name
}
func (v val) Type() types.Type {
return v.typ
}
func (v val) Parent() *ssa.Function {
return nil
}
func (v val) Referrers() *[]ssa.Instruction {
return nil
}
func (v val) Pos() token.Pos {
return token.NoPos
}
// newLocal creates a new local node with ssa.Value
// named `name` and type `t`.
func newLocal(name string, t types.Type) local {
return local{val: val{name: name, typ: t}}
}
// newNamedType creates a bogus type named `name`.
func newNamedType(name string) *types.Named {
return types.NewNamed(types.NewTypeName(token.NoPos, nil, name, nil), nil, nil)
}
// sccString is a utility for stringifying `nodeToScc`. Every
// scc is represented as a string where string representation
// of scc nodes are sorted and concatenated using `;`.
func sccString(nodeToScc map[node]int) []string {
sccs := make(map[int][]node)
for n, id := range nodeToScc {
sccs[id] = append(sccs[id], n)
}
var sccsStr []string
for _, scc := range sccs {
var nodesStr []string
for _, node := range scc {
nodesStr = append(nodesStr, node.String())
}
sort.Strings(nodesStr)
sccsStr = append(sccsStr, strings.Join(nodesStr, ";"))
}
return sccsStr
}
// nodeToTypeString is testing utility for stringifying results
// of type propagation: propTypeMap `pMap` is converted to a map
// from node strings to a string consisting of type stringifications
// concatenated with `;`. We stringify reachable type information
// that also has an accompanying function by the function name.
func nodeToTypeString(pMap propTypeMap) map[string]string {
// Convert propType to a string. If propType has
// an attached function, return the function name.
// Otherwise, return the type name.
propTypeString := func(p propType) string {
if p.f != nil {
return p.f.Name()
}
return p.typ.String()
}
nodeToTypeStr := make(map[string]string)
for node := range pMap.nodeToScc {
var propStrings []string
for prop := range pMap.propTypes(node) {
propStrings = append(propStrings, propTypeString(prop))
}
sort.Strings(propStrings)
nodeToTypeStr[node.String()] = strings.Join(propStrings, ";")
}
return nodeToTypeStr
}
// sccEqual compares two sets of SCC stringifications.
func sccEqual(sccs1 []string, sccs2 []string) bool {
if len(sccs1) != len(sccs2) {
return false
}
sort.Strings(sccs1)
sort.Strings(sccs2)
return reflect.DeepEqual(sccs1, sccs2)
}
// isRevTopSorted checks if sccs of `g` are sorted in reverse
// topological order:
// for every edge x -> y in g, nodeToScc[x] > nodeToScc[y]
func isRevTopSorted(g vtaGraph, nodeToScc map[node]int) bool {
for n, succs := range g {
for s := range succs {
if nodeToScc[n] < nodeToScc[s] {
return false
}
}
}
return true
}
// setName sets name of the function `f` to `name`
// using reflection since setting the name otherwise
// is only possible within the ssa package.
func setName(f *ssa.Function, name string) {
fi := reflect.ValueOf(f).Elem().FieldByName("name")
fi = reflect.NewAt(fi.Type(), unsafe.Pointer(fi.UnsafeAddr())).Elem()
fi.SetString(name)
}
// testSuite produces a named set of graphs as follows, where
// parentheses contain node types and F nodes stand for function
// nodes whose content is function named F:
//
// no-cycles:
// t0 (A) -> t1 (B) -> t2 (C)
//
// trivial-cycle:
// <-------- <--------
// | | | |
// t0 (A) -> t1 (B) ->
//
// circle-cycle:
// t0 (A) -> t1 (A) -> t2 (B)
// | |
// <--------------------
//
// fully-connected:
// t0 (A) <-> t1 (B)
// \ /
// t2(C)
//
// subsumed-scc:
// t0 (A) -> t1 (B) -> t2(B) -> t3 (A)
// | | | |
// | <--------- |
// <-----------------------------
//
// more-realistic:
// <--------
// | |
// t0 (A) -->
// ---------->
// | |
// t1 (A) -> t2 (B) -> F1 -> F2 -> F3 -> F4
// | | | |
// <------- <------------
func testSuite() map[string]vtaGraph {
a := newNamedType("A")
b := newNamedType("B")
c := newNamedType("C")
sig := types.NewSignature(nil, types.NewTuple(), types.NewTuple(), false)
f1 := &ssa.Function{Signature: sig}
setName(f1, "F1")
f2 := &ssa.Function{Signature: sig}
setName(f2, "F2")
f3 := &ssa.Function{Signature: sig}
setName(f3, "F3")
f4 := &ssa.Function{Signature: sig}
setName(f4, "F4")
graphs := make(map[string]vtaGraph)
graphs["no-cycles"] = map[node]map[node]bool{
newLocal("t0", a): {newLocal("t1", b): true},
newLocal("t1", b): {newLocal("t2", c): true},
}
graphs["trivial-cycle"] = map[node]map[node]bool{
newLocal("t0", a): {newLocal("t0", a): true},
newLocal("t1", b): {newLocal("t1", b): true},
}
graphs["circle-cycle"] = map[node]map[node]bool{
newLocal("t0", a): {newLocal("t1", a): true},
newLocal("t1", a): {newLocal("t2", b): true},
newLocal("t2", b): {newLocal("t0", a): true},
}
graphs["fully-connected"] = map[node]map[node]bool{
newLocal("t0", a): {newLocal("t1", b): true, newLocal("t2", c): true},
newLocal("t1", b): {newLocal("t0", a): true, newLocal("t2", c): true},
newLocal("t2", c): {newLocal("t0", a): true, newLocal("t1", b): true},
}
graphs["subsumed-scc"] = map[node]map[node]bool{
newLocal("t0", a): {newLocal("t1", b): true},
newLocal("t1", b): {newLocal("t2", b): true},
newLocal("t2", b): {newLocal("t1", b): true, newLocal("t3", a): true},
newLocal("t3", a): {newLocal("t0", a): true},
}
graphs["more-realistic"] = map[node]map[node]bool{
newLocal("t0", a): {newLocal("t0", a): true},
newLocal("t1", a): {newLocal("t2", b): true},
newLocal("t2", b): {newLocal("t1", a): true, function{f1}: true},
function{f1}: {function{f2}: true, function{f3}: true},
function{f2}: {function{f3}: true},
function{f3}: {function{f1}: true, function{f4}: true},
}
return graphs
}
func TestSCC(t *testing.T) {
suite := testSuite()
for _, test := range []struct {
name string
graph vtaGraph
want []string
}{
// No cycles results in three separate SCCs: {t0} {t1} {t2}
{name: "no-cycles", graph: suite["no-cycles"], want: []string{"Local(t0)", "Local(t1)", "Local(t2)"}},
// The two trivial self-loop cycles results in: {t0} {t1}
{name: "trivial-cycle", graph: suite["trivial-cycle"], want: []string{"Local(t0)", "Local(t1)"}},
// The circle cycle produce a single SCC: {t0, t1, t2}
{name: "circle-cycle", graph: suite["circle-cycle"], want: []string{"Local(t0);Local(t1);Local(t2)"}},
// Similar holds for fully connected SCC: {t0, t1, t2}
{name: "fully-connected", graph: suite["fully-connected"], want: []string{"Local(t0);Local(t1);Local(t2)"}},
// Subsumed SCC also has a single SCC: {t0, t1, t2, t3}
{name: "subsumed-scc", graph: suite["subsumed-scc"], want: []string{"Local(t0);Local(t1);Local(t2);Local(t3)"}},
// The more realistic example has the following SCCs: {t0} {t1, t2} {F1, F2, F3} {F4}
{name: "more-realistic", graph: suite["more-realistic"], want: []string{"Local(t0)", "Local(t1);Local(t2)", "Function(F1);Function(F2);Function(F3)", "Function(F4)"}},
} {
sccs, _ := scc(test.graph)
if got := sccString(sccs); !sccEqual(test.want, got) {
t.Errorf("want %v for graph %v; got %v", test.want, test.name, got)
}
if !isRevTopSorted(test.graph, sccs) {
t.Errorf("%v not topologically sorted", test.name)
}
}
}
func TestPropagation(t *testing.T) {
suite := testSuite()
var canon typeutil.Map
for _, test := range []struct {
name string
graph vtaGraph
want map[string]string
}{
// No cycles graph pushes type information forward.
{name: "no-cycles", graph: suite["no-cycles"],
want: map[string]string{
"Local(t0)": "A",
"Local(t1)": "A;B",
"Local(t2)": "A;B;C",
},
},
// No interesting type flow in trivial cycle graph.
{name: "trivial-cycle", graph: suite["trivial-cycle"],
want: map[string]string{
"Local(t0)": "A",
"Local(t1)": "B",
},
},
// Circle cycle makes type A and B get propagated everywhere.
{name: "circle-cycle", graph: suite["circle-cycle"],
want: map[string]string{
"Local(t0)": "A;B",
"Local(t1)": "A;B",
"Local(t2)": "A;B",
},
},
// Similarly for fully connected graph.
{name: "fully-connected", graph: suite["fully-connected"],
want: map[string]string{
"Local(t0)": "A;B;C",
"Local(t1)": "A;B;C",
"Local(t2)": "A;B;C",
},
},
// The outer loop of subsumed-scc pushes A an B through the graph.
{name: "subsumed-scc", graph: suite["subsumed-scc"],
want: map[string]string{
"Local(t0)": "A;B",
"Local(t1)": "A;B",
"Local(t2)": "A;B",
"Local(t3)": "A;B",
},
},
// More realistic graph has a more fine grained flow.
{name: "more-realistic", graph: suite["more-realistic"],
want: map[string]string{
"Local(t0)": "A",
"Local(t1)": "A;B",
"Local(t2)": "A;B",
"Function(F1)": "A;B;F1;F2;F3",
"Function(F2)": "A;B;F1;F2;F3",
"Function(F3)": "A;B;F1;F2;F3",
"Function(F4)": "A;B;F1;F2;F3;F4",
},
},
} {
if got := nodeToTypeString(propagate(test.graph, &canon)); !reflect.DeepEqual(got, test.want) {
t.Errorf("want %v for graph %v; got %v", test.want, test.name, got)
}
}
}