cmd/callgraph: add -algo=static and -algo=cha options.

"static" ignores dynamic calls altogether.

"cha" uses Class Hierarchy Analysis, which assumes that a
dynamic call may dispatch to any func or method that satisfies
the type.

Both these algorithms can work on partial programs,
e.g. libraries without a main function or tests.

(This feature was requested after my talk last night.)

+ Tests.

LGTM=sameer
R=sameer, minux
CC=golang-codereviews, gri
https://golang.org/cl/176780043
This commit is contained in:
Alan Donovan 2014-11-21 11:07:56 -05:00
parent ce7df396da
commit a9651d6ad7
8 changed files with 535 additions and 38 deletions

View File

@ -31,7 +31,9 @@ import (
"text/template" "text/template"
"golang.org/x/tools/go/callgraph" "golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/callgraph/cha"
"golang.org/x/tools/go/callgraph/rta" "golang.org/x/tools/go/callgraph/rta"
"golang.org/x/tools/go/callgraph/static"
"golang.org/x/tools/go/loader" "golang.org/x/tools/go/loader"
"golang.org/x/tools/go/pointer" "golang.org/x/tools/go/pointer"
"golang.org/x/tools/go/ssa" "golang.org/x/tools/go/ssa"
@ -51,21 +53,30 @@ const Usage = `callgraph: display the the call graph of a Go program.
Usage: Usage:
callgraph [-algo=rta|pta] [-test] [-format=...] <args>... callgraph [-algo=static|cha|rta|pta] [-test] [-format=...] <args>...
Flags: Flags:
-algo Specifies the call-graph construction algorithm. One of: -algo Specifies the call-graph construction algorithm, one of:
"rta": Rapid Type Analysis (simple and fast)
"pta": inclusion-based Points-To Analysis (slower but more precise) static static calls only (unsound)
cha Class Hierarchy Analysis
rta Rapid Type Analysis
pta inclusion-based Points-To Analysis
The algorithms are ordered by increasing precision in their
treatment of dynamic calls (and thus also computational cost).
RTA and PTA require a whole program (main or test), and
include only functions reachable from main.
-test Include the package's tests in the analysis. -test Include the package's tests in the analysis.
-format Specifies the format in which each call graph edge is displayed. -format Specifies the format in which each call graph edge is displayed.
One of: One of:
"digraph": output suitable for input to
digraph output suitable for input to
golang.org/x/tools/cmd/digraph. golang.org/x/tools/cmd/digraph.
"graphviz": output in AT&T GraphViz (.dot) format. graphviz output in AT&T GraphViz (.dot) format.
All other values are interpreted using text/template syntax. All other values are interpreted using text/template syntax.
The default value is: The default value is:
@ -168,44 +179,22 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
prog := ssa.Create(iprog, 0) prog := ssa.Create(iprog, 0)
prog.BuildAll() prog.BuildAll()
// Determine the main package.
// TODO(adonovan): allow independent control over tests, mains
// and libraries.
// TODO(adonovan): put this logic in a library; we keep reinventing it.
var main *ssa.Package
pkgs := prog.AllPackages()
if tests {
// If -test, use all packages' tests.
if len(pkgs) > 0 {
main = prog.CreateTestMainPackage(pkgs...)
}
if main == nil {
return fmt.Errorf("no tests")
}
} else {
// Otherwise, use main.main.
for _, pkg := range pkgs {
if pkg.Object.Name() == "main" {
main = pkg
if main.Func("main") == nil {
return fmt.Errorf("no func main() in main package")
}
break
}
}
if main == nil {
return fmt.Errorf("no main package")
}
}
// Invariant: main package has a main() function.
// -- call graph construction ------------------------------------------ // -- call graph construction ------------------------------------------
var cg *callgraph.Graph var cg *callgraph.Graph
switch algo { switch algo {
case "static":
cg = static.CallGraph(prog)
case "cha":
cg = cha.CallGraph(prog)
case "pta": case "pta":
main, err := mainPackage(prog, tests)
if err != nil {
return err
}
config := &pointer.Config{ config := &pointer.Config{
Mains: []*ssa.Package{main}, Mains: []*ssa.Package{main},
BuildCallGraph: true, BuildCallGraph: true,
@ -217,6 +206,10 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
cg = ptares.CallGraph cg = ptares.CallGraph
case "rta": case "rta":
main, err := mainPackage(prog, tests)
if err != nil {
return err
}
roots := []*ssa.Function{ roots := []*ssa.Function{
main.Func("init"), main.Func("init"),
main.Func("main"), main.Func("main"),
@ -279,6 +272,37 @@ func doCallgraph(ctxt *build.Context, algo, format string, tests bool, args []st
return nil return nil
} }
// mainPackage returns the main package to analyze.
// The resulting package has a main() function.
func mainPackage(prog *ssa.Program, tests bool) (*ssa.Package, error) {
pkgs := prog.AllPackages()
// TODO(adonovan): allow independent control over tests, mains and libraries.
// TODO(adonovan): put this logic in a library; we keep reinventing it.
if tests {
// If -test, use all packages' tests.
if len(pkgs) > 0 {
if main := prog.CreateTestMainPackage(pkgs...); main != nil {
return main, nil
}
}
return nil, fmt.Errorf("no tests")
}
// Otherwise, use the first package named main.
for _, pkg := range pkgs {
if pkg.Object.Name() == "main" {
if pkg.Func("main") == nil {
return nil, fmt.Errorf("no func main() in main package")
}
return pkg, nil
}
}
return nil, fmt.Errorf("no main package")
}
type Edge struct { type Edge struct {
Caller *ssa.Function Caller *ssa.Function
Callee *ssa.Function Callee *ssa.Function

120
go/callgraph/cha/cha.go Normal file
View File

@ -0,0 +1,120 @@
// Package cha computes the call graph of a Go program using the Class
// Hierarchy Analysis (CHA) algorithm.
//
// CHA was first described in "Optimization of Object-Oriented Programs
// Using Static Class Hierarchy Analysis", Jeffrey Dean, David Grove,
// and Craig Chambers, ECOOP'95.
//
// CHA is related to RTA (see go/callgraph/rta); the difference is that
// CHA conservatively computes the entire "implements" relation between
// interfaces and concrete types ahead of time, whereas RTA uses dynamic
// programming to construct it on the fly as it encounters new functions
// reachable from main. CHA may thus include spurious call edges for
// types that haven't been instantiated yet, or types that are never
// instantiated.
//
// Since CHA conservatively assumes that all functions are address-taken
// and all concrete types are put into interfaces, it is sound to run on
// partial programs, such as libraries without a main or test function.
//
package cha
import (
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
"golang.org/x/tools/go/types"
"golang.org/x/tools/go/types/typeutil"
)
// CallGraph computes the call graph of the specified program using the
// Class Hierarchy Analysis algorithm.
//
func CallGraph(prog *ssa.Program) *callgraph.Graph {
cg := callgraph.New(nil) // TODO(adonovan) eliminate concept of rooted callgraph
allFuncs := ssautil.AllFunctions(prog)
// funcsBySig contains all functions, keyed by signature. It is
// the effective set of address-taken functions used to resolve
// a dynamic call of a particular signature.
var funcsBySig typeutil.Map // value is []*ssa.Function
// methodsByName contains all methods,
// grouped by name for efficient lookup.
methodsByName := make(map[string][]*ssa.Function)
// methodsMemo records, for every abstract method call call I.f on
// interface type I, the set of concrete methods C.f of all
// types C that satisfy interface I.
methodsMemo := make(map[*types.Func][]*ssa.Function)
lookupMethods := func(m *types.Func) []*ssa.Function {
methods, ok := methodsMemo[m]
if !ok {
I := m.Type().(*types.Signature).Recv().Type().Underlying().(*types.Interface)
for _, f := range methodsByName[m.Name()] {
C := f.Signature.Recv().Type() // named or *named
if types.Implements(C, I) {
methods = append(methods, f)
}
}
methodsMemo[m] = methods
}
return methods
}
for f := range allFuncs {
if f.Signature.Recv() == nil {
// Package initializers can never be address-taken.
if f.Name() == "init" && f.Synthetic == "package initializer" {
continue
}
funcs, _ := funcsBySig.At(f.Signature).([]*ssa.Function)
funcs = append(funcs, f)
funcsBySig.Set(f.Signature, funcs)
} else {
methodsByName[f.Name()] = append(methodsByName[f.Name()], f)
}
}
addEdge := func(fnode *callgraph.Node, site ssa.CallInstruction, g *ssa.Function) {
gnode := cg.CreateNode(g)
callgraph.AddEdge(fnode, site, gnode)
}
addEdges := func(fnode *callgraph.Node, site ssa.CallInstruction, callees []*ssa.Function) {
// Because every call to a highly polymorphic and
// frequently used abstract method such as
// (io.Writer).Write is assumed to call every concrete
// Write method in the program, the call graph can
// contain a lot of duplication.
//
// TODO(adonovan): opt: consider factoring the callgraph
// API so that the Callers component of each edge is a
// slice of nodes, not a singleton.
for _, g := range callees {
addEdge(fnode, site, g)
}
}
for f := range allFuncs {
fnode := cg.CreateNode(f)
for _, b := range f.Blocks {
for _, instr := range b.Instrs {
if site, ok := instr.(ssa.CallInstruction); ok {
call := site.Common()
if call.IsInvoke() {
addEdges(fnode, site, lookupMethods(call.Method))
} else if g := call.StaticCallee(); g != nil {
addEdge(fnode, site, g)
} else if _, ok := call.Value.(*ssa.Builtin); !ok {
callees, _ := funcsBySig.At(call.Signature()).([]*ssa.Function)
addEdges(fnode, site, callees)
}
}
}
}
}
return cg
}

View File

@ -0,0 +1,107 @@
// Copyright 2014 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 cha_test
import (
"bytes"
"fmt"
"go/ast"
"go/parser"
"go/token"
"io/ioutil"
"sort"
"strings"
"testing"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/callgraph/cha"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/types"
)
var inputs = []string{
"testdata/func.go",
"testdata/iface.go",
"testdata/recv.go",
}
func expectation(f *ast.File) (string, token.Pos) {
for _, c := range f.Comments {
text := strings.TrimSpace(c.Text())
if t := strings.TrimPrefix(text, "WANT:\n"); t != text {
return t, c.Pos()
}
}
return "", token.NoPos
}
// TestCHA runs CHA on each file in inputs, prints the dynamic edges of
// the call graph, and compares it with the golden results embedded in
// the WANT comment at the end of the file.
//
func TestCHA(t *testing.T) {
for _, filename := range inputs {
content, err := ioutil.ReadFile(filename)
if err != nil {
t.Errorf("couldn't read file '%s': %s", filename, err)
continue
}
conf := loader.Config{
SourceImports: true,
ParserMode: parser.ParseComments,
}
f, err := conf.ParseFile(filename, content)
if err != nil {
t.Error(err)
continue
}
want, pos := expectation(f)
if pos == token.NoPos {
t.Errorf("No WANT: comment in %s", filename)
continue
}
conf.CreateFromFiles("main", f)
iprog, err := conf.Load()
if err != nil {
t.Error(err)
continue
}
prog := ssa.Create(iprog, 0)
mainPkg := prog.Package(iprog.Created[0].Pkg)
prog.BuildAll()
cg := cha.CallGraph(prog)
if got := printGraph(cg, mainPkg.Object); got != want {
t.Errorf("%s: got:\n%s\nwant:\n%s",
prog.Fset.Position(pos), got, want)
}
}
}
func printGraph(cg *callgraph.Graph, from *types.Package) string {
var edges []string
callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error {
if strings.Contains(e.Description(), "dynamic") {
edges = append(edges, fmt.Sprintf("%s --> %s",
e.Caller.Func.RelString(from),
e.Callee.Func.RelString(from)))
}
return nil
})
sort.Strings(edges)
var buf bytes.Buffer
buf.WriteString("Dynamic calls\n")
for _, edge := range edges {
fmt.Fprintf(&buf, " %s\n", edge)
}
return strings.TrimSpace(buf.String())
}

23
go/callgraph/cha/testdata/func.go vendored Normal file
View File

@ -0,0 +1,23 @@
//+build ignore
package main
// Test of dynamic function calls; no interfaces.
func A(int) {}
var (
B = func(int) {}
C = func(int) {}
)
func f() {
pfn := B
pfn(0) // calls A, B, C, even though A is not even address-taken
}
// WANT:
// Dynamic calls
// f --> A
// f --> init$1
// f --> init$2

65
go/callgraph/cha/testdata/iface.go vendored Normal file
View File

@ -0,0 +1,65 @@
//+build ignore
package main
// Test of interface calls. None of the concrete types are ever
// instantiated or converted to interfaces.
type I interface {
f()
}
type J interface {
f()
g()
}
type C int // implements I
func (*C) f()
type D int // implements I and J
func (*D) f()
func (*D) g()
func one(i I, j J) {
i.f() // calls *C and *D
}
func two(i I, j J) {
j.f() // calls *D (but not *C, even though it defines method f)
}
func three(i I, j J) {
j.g() // calls *D
}
func four(i I, j J) {
Jf := J.f
if unknown {
Jf = nil // suppress SSA constant propagation
}
Jf(nil) // calls *D
}
func five(i I, j J) {
jf := j.f
if unknown {
jf = nil // suppress SSA constant propagation
}
jf() // calls *D
}
var unknown bool
// WANT:
// Dynamic calls
// (J).f$bound --> (*D).f
// (J).f$thunk --> (*D).f
// five --> (J).f$bound
// four --> (J).f$thunk
// one --> (*C).f
// one --> (*D).f
// three --> (*D).g
// two --> (*D).f

37
go/callgraph/cha/testdata/recv.go vendored Normal file
View File

@ -0,0 +1,37 @@
//+build ignore
package main
type I interface {
f()
}
type J interface {
g()
}
type C int // C and *C implement I; *C implements J
func (C) f()
func (*C) g()
type D int // *D implements I and J
func (*D) f()
func (*D) g()
func f(i I) {
i.f() // calls C, *C, *D
}
func g(j J) {
j.g() // calls *C, *D
}
// WANT:
// Dynamic calls
// f --> (*C).f
// f --> (*D).f
// f --> (C).f
// g --> (*C).g
// g --> (*D).g

View File

@ -0,0 +1,33 @@
// Package static computes the call graph of a Go program containing
// only static call edges.
package static
import (
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/ssa"
"golang.org/x/tools/go/ssa/ssautil"
)
// CallGraph computes the call graph of the specified program
// considering only static calls.
//
func CallGraph(prog *ssa.Program) *callgraph.Graph {
cg := callgraph.New(nil) // TODO(adonovan) eliminate concept of rooted callgraph
// TODO(adonovan): opt: use only a single pass over the ssa.Program.
for f := range ssautil.AllFunctions(prog) {
fnode := cg.CreateNode(f)
for _, b := range f.Blocks {
for _, instr := range b.Instrs {
if site, ok := instr.(ssa.CallInstruction); ok {
if g := site.Common().StaticCallee(); g != nil {
gnode := cg.CreateNode(g)
callgraph.AddEdge(fnode, site, gnode)
}
}
}
}
}
return cg
}

View File

@ -0,0 +1,88 @@
// Copyright 2014 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 static_test
import (
"fmt"
"go/parser"
"reflect"
"sort"
"testing"
"golang.org/x/tools/go/callgraph"
"golang.org/x/tools/go/callgraph/static"
"golang.org/x/tools/go/loader"
"golang.org/x/tools/go/ssa"
)
const input = `package P
type C int
func (C) f()
type I interface{f()}
func f() {
p := func() {}
g()
p() // SSA constant propagation => static
if unknown {
p = h
}
p() // dynamic
C(0).f()
}
func g() {
var i I = C(0)
i.f()
}
func h()
var unknown bool
`
func TestStatic(t *testing.T) {
conf := loader.Config{ParserMode: parser.ParseComments}
f, err := conf.ParseFile("P.go", input)
if err != nil {
t.Fatal(err)
}
conf.CreateFromFiles("P", f)
iprog, err := conf.Load()
if err != nil {
t.Fatal(err)
}
P := iprog.Created[0].Pkg
prog := ssa.Create(iprog, 0)
prog.BuildAll()
cg := static.CallGraph(prog)
var edges []string
callgraph.GraphVisitEdges(cg, func(e *callgraph.Edge) error {
edges = append(edges, fmt.Sprintf("%s -> %s",
e.Caller.Func.RelString(P),
e.Callee.Func.RelString(P)))
return nil
})
sort.Strings(edges)
want := []string{
"(*C).f -> (C).f",
"f -> (C).f",
"f -> f$1",
"f -> g",
}
if !reflect.DeepEqual(edges, want) {
t.Errorf("Got edges %v, want %v", edges, want)
}
}