Skip to content

Commit 3d22fef

Browse files
adonovangopherbot
authored andcommitted
gopls/internal/analysis/modernize: disable minmax on floating point
The built-in min and max functions return NaN if any operand is NaN, so the minmax transformation is not sound for certain inputs. Since it is usually infeasible to prove that the operands are not NaN, this CL disables minmax for floating-point operands. Behavior-preserving translation: celebrating 75 years of being harder than it looks. Fixes golang/go#72829 Change-Id: Idb3454fea7ec37842e622154f66d5898703a392f Reviewed-on: https://go-review.googlesource.com/c/tools/+/657955 Auto-Submit: Alan Donovan <[email protected]> Commit-Queue: Alan Donovan <[email protected]> LUCI-TryBot-Result: Go LUCI <[email protected]> Reviewed-by: Robert Findley <[email protected]>
1 parent e7b4c64 commit 3d22fef

File tree

3 files changed

+50
-2
lines changed

3 files changed

+50
-2
lines changed

gopls/internal/analysis/modernize/minmax.go

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"golang.org/x/tools/go/ast/inspector"
1717
"golang.org/x/tools/internal/analysisinternal"
1818
"golang.org/x/tools/internal/astutil/cursor"
19+
"golang.org/x/tools/internal/typeparams"
1920
)
2021

2122
// The minmax pass replaces if/else statements with calls to min or max.
@@ -25,6 +26,10 @@ import (
2526
// 1. if a < b { x = a } else { x = b } => x = min(a, b)
2627
// 2. x = a; if a < b { x = b } => x = max(a, b)
2728
//
29+
// Pattern 1 requires that a is not NaN, and pattern 2 requires that b
30+
// is not Nan. Since this is hard to prove, we reject floating-point
31+
// numbers.
32+
//
2833
// Variants:
2934
// - all four ordered comparisons
3035
// - "x := a" or "x = a" or "var x = a" in pattern 2
@@ -172,15 +177,17 @@ func minmax(pass *analysis.Pass) {
172177
}
173178

174179
// Find all "if a < b { lhs = rhs }" statements.
180+
info := pass.TypesInfo
175181
inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
176-
for curFile := range filesUsing(inspect, pass.TypesInfo, "go1.21") {
182+
for curFile := range filesUsing(inspect, info, "go1.21") {
177183
astFile := curFile.Node().(*ast.File)
178184
for curIfStmt := range curFile.Preorder((*ast.IfStmt)(nil)) {
179185
ifStmt := curIfStmt.Node().(*ast.IfStmt)
180186
if compare, ok := ifStmt.Cond.(*ast.BinaryExpr); ok &&
181187
ifStmt.Init == nil &&
182188
isInequality(compare.Op) != 0 &&
183-
isAssignBlock(ifStmt.Body) {
189+
isAssignBlock(ifStmt.Body) &&
190+
!maybeNaN(info.TypeOf(ifStmt.Body.List[0].(*ast.AssignStmt).Lhs[0])) { // lhs
184191

185192
// Have: if a < b { lhs = rhs }
186193
check(astFile, curIfStmt, compare)
@@ -219,6 +226,21 @@ func isSimpleAssign(n ast.Node) bool {
219226
len(assign.Rhs) == 1
220227
}
221228

229+
// maybeNaN reports whether t is (or may be) a floating-point type.
230+
func maybeNaN(t types.Type) bool {
231+
// For now, we rely on core types.
232+
// TODO(adonovan): In the post-core-types future,
233+
// follow the approach of types.Checker.applyTypeFunc.
234+
t = typeparams.CoreType(t)
235+
if t == nil {
236+
return true // fail safe
237+
}
238+
if basic, ok := t.(*types.Basic); ok && basic.Info()&types.IsFloat != 0 {
239+
return true
240+
}
241+
return false
242+
}
243+
222244
// -- utils --
223245

224246
func is[T any](x any) bool {

gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,3 +136,16 @@ func fix72727(a, b int) {
136136
o = b
137137
}
138138
}
139+
140+
type myfloat float64
141+
142+
// The built-in min/max differ in their treatement of NaN,
143+
// so reject floating-point numbers (#72829).
144+
func nopeFloat(a, b myfloat) (res myfloat) {
145+
if a < b {
146+
res = a
147+
} else {
148+
res = b
149+
}
150+
return
151+
}

gopls/internal/analysis/modernize/testdata/src/minmax/minmax.go.golden

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,16 @@ func fix72727(a, b int) {
123123
// want "if statement can be modernized using max"
124124
o := max(a-42, b)
125125
}
126+
127+
type myfloat float64
128+
129+
// The built-in min/max differ in their treatement of NaN,
130+
// so reject floating-point numbers (#72829).
131+
func nopeFloat(a, b myfloat) (res myfloat) {
132+
if a < b {
133+
res = a
134+
} else {
135+
res = b
136+
}
137+
return
138+
}

0 commit comments

Comments
 (0)