go/analysis/passes/bools: eliminate quadratic runtime, output
If you have x == 1 || x == 2 || x == 3 || x == 4, the pass considered the set
{x==1, x==2, x==3, x==4}
and then also
{x==2, x==3, x==4}
{x==3, x==4}
Since the comparison is itself linear in the size of the set, this was overall
taking time quadratic in the length of the || or && sequence.
Worse, if it found duplicates, they'd be reported a quadratic number of times.
This CL cuts the time and output to linear by avoiding already-checked
subexpressions. This cuts the time spent analyzing cmd/compile/internal/ssa
(with all passes enabled, not just this one) by 20%.
Fixes golang/go#28086.
Change-Id: I812f64bd5a44fea995c9ab0c4fa2fbefb44037ce
Reviewed-on: https://go-review.googlesource.com/c/tools/+/176457
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: Rob Pike <r@golang.org>
Reviewed-by: Alan Donovan <adonovan@google.com>
diff --git a/go/analysis/passes/bools/bools.go b/go/analysis/passes/bools/bools.go
index 833c9d7..c82d367 100644
--- a/go/analysis/passes/bools/bools.go
+++ b/go/analysis/passes/bools/bools.go
@@ -30,8 +30,13 @@
nodeFilter := []ast.Node{
(*ast.BinaryExpr)(nil),
}
+ seen := make(map[*ast.BinaryExpr]bool)
inspect.Preorder(nodeFilter, func(n ast.Node) {
e := n.(*ast.BinaryExpr)
+ if seen[e] {
+ // Already processed as a subexpression of an earlier node.
+ return
+ }
var op boolOp
switch e.Op {
@@ -43,10 +48,7 @@
return
}
- // TODO(adonovan): this reports n(n-1)/2 errors for an
- // expression e||...||e of depth n. Fix.
- // See https://golang.org/issue/28086.
- comm := op.commutativeSets(pass.TypesInfo, e)
+ comm := op.commutativeSets(pass.TypesInfo, e, seen)
for _, exprs := range comm {
op.checkRedundant(pass, exprs)
op.checkSuspect(pass, exprs)
@@ -70,8 +72,9 @@
// expressions in e that are connected by op.
// For example, given 'a || b || f() || c || d' with the or op,
// commutativeSets returns {{b, a}, {d, c}}.
-func (op boolOp) commutativeSets(info *types.Info, e *ast.BinaryExpr) [][]ast.Expr {
- exprs := op.split(e)
+// commutativeSets adds any expanded BinaryExprs to seen.
+func (op boolOp) commutativeSets(info *types.Info, e *ast.BinaryExpr, seen map[*ast.BinaryExpr]bool) [][]ast.Expr {
+ exprs := op.split(e, seen)
// Partition the slice of expressions into commutative sets.
i := 0
@@ -188,11 +191,13 @@
// split returns a slice of all subexpressions in e that are connected by op.
// For example, given 'a || (b || c) || d' with the or op,
// split returns []{d, c, b, a}.
-func (op boolOp) split(e ast.Expr) (exprs []ast.Expr) {
+// seen[e] is already true; any newly processed exprs are added to seen.
+func (op boolOp) split(e ast.Expr, seen map[*ast.BinaryExpr]bool) (exprs []ast.Expr) {
for {
e = unparen(e)
if b, ok := e.(*ast.BinaryExpr); ok && b.Op == op.tok {
- exprs = append(exprs, op.split(b.Y)...)
+ seen[b] = true
+ exprs = append(exprs, op.split(b.Y, seen)...)
e = b.X
} else {
exprs = append(exprs, e)
diff --git a/go/analysis/passes/bools/testdata/src/a/a.go b/go/analysis/passes/bools/testdata/src/a/a.go
index c4b7dfd..5358fbc 100644
--- a/go/analysis/passes/bools/testdata/src/a/a.go
+++ b/go/analysis/passes/bools/testdata/src/a/a.go
@@ -46,22 +46,17 @@
_ = i+1 == 1 || i+1 == 1 // want `redundant or: i\+1 == 1 \|\| i\+1 == 1`
_ = i == 1 || j+1 == i || i == 1 // want `redundant or: i == 1 \|\| i == 1`
- // The various r.* patterns are intended to match duplicate
- // diagnostics reported for the same underlying problem.
- // See https://golang.org/issue/28086.
- // TODO(adonovan): fix the checker.
-
- _ = i == 1 || i == 1 || f() == 1 // want `redundant or: i == 1 \|\| i == 1` `r.*`
+ _ = i == 1 || i == 1 || f() == 1 // want `redundant or: i == 1 \|\| i == 1`
_ = i == 1 || f() == 1 || i == 1 // OK f may alter i as a side effect
_ = f() == 1 || i == 1 || i == 1 // want `redundant or: i == 1 \|\| i == 1`
// Test partition edge cases
- _ = f() == 1 || i == 1 || i == 1 || j == 1 // want `redundant or: i == 1 \|\| i == 1` `r.*`
+ _ = f() == 1 || i == 1 || i == 1 || j == 1 // want `redundant or: i == 1 \|\| i == 1`
_ = f() == 1 || j == 1 || i == 1 || i == 1 // want `redundant or: i == 1 \|\| i == 1`
_ = i == 1 || f() == 1 || i == 1 || i == 1 // want `redundant or: i == 1 \|\| i == 1`
- _ = i == 1 || i == 1 || f() == 1 || i == 1 // want `redundant or: i == 1 \|\| i == 1` `r.*` `r.*`
- _ = i == 1 || i == 1 || j == 1 || f() == 1 // want `redundant or: i == 1 \|\| i == 1` `r.*` `r.*`
- _ = j == 1 || i == 1 || i == 1 || f() == 1 // want `redundant or: i == 1 \|\| i == 1` `r.*`
+ _ = i == 1 || i == 1 || f() == 1 || i == 1 // want `redundant or: i == 1 \|\| i == 1`
+ _ = i == 1 || i == 1 || j == 1 || f() == 1 // want `redundant or: i == 1 \|\| i == 1`
+ _ = j == 1 || i == 1 || i == 1 || f() == 1 // want `redundant or: i == 1 \|\| i == 1`
_ = i == 1 || f() == 1 || f() == 1 || i == 1
_ = i == 1 || (i == 1 || i == 2) // want `redundant or: i == 1 \|\| i == 1`
@@ -76,9 +71,9 @@
_ = j == 0 ||
i == 1 ||
f() == 1 ||
- j == 0 || // want `redundant or: j == 0 \|\| j == 0` `r.*`
- i == 1 || // want `redundant or: i == 1 \|\| i == 1` `r.*` `r.*` `r.*`
- i == 1 || // want `redundant or: i == 1 \|\| i == 1` `r.*` `r.*`
+ j == 0 || // want `redundant or: j == 0 \|\| j == 0`
+ i == 1 || // want `redundant or: i == 1 \|\| i == 1`
+ i == 1 || // want `redundant or: i == 1 \|\| i == 1`
i == 1 ||
j == 0 ||
k == 0
@@ -94,7 +89,7 @@
_ = 0 != <-c && 0 != <-c // OK subsequent receives may yield different values
_ = f() != 0 && f() != 0 // OK f might have side effects
_ = f != nil && f != nil // want `redundant and: f != nil && f != nil`
- _ = i != 1 && i != 1 && f() != 1 // want `redundant and: i != 1 && i != 1` `r.*`
+ _ = i != 1 && i != 1 && f() != 1 // want `redundant and: i != 1 && i != 1`
_ = i != 1 && f() != 1 && i != 1 // OK f may alter i as a side effect
_ = f() != 1 && i != 1 && i != 1 // want `redundant and: i != 1 && i != 1`
}