Skip to content

Commit b74c098

Browse files
adonovangopherbot
authored andcommitted
go/ssa: exploit "noreturn" info to prune spurious CFG edges
This CL causes the buildssa analyzer to exploit interprocedural "noreturn" information (computed by the ctrlflow analyzer) when building SSA. This allows it to prune infeasible control-flow edges after a call that cannot return, such as t.Fatal or log.Fatal, making the dominator tree deeper and improving the precision of downstream analysis such as nilness and yield. For example, nilness can now report cases such as this: f, err := os.Open("") if err != nil { log.Fatal(err) // can't return } if err != nil { // "impossible condition" log.Fatal(err) } f.Close() and it now reports 26 (was 20) diagnostics in std. Similarly, gopls' yield analyzer is now silent where before it reported a false positive here: return func(yield func(int) bool) { for { if !yield(55) { // "yield may be called again after returning false" runtime.Goexit() } } } Various "internal" hooks were required to expose the three hidden accessor functions, which are logically equivalent to these potential future API additions: (cfg.CFG).NoReturn() bool (*ctrlflow.CFGs).NoReturn(*types.Func) bool (*ssa.Program).SetNoReturn(func (*types.Func) bool) See golang/go#76161 for the proposal. Change-Id: Ic91572a19f062babd520762dfe6d5749e12d8cd0 Reviewed-on: https://go-review.googlesource.com/c/tools/+/716920 Auto-Submit: Alan Donovan <adonovan@google.com> LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Markus Kusano <kusano@google.com>
1 parent 655fb07 commit b74c098

File tree

15 files changed

+389
-110
lines changed

15 files changed

+389
-110
lines changed

cmd/ssadump/main.go

Lines changed: 19 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -132,28 +132,30 @@ func doMain() error {
132132
return fmt.Errorf("packages contain errors")
133133
}
134134

135-
// Turn on instantiating generics during build if the program will be run.
136-
if *runFlag {
137-
mode |= ssa.InstantiateGenerics
138-
}
139-
140-
// Create SSA-form program representation.
141-
prog, pkgs := ssautil.AllPackages(initial, mode)
142-
143-
for i, p := range pkgs {
144-
if p == nil {
145-
return fmt.Errorf("cannot build SSA for package %s", initial[i])
146-
}
147-
}
148-
149135
if !*runFlag {
150-
// Build and display only the initial packages
151-
// (and synthetic wrappers).
152-
for _, p := range pkgs {
136+
// Create (and display) SSA only for initial packages and wrappers.
137+
_, pkgs := ssautil.Packages(initial, mode)
138+
for i, p := range pkgs {
139+
if p == nil {
140+
return fmt.Errorf("cannot build SSA for package %s", initial[i])
141+
}
153142
p.Build()
154143
}
155144

156145
} else {
146+
// Create SSA for initial packages and all dependencies, instantiating generics.
147+
mode |= ssa.InstantiateGenerics
148+
149+
// TODO(adonovan): opt: use noreturn analysis over transitive
150+
// dependencies to prune spurious control flow graph edges.
151+
152+
prog, pkgs := ssautil.AllPackages(initial, mode)
153+
for i, p := range pkgs {
154+
if p == nil {
155+
return fmt.Errorf("cannot build SSA for package %s", initial[i])
156+
}
157+
}
158+
157159
// Run the interpreter.
158160
// Build SSA for all packages.
159161
prog.Build()

go/analysis/passes/buildssa/buildssa.go

Lines changed: 48 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,28 @@ package buildssa
1111
import (
1212
"go/ast"
1313
"go/types"
14+
"iter"
1415
"reflect"
1516

1617
"golang.org/x/tools/go/analysis"
18+
"golang.org/x/tools/go/analysis/passes/ctrlflow"
19+
"golang.org/x/tools/go/analysis/passes/internal/ctrlflowinternal"
1720
"golang.org/x/tools/go/ssa"
21+
"golang.org/x/tools/internal/ssainternal"
1822
)
1923

2024
var Analyzer = &analysis.Analyzer{
2125
Name: "buildssa",
2226
Doc: "build SSA-form IR for later passes",
2327
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/buildssa",
2428
Run: run,
25-
ResultType: reflect.TypeOf(new(SSA)),
29+
Requires: []*analysis.Analyzer{ctrlflow.Analyzer},
30+
ResultType: reflect.TypeFor[*SSA](),
31+
// Do not add FactTypes here: SSA construction of P must not
32+
// require SSA construction of all of P's dependencies.
33+
// (That's why we enlist the cheaper ctrlflow pass to compute
34+
// noreturn instead of having go/ssa + buildssa do it.)
35+
FactTypes: nil,
2636
}
2737

2838
// SSA provides SSA-form intermediate representation for all the
@@ -33,6 +43,8 @@ type SSA struct {
3343
}
3444

3545
func run(pass *analysis.Pass) (any, error) {
46+
cfgs := pass.ResultOf[ctrlflow.Analyzer].(*ctrlflow.CFGs)
47+
3648
// We must create a new Program for each Package because the
3749
// analysis API provides no place to hang a Program shared by
3850
// all Packages. Consequently, SSA Packages and Functions do not
@@ -49,6 +61,11 @@ func run(pass *analysis.Pass) (any, error) {
4961

5062
prog := ssa.NewProgram(pass.Fset, mode)
5163

64+
// Use the result of the ctrlflow analysis to improve the SSA CFG.
65+
ssainternal.SetNoReturn(prog, func(fn *types.Func) bool {
66+
return ctrlflowinternal.NoReturn(cfgs, fn)
67+
})
68+
5269
// Create SSA packages for direct imports.
5370
for _, p := range pass.Pkg.Imports() {
5471
prog.CreatePackage(p, nil, nil, true)
@@ -61,34 +78,41 @@ func run(pass *analysis.Pass) (any, error) {
6178
// Compute list of source functions, including literals,
6279
// in source order.
6380
var funcs []*ssa.Function
64-
for _, f := range pass.Files {
65-
for _, decl := range f.Decls {
66-
if fdecl, ok := decl.(*ast.FuncDecl); ok {
67-
// (init functions have distinct Func
68-
// objects named "init" and distinct
69-
// ssa.Functions named "init#1", ...)
70-
71-
fn := pass.TypesInfo.Defs[fdecl.Name].(*types.Func)
72-
if fn == nil {
73-
panic(fn)
74-
}
81+
for _, fn := range allFunctions(pass) {
82+
// (init functions have distinct Func
83+
// objects named "init" and distinct
84+
// ssa.Functions named "init#1", ...)
7585

76-
f := ssapkg.Prog.FuncValue(fn)
77-
if f == nil {
78-
panic(fn)
79-
}
86+
f := ssapkg.Prog.FuncValue(fn)
87+
if f == nil {
88+
panic(fn)
89+
}
8090

81-
var addAnons func(f *ssa.Function)
82-
addAnons = func(f *ssa.Function) {
83-
funcs = append(funcs, f)
84-
for _, anon := range f.AnonFuncs {
85-
addAnons(anon)
86-
}
87-
}
88-
addAnons(f)
91+
var addAnons func(f *ssa.Function)
92+
addAnons = func(f *ssa.Function) {
93+
funcs = append(funcs, f)
94+
for _, anon := range f.AnonFuncs {
95+
addAnons(anon)
8996
}
9097
}
98+
addAnons(f)
9199
}
92100

93101
return &SSA{Pkg: ssapkg, SrcFuncs: funcs}, nil
94102
}
103+
104+
// allFunctions returns an iterator over all named functions.
105+
func allFunctions(pass *analysis.Pass) iter.Seq2[*ast.FuncDecl, *types.Func] {
106+
return func(yield func(*ast.FuncDecl, *types.Func) bool) {
107+
for _, file := range pass.Files {
108+
for _, decl := range file.Decls {
109+
if decl, ok := decl.(*ast.FuncDecl); ok {
110+
fn := pass.TypesInfo.Defs[decl.Name].(*types.Func)
111+
if !yield(decl, fn) {
112+
return
113+
}
114+
}
115+
}
116+
}
117+
}
118+
}

go/analysis/passes/ctrlflow/ctrlflow.go

Lines changed: 51 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -16,17 +16,20 @@ import (
1616

1717
"golang.org/x/tools/go/analysis"
1818
"golang.org/x/tools/go/analysis/passes/inspect"
19+
"golang.org/x/tools/go/analysis/passes/internal/ctrlflowinternal"
1920
"golang.org/x/tools/go/ast/inspector"
2021
"golang.org/x/tools/go/cfg"
2122
"golang.org/x/tools/go/types/typeutil"
23+
"golang.org/x/tools/internal/cfginternal"
24+
"golang.org/x/tools/internal/typesinternal"
2225
)
2326

2427
var Analyzer = &analysis.Analyzer{
2528
Name: "ctrlflow",
2629
Doc: "build a control-flow graph",
2730
URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/ctrlflow",
2831
Run: run,
29-
ResultType: reflect.TypeOf(new(CFGs)),
32+
ResultType: reflect.TypeFor[*CFGs](),
3033
FactTypes: []analysis.Fact{new(noReturn)},
3134
Requires: []*analysis.Analyzer{inspect.Analyzer},
3235
}
@@ -44,7 +47,20 @@ type CFGs struct {
4447
defs map[*ast.Ident]types.Object // from Pass.TypesInfo.Defs
4548
funcDecls map[*types.Func]*declInfo
4649
funcLits map[*ast.FuncLit]*litInfo
47-
pass *analysis.Pass // transient; nil after construction
50+
noReturn map[*types.Func]bool // functions lacking a reachable return statement
51+
pass *analysis.Pass // transient; nil after construction
52+
}
53+
54+
// TODO(adonovan): add (*CFGs).NoReturn to public API.
55+
func (c *CFGs) isNoReturn(fn *types.Func) bool {
56+
return c.noReturn[fn]
57+
}
58+
59+
func init() {
60+
// Expose the hidden method to callers in x/tools.
61+
ctrlflowinternal.NoReturn = func(c any, fn *types.Func) bool {
62+
return c.(*CFGs).isNoReturn(fn)
63+
}
4864
}
4965

5066
// CFGs has two maps: funcDecls for named functions and funcLits for
@@ -54,15 +70,14 @@ type CFGs struct {
5470
// *types.Func but not the other way.
5571

5672
type declInfo struct {
57-
decl *ast.FuncDecl
58-
cfg *cfg.CFG // iff decl.Body != nil
59-
started bool // to break cycles
60-
noReturn bool
73+
decl *ast.FuncDecl
74+
cfg *cfg.CFG // iff decl.Body != nil
75+
started bool // to break cycles
6176
}
6277

6378
type litInfo struct {
6479
cfg *cfg.CFG
65-
noReturn bool
80+
noReturn bool // (currently unused)
6681
}
6782

6883
// FuncDecl returns the control-flow graph for a named function.
@@ -118,6 +133,7 @@ func run(pass *analysis.Pass) (any, error) {
118133
defs: pass.TypesInfo.Defs,
119134
funcDecls: funcDecls,
120135
funcLits: funcLits,
136+
noReturn: make(map[*types.Func]bool),
121137
pass: pass,
122138
}
123139

@@ -138,7 +154,7 @@ func run(pass *analysis.Pass) (any, error) {
138154
li := funcLits[lit]
139155
if li.cfg == nil {
140156
li.cfg = cfg.New(lit.Body, c.callMayReturn)
141-
if !hasReachableReturn(li.cfg) {
157+
if cfginternal.IsNoReturn(li.cfg) {
142158
li.noReturn = true
143159
}
144160
}
@@ -158,27 +174,28 @@ func (c *CFGs) buildDecl(fn *types.Func, di *declInfo) {
158174
// The buildDecl call tree thus resembles the static call graph.
159175
// We mark each node when we start working on it to break cycles.
160176

161-
if !di.started { // break cycle
162-
di.started = true
177+
if di.started {
178+
return // break cycle
179+
}
180+
di.started = true
163181

164-
if isIntrinsicNoReturn(fn) {
165-
di.noReturn = true
166-
}
167-
if di.decl.Body != nil {
168-
di.cfg = cfg.New(di.decl.Body, c.callMayReturn)
169-
if !hasReachableReturn(di.cfg) {
170-
di.noReturn = true
171-
}
172-
}
173-
if di.noReturn {
174-
c.pass.ExportObjectFact(fn, new(noReturn))
175-
}
182+
noreturn := isIntrinsicNoReturn(fn)
176183

177-
// debugging
178-
if false {
179-
log.Printf("CFG for %s:\n%s (noreturn=%t)\n", fn, di.cfg.Format(c.pass.Fset), di.noReturn)
184+
if di.decl.Body != nil {
185+
di.cfg = cfg.New(di.decl.Body, c.callMayReturn)
186+
if cfginternal.IsNoReturn(di.cfg) {
187+
noreturn = true
180188
}
181189
}
190+
if noreturn {
191+
c.pass.ExportObjectFact(fn, new(noReturn))
192+
c.noReturn[fn] = true
193+
}
194+
195+
// debugging
196+
if false {
197+
log.Printf("CFG for %s:\n%s (noreturn=%t)\n", fn, di.cfg.Format(c.pass.Fset), noreturn)
198+
}
182199
}
183200

184201
// callMayReturn reports whether the called function may return.
@@ -201,31 +218,26 @@ func (c *CFGs) callMayReturn(call *ast.CallExpr) (r bool) {
201218
// Function or method declared in this package?
202219
if di, ok := c.funcDecls[fn]; ok {
203220
c.buildDecl(fn, di)
204-
return !di.noReturn
221+
return !c.noReturn[fn]
205222
}
206223

207224
// Not declared in this package.
208225
// Is there a fact from another package?
209-
return !c.pass.ImportObjectFact(fn, new(noReturn))
226+
if c.pass.ImportObjectFact(fn, new(noReturn)) {
227+
c.noReturn[fn] = true
228+
return false
229+
}
230+
231+
return true
210232
}
211233

212234
var panicBuiltin = types.Universe.Lookup("panic").(*types.Builtin)
213235

214-
func hasReachableReturn(g *cfg.CFG) bool {
215-
for _, b := range g.Blocks {
216-
if b.Live && b.Return() != nil {
217-
return true
218-
}
219-
}
220-
return false
221-
}
222-
223236
// isIntrinsicNoReturn reports whether a function intrinsically never
224237
// returns because it stops execution of the calling thread.
225238
// It is the base case in the recursion.
226239
func isIntrinsicNoReturn(fn *types.Func) bool {
227240
// Add functions here as the need arises, but don't allocate memory.
228-
path, name := fn.Pkg().Path(), fn.Name()
229-
return path == "syscall" && (name == "Exit" || name == "ExitProcess" || name == "ExitThread") ||
230-
path == "runtime" && name == "Goexit"
241+
return typesinternal.IsFunctionNamed(fn, "syscall", "Exit", "ExitProcess", "ExitThread") ||
242+
typesinternal.IsFunctionNamed(fn, "runtime", "Goexit")
231243
}

0 commit comments

Comments
 (0)