Skip to content

Commit e0d37a3

Browse files
committed
cmd/compile: teach prove to handle expressions like len(s)-delta
When a loop has bound len(s)-delta, findIndVar detected it and returned len(s) as (conservative) upper bound. This little lie allowed loopbce to drop bound checks. It is obviously more generic to teach prove about relations like x+d<w for non-constant "w"; we already handled the case for constant "w", so we just want to learn that if d<0, then x+d<w proves that x<w. To be able to remove the code from findIndVar, we also need to teach prove that len() and cap() are always non-negative. This CL allows to prove 633 more checks in cmd+std. Most of them are cases where the code was already testing before accessing a slice but the compiler didn't know it. For instance, take strings.HasSuffix: func HasSuffix(s, suffix string) bool { return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix } When suffix is a literal string, the compiler now understands that the explicit check is enough to not emit a slice check. I also found a loopbce test that was incorrectly written to detect an overflow but had a off-by-one (on the conservative side), so it unexpectly passed with this CL; I changed it to really trigger the overflow as intended. Change-Id: Ib5abade337db46b8811425afebad4719b6e46c4a Reviewed-on: https://go-review.googlesource.com/105635 Run-TryBot: Giovanni Bajo <rasky@develer.com> TryBot-Result: Gobot Gobot <gobot@golang.org> Reviewed-by: David Chase <drchase@google.com>
1 parent 6d379ad commit e0d37a3

File tree

4 files changed

+147
-69
lines changed

4 files changed

+147
-69
lines changed

src/cmd/compile/internal/ssa/loopbce.go

-7
Original file line numberDiff line numberDiff line change
@@ -150,13 +150,6 @@ nextb:
150150
continue
151151
}
152152

153-
// If max is c + SliceLen with c <= 0 then we drop c.
154-
// Makes sure c + SliceLen doesn't overflow when SliceLen == 0.
155-
// TODO: save c as an offset from max.
156-
if w, c := dropAdd64(max); (w.Op == OpStringLen || w.Op == OpSliceLen) && 0 >= c && -c >= 0 {
157-
max = w
158-
}
159-
160153
// We can only guarantee that the loops runs within limits of induction variable
161154
// if the increment is ±1 or when the limits are constants.
162155
if inc.AuxInt != 1 && inc.AuxInt != -1 {

src/cmd/compile/internal/ssa/prove.go

+86-59
Original file line numberDiff line numberDiff line change
@@ -389,70 +389,78 @@ func (ft *factsTable) update(parent *Block, v, w *Value, d domain, r relation) {
389389
}
390390
}
391391

392-
// Process: x+delta > w (with delta,w constants)
393-
//
394-
// We want to derive: x+delta > w ⇒ x > w-delta
395-
//
396-
// We do this for signed numbers for now, as that allows to prove many
397-
// accesses to slices in loops.
398-
//
399-
// From x+delta > w, we compute (using integers of the correct size):
400-
// min = w - delta
401-
// max = MaxInt - delta
402-
//
403-
// And we prove that:
404-
// if min<max: min < x AND x <= max
405-
// if min>max: min < x OR x <= max
406-
//
407-
// This is always correct, even in case of overflow.
408-
//
409-
// If the initial fact is x+delta >= w instead, the derived conditions are:
410-
// if min<max: min <= x AND x <= max
411-
// if min>max: min <= x OR x <= max
412-
//
413-
// Notice the conditions for max are still <=, as they handle overflows.
392+
// Process: x+delta > w (with delta constant)
393+
// Only signed domain for now (useful for accesses to slices in loops).
414394
if r == gt || r == gt|eq {
415-
if x, delta := isConstDelta(v); x != nil && w.isGenericIntConst() && d == signed {
395+
if x, delta := isConstDelta(v); x != nil && d == signed {
416396
if parent.Func.pass.debug > 1 {
417397
parent.Func.Warnl(parent.Pos, "x+d >= w; x:%v %v delta:%v w:%v d:%v", x, parent.String(), delta, w.AuxInt, d)
418398
}
419-
420-
var min, max int64
421-
var vmin, vmax *Value
422-
switch x.Type.Size() {
423-
case 8:
424-
min = w.AuxInt - delta
425-
max = int64(^uint64(0)>>1) - delta
426-
427-
vmin = parent.NewValue0I(parent.Pos, OpConst64, parent.Func.Config.Types.Int64, min)
428-
vmax = parent.NewValue0I(parent.Pos, OpConst64, parent.Func.Config.Types.Int64, max)
429-
430-
case 4:
431-
min = int64(int32(w.AuxInt) - int32(delta))
432-
max = int64(int32(^uint32(0)>>1) - int32(delta))
433-
434-
vmin = parent.NewValue0I(parent.Pos, OpConst32, parent.Func.Config.Types.Int32, min)
435-
vmax = parent.NewValue0I(parent.Pos, OpConst32, parent.Func.Config.Types.Int32, max)
436-
437-
default:
438-
panic("unimplemented")
439-
}
440-
441-
if min < max {
442-
// Record that x > min and max >= x
443-
ft.update(parent, x, vmin, d, r)
444-
ft.update(parent, vmax, x, d, r|eq)
399+
if !w.isGenericIntConst() {
400+
// If we know that x+delta > w but w is not constant, we can derive:
401+
// if delta < 0 and x > MinInt - delta, then x > w (because x+delta cannot underflow)
402+
// This is useful for loops with bounds "len(slice)-K" (delta = -K)
403+
if l, has := ft.limits[x.ID]; has && delta < 0 {
404+
if (x.Type.Size() == 8 && l.min >= math.MinInt64-delta) ||
405+
(x.Type.Size() == 4 && l.min >= math.MinInt32-delta) {
406+
ft.update(parent, x, w, signed, r)
407+
}
408+
}
445409
} else {
446-
// We know that either x>min OR x<=max. factsTable cannot record OR conditions,
447-
// so let's see if we can already prove that one of them is false, in which case
448-
// the other must be true
449-
if l, has := ft.limits[x.ID]; has {
450-
if l.max <= min {
451-
// x>min is impossible, so it must be x<=max
452-
ft.update(parent, vmax, x, d, r|eq)
453-
} else if l.min > max {
454-
// x<=max is impossible, so it must be x>min
455-
ft.update(parent, x, vmin, d, r)
410+
// With w,delta constants, we want to derive: x+delta > w ⇒ x > w-delta
411+
//
412+
// We compute (using integers of the correct size):
413+
// min = w - delta
414+
// max = MaxInt - delta
415+
//
416+
// And we prove that:
417+
// if min<max: min < x AND x <= max
418+
// if min>max: min < x OR x <= max
419+
//
420+
// This is always correct, even in case of overflow.
421+
//
422+
// If the initial fact is x+delta >= w instead, the derived conditions are:
423+
// if min<max: min <= x AND x <= max
424+
// if min>max: min <= x OR x <= max
425+
//
426+
// Notice the conditions for max are still <=, as they handle overflows.
427+
var min, max int64
428+
var vmin, vmax *Value
429+
switch x.Type.Size() {
430+
case 8:
431+
min = w.AuxInt - delta
432+
max = int64(^uint64(0)>>1) - delta
433+
434+
vmin = parent.NewValue0I(parent.Pos, OpConst64, parent.Func.Config.Types.Int64, min)
435+
vmax = parent.NewValue0I(parent.Pos, OpConst64, parent.Func.Config.Types.Int64, max)
436+
437+
case 4:
438+
min = int64(int32(w.AuxInt) - int32(delta))
439+
max = int64(int32(^uint32(0)>>1) - int32(delta))
440+
441+
vmin = parent.NewValue0I(parent.Pos, OpConst32, parent.Func.Config.Types.Int32, min)
442+
vmax = parent.NewValue0I(parent.Pos, OpConst32, parent.Func.Config.Types.Int32, max)
443+
444+
default:
445+
panic("unimplemented")
446+
}
447+
448+
if min < max {
449+
// Record that x > min and max >= x
450+
ft.update(parent, x, vmin, d, r)
451+
ft.update(parent, vmax, x, d, r|eq)
452+
} else {
453+
// We know that either x>min OR x<=max. factsTable cannot record OR conditions,
454+
// so let's see if we can already prove that one of them is false, in which case
455+
// the other must be true
456+
if l, has := ft.limits[x.ID]; has {
457+
if l.max <= min {
458+
// x>min is impossible, so it must be x<=max
459+
ft.update(parent, vmax, x, d, r|eq)
460+
} else if l.min > max {
461+
// x<=max is impossible, so it must be x>min
462+
ft.update(parent, x, vmin, d, r)
463+
}
456464
}
457465
}
458466
}
@@ -661,24 +669,43 @@ func prove(f *Func) {
661669
ft := newFactsTable()
662670

663671
// Find length and capacity ops.
672+
var zero *Value
664673
for _, b := range f.Blocks {
665674
for _, v := range b.Values {
675+
// If we found a zero constant, save it (so we don't have
676+
// to build one later).
677+
if zero == nil && v.Op == OpConst64 && v.AuxInt == 0 {
678+
zero = v
679+
}
666680
if v.Uses == 0 {
667681
// We don't care about dead values.
668682
// (There can be some that are CSEd but not removed yet.)
669683
continue
670684
}
671685
switch v.Op {
686+
case OpStringLen:
687+
if zero == nil {
688+
zero = b.NewValue0I(b.Pos, OpConst64, f.Config.Types.Int64, 0)
689+
}
690+
ft.update(b, v, zero, signed, gt|eq)
672691
case OpSliceLen:
673692
if ft.lens == nil {
674693
ft.lens = map[ID]*Value{}
675694
}
676695
ft.lens[v.Args[0].ID] = v
696+
if zero == nil {
697+
zero = b.NewValue0I(b.Pos, OpConst64, f.Config.Types.Int64, 0)
698+
}
699+
ft.update(b, v, zero, signed, gt|eq)
677700
case OpSliceCap:
678701
if ft.caps == nil {
679702
ft.caps = map[ID]*Value{}
680703
}
681704
ft.caps[v.Args[0].ID] = v
705+
if zero == nil {
706+
zero = b.NewValue0I(b.Pos, OpConst64, f.Config.Types.Int64, 0)
707+
}
708+
ft.update(b, v, zero, signed, gt|eq)
682709
}
683710
}
684711
}

test/loopbce.go

+24-1
Original file line numberDiff line numberDiff line change
@@ -100,6 +100,22 @@ func g0d(a string) int {
100100
return x
101101
}
102102

103+
func g0e(a string) int {
104+
x := 0
105+
for i := len(a) - 1; i >= 0; i-- { // ERROR "Induction variable: limits \[0,\?\], increment -1$"
106+
x += int(a[i]) // ERROR "Proved IsInBounds$"
107+
}
108+
return x
109+
}
110+
111+
func g0f(a string) int {
112+
x := 0
113+
for i := len(a) - 1; 0 <= i; i-- { // ERROR "Induction variable: limits \[0,\?\], increment -1$"
114+
x += int(a[i]) // ERROR "Proved IsInBounds$"
115+
}
116+
return x
117+
}
118+
103119
func g1() int {
104120
a := "evenlength"
105121
x := 0
@@ -265,7 +281,14 @@ func nobce2(a string) {
265281
useString(a[i:]) // ERROR "Proved IsSliceInBounds$"
266282
}
267283
for i := int64(0); i < int64(len(a))+int64(-1<<63); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$"
268-
// tests an overflow of StringLen-MinInt64
284+
useString(a[i:]) // ERROR "Proved IsSliceInBounds$"
285+
}
286+
j := int64(len(a)) - 123
287+
for i := int64(0); i < j+123+int64(-1<<63); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$"
288+
useString(a[i:]) // ERROR "Proved IsSliceInBounds$"
289+
}
290+
for i := int64(0); i < j+122+int64(-1<<63); i++ { // ERROR "Induction variable: limits \[0,\?\), increment 1$"
291+
// len(a)-123+122+MinInt overflows when len(a) == 0, so a bound check is needed here
269292
useString(a[i:])
270293
}
271294
}

test/prove.go

+37-2
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,8 @@ func f1b(a []int, i int, j uint) int {
4242
if i >= 10 && i < len(a) {
4343
return a[i] // ERROR "Proved IsInBounds$"
4444
}
45-
if i >= 10 && i < len(a) { // todo: handle this case
46-
return a[i-10]
45+
if i >= 10 && i < len(a) {
46+
return a[i-10] // ERROR "Proved IsInBounds$"
4747
}
4848
if j < uint(len(a)) {
4949
return a[j] // ERROR "Proved IsInBounds$"
@@ -613,6 +613,41 @@ func trans3(a, b []int, i int) {
613613
_ = b[i] // ERROR "Proved IsInBounds$"
614614
}
615615

616+
// Derived from nat.cmp
617+
func natcmp(x, y []uint) (r int) {
618+
m := len(x)
619+
n := len(y)
620+
if m != n || m == 0 {
621+
return
622+
}
623+
624+
i := m - 1
625+
for i > 0 && // ERROR "Induction variable: limits \(0,\?\], increment -1"
626+
x[i] == // ERROR "Proved IsInBounds$"
627+
y[i] { // ERROR "Proved IsInBounds$"
628+
i--
629+
}
630+
631+
switch {
632+
case x[i] < // todo, cannot prove this because it's dominated by i<=0 || x[i]==y[i]
633+
y[i]: // ERROR "Proved IsInBounds$"
634+
r = -1
635+
case x[i] > // ERROR "Proved IsInBounds$"
636+
y[i]: // ERROR "Proved IsInBounds$"
637+
r = 1
638+
}
639+
return
640+
}
641+
642+
func suffix(s, suffix string) bool {
643+
// todo, we're still not able to drop the bound check here in the general case
644+
return len(s) >= len(suffix) && s[len(s)-len(suffix):] == suffix
645+
}
646+
647+
func constsuffix(s string) bool {
648+
return suffix(s, "abc") // ERROR "Proved IsSliceInBounds$"
649+
}
650+
616651
//go:noinline
617652
func useInt(a int) {
618653
}

0 commit comments

Comments
 (0)