Skip to content

Commit b80f1ed

Browse files
committed
gopls/internal/analysis/yield: peephole-optimize phi(false, x)
The yield analyzer reports spurious diagnostics for constructs such as: func f(yield func() bool) { ok := yield() // yield may be called again ok = ok && yield() // yield may be called again ok = ok && yield() } This CL reduces phi(false, yield()) conditions as a special case. The test suite includes a counterexample that still elicits a false positive. If variants of this sort show up in practice, we may need a more systematic treatment of dominating conditions, similar to the nilness analysis, or even a full monotone framework with lattice joins and fixed point iteration. Fixes golang/go#70598 Change-Id: Ia602602cf5c233820d8ddbe73a286799bb3ea16c Reviewed-on: https://go-review.googlesource.com/c/tools/+/632117 LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com> Reviewed-by: Robert Findley <rfindley@google.com>
1 parent e7bd227 commit b80f1ed

File tree

2 files changed

+70
-2
lines changed

2 files changed

+70
-2
lines changed

gopls/internal/analysis/yield/testdata/src/a/a.go

+35
Original file line numberDiff line numberDiff line change
@@ -83,3 +83,38 @@ func tricky(in io.ReadCloser) func(yield func(string, error) bool) {
8383
}
8484
}
8585
}
86+
87+
// Regression test for issue #70598.
88+
func shortCircuitAND(yield func(int) bool) {
89+
ok := yield(1)
90+
ok = ok && yield(2)
91+
ok = ok && yield(3)
92+
ok = ok && yield(4)
93+
}
94+
95+
// This example has a bug because a false yield(2) may be followed by yield(3).
96+
func tricky2(yield func(int) bool) {
97+
cleanup := func() {}
98+
ok := yield(1) // want "yield may be called again .on L104"
99+
stop := !ok || yield(2) // want "yield may be called again .on L104"
100+
if stop {
101+
cleanup()
102+
} else {
103+
// dominated by !stop => !(!ok || yield(2)) => yield(1) && !yield(2): bad.
104+
yield(3)
105+
}
106+
}
107+
108+
// This example is sound, but the analyzer reports a false positive.
109+
// TODO(adonovan): prune infeasible paths more carefully.
110+
func tricky3(yield func(int) bool) {
111+
cleanup := func() {}
112+
ok := yield(1) // want "yield may be called again .on L118"
113+
stop := !ok || !yield(2) // want "yield may be called again .on L118"
114+
if stop {
115+
cleanup()
116+
} else {
117+
// dominated by !stop => !(!ok || !yield(2)) => yield(1) && yield(2): good.
118+
yield(3)
119+
}
120+
}

gopls/internal/analysis/yield/yield.go

+35-2
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ import (
2020
_ "embed"
2121
"fmt"
2222
"go/ast"
23+
"go/constant"
2324
"go/token"
2425
"go/types"
2526

@@ -119,9 +120,34 @@ func run(pass *analysis.Pass) (interface{}, error) {
119120
// In that case visit only the "if !yield()" block.
120121
cond := instr.Cond
121122
t, f := b.Succs[0], b.Succs[1]
122-
if unop, ok := cond.(*ssa.UnOp); ok && unop.Op == token.NOT {
123-
cond, t, f = unop.X, f, t
123+
124+
// Strip off any NOT operator.
125+
cond, t, f = unnegate(cond, t, f)
126+
127+
// As a peephole optimization for this special case:
128+
// ok := yield()
129+
// ok = ok && yield()
130+
// ok = ok && yield()
131+
// which in SSA becomes:
132+
// yield()
133+
// phi(false, yield())
134+
// phi(false, yield())
135+
// we reduce a cond of phi(false, x) to just x.
136+
if phi, ok := cond.(*ssa.Phi); ok {
137+
var nonFalse []ssa.Value
138+
for _, v := range phi.Edges {
139+
if c, ok := v.(*ssa.Const); ok &&
140+
!constant.BoolVal(c.Value) {
141+
continue // constant false
142+
}
143+
nonFalse = append(nonFalse, v)
144+
}
145+
if len(nonFalse) == 1 {
146+
cond = nonFalse[0]
147+
cond, t, f = unnegate(cond, t, f)
148+
}
124149
}
150+
125151
if cond, ok := cond.(*ssa.Call); ok && ssaYieldCalls[cond] != nil {
126152
// Skip the successor reached by "if yield() { ... }".
127153
} else {
@@ -143,3 +169,10 @@ func run(pass *analysis.Pass) (interface{}, error) {
143169

144170
return nil, nil
145171
}
172+
173+
func unnegate(cond ssa.Value, t, f *ssa.BasicBlock) (_ ssa.Value, _, _ *ssa.BasicBlock) {
174+
if unop, ok := cond.(*ssa.UnOp); ok && unop.Op == token.NOT {
175+
return unop.X, f, t
176+
}
177+
return cond, t, f
178+
}

0 commit comments

Comments
 (0)