Skip to content

Commit c20d959

Browse files
committed
cmd/compile: experimental loop iterator capture semantics change
Adds: GOEXPERIMENT=loopvar (expected way of invoking) -d=loopvar={-1,0,1,2,11,12} (for per-package control and/or logging) -d=loopvarhash=... (for hash debugging) loopvar=11,12 are for testing, benchmarking, and debugging. If enabled,for loops of the form `for x,y := range thing`, if x and/or y are addressed or captured by a closure, are transformed by renaming x/y to a temporary and prepending an assignment to the body of the loop x := tmp_x. This changes the loop semantics by making each iteration's instance of x be distinct from the others (currently they are all aliased, and when this matters, it is almost always a bug). 3-range with captured iteration variables are also transformed, though it is a more complex transformation. "Optimized" to do a simpler transformation for 3-clause for where the increment is empty. (Prior optimization of address-taking under Return disabled, because it was incorrect; returns can have loops for children. Restored in a later CL.) Includes support for -d=loopvarhash=<binary string> intended for use with hash search and GOCOMPILEDEBUG=loopvarhash=<binary string> (use `gossahash -e loopvarhash command-that-fails`). Minor feature upgrades to hash-triggered features; clients can specify that file-position hashes use only the most-inline position, and/or that they use only the basenames of source files (not the full directory path). Most-inlined is the right choice for debugging loop-iteration change once the semantics are linked to the package across inlining; basename-only makes it tractable to write tests (which, otherwise, depend on the full pathname of the source file and thus vary). Updates #57969. Change-Id: I180a51a3f8d4173f6210c861f10de23de8a1b1db Reviewed-on: https://go-review.googlesource.com/c/go/+/411904 Reviewed-by: Matthew Dempsky <[email protected]> Run-TryBot: David Chase <[email protected]> TryBot-Result: Gopher Robot <[email protected]>
1 parent dbdb335 commit c20d959

30 files changed

+1462
-20
lines changed

src/cmd/compile/internal/base/debug.go

+2
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,8 @@ type DebugFlags struct {
3333
InlStaticInit int `help:"allow static initialization of inlined calls" concurrent:"ok"`
3434
InterfaceCycles int `help:"allow anonymous interface cycles"`
3535
Libfuzzer int `help:"enable coverage instrumentation for libfuzzer"`
36+
LoopVar int `help:"shared (0, default), 1 (private loop variables), 2, private + log"`
37+
LoopVarHash string `help:"for debugging changes in loop behavior. Overrides experiment and loopvar flag."`
3638
LocationLists int `help:"print information about DWARF location list creation"`
3739
Nil int `help:"print information about nil checks"`
3840
NoOpenDefer int `help:"disable open-coded defers" concurrent:"ok"`

src/cmd/compile/internal/base/flag.go

+55-1
Original file line numberDiff line numberDiff line change
@@ -186,7 +186,61 @@ func ParseFlags() {
186186
}
187187

188188
if Debug.Gossahash != "" {
189-
hashDebug = NewHashDebug("gosshash", Debug.Gossahash, nil)
189+
hashDebug = NewHashDebug("gossahash", Debug.Gossahash, nil)
190+
}
191+
192+
// Three inputs govern loop iteration variable rewriting, hash, experiment, flag.
193+
// The loop variable rewriting is:
194+
// IF non-empty hash, then hash determines behavior (function+line match) (*)
195+
// ELSE IF experiment and flag==0, then experiment (set flag=1)
196+
// ELSE flag (note that build sets flag per-package), with behaviors:
197+
// -1 => no change to behavior.
198+
// 0 => no change to behavior (unless non-empty hash, see above)
199+
// 1 => apply change to likely-iteration-variable-escaping loops
200+
// 2 => apply change, log results
201+
// 11 => apply change EVERYWHERE, do not log results (for debugging/benchmarking)
202+
// 12 => apply change EVERYWHERE, log results (for debugging/benchmarking)
203+
//
204+
// The expected uses of the these inputs are, in believed most-likely to least likely:
205+
// GOEXPERIMENT=loopvar -- apply change to entire application
206+
// -gcflags=some_package=-d=loopvar=1 -- apply change to some_package (**)
207+
// -gcflags=some_package=-d=loopvar=2 -- apply change to some_package, log it
208+
// GOEXPERIMENT=loopvar -gcflags=some_package=-d=loopvar=-1 -- apply change to all but one package
209+
// GOCOMPILEDEBUG=loopvarhash=... -- search for failure cause
210+
//
211+
// (*) For debugging purposes, providing loopvar flag >= 11 will expand the hash-eligible set of loops to all.
212+
// (**) Currently this applies to all code in the compilation of some_package, including
213+
// inlines from other packages that may have been compiled w/o the change.
214+
215+
if Debug.LoopVarHash != "" {
216+
// This first little bit controls the inputs for debug-hash-matching.
217+
basenameOnly := false
218+
mostInlineOnly := true
219+
if strings.HasPrefix(Debug.LoopVarHash, "FS") {
220+
// Magic handshake for testing, use file suffixes only when hashing on a position.
221+
// i.e., rather than /tmp/asdfasdfasdf/go-test-whatever/foo_test.go,
222+
// hash only on "foo_test.go", so that it will be the same hash across all runs.
223+
Debug.LoopVarHash = Debug.LoopVarHash[2:]
224+
basenameOnly = true
225+
}
226+
if strings.HasPrefix(Debug.LoopVarHash, "IL") {
227+
// When hash-searching on a position that is an inline site, default is to use the
228+
// most-inlined position only. This makes the hash faster, plus there's no point
229+
// reporting a problem with all the inlining; there's only one copy of the source.
230+
// However, if for some reason you wanted it per-site, you can get this. (The default
231+
// hash-search behavior for compiler debugging is at an inline site.)
232+
Debug.LoopVarHash = Debug.LoopVarHash[2:]
233+
mostInlineOnly = false
234+
}
235+
// end of testing trickiness
236+
LoopVarHash = NewHashDebug("loopvarhash", Debug.LoopVarHash, nil)
237+
if Debug.LoopVar < 11 { // >= 11 means all loops are rewrite-eligible
238+
Debug.LoopVar = 1 // 1 means those loops that syntactically escape their dcl vars are eligible.
239+
}
240+
LoopVarHash.SetInlineSuffixOnly(mostInlineOnly)
241+
LoopVarHash.SetFileSuffixOnly(basenameOnly)
242+
} else if buildcfg.Experiment.LoopVar && Debug.LoopVar == 0 {
243+
Debug.LoopVar = 1
190244
}
191245

192246
if Debug.Fmahash != "" {

src/cmd/compile/internal/base/hashdebug.go

+48-10
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
"fmt"
1313
"io"
1414
"os"
15+
"path/filepath"
1516
"strconv"
1617
"strings"
1718
"sync"
@@ -34,16 +35,38 @@ type HashDebug struct {
3435
name string // base name of the flag/variable.
3536
// what file (if any) receives the yes/no logging?
3637
// default is os.Stdout
37-
logfile writeSyncer
38-
posTmp []src.Pos
39-
bytesTmp bytes.Buffer
40-
matches []hashAndMask // A hash matches if one of these matches.
41-
yes, no bool
38+
logfile writeSyncer
39+
posTmp []src.Pos
40+
bytesTmp bytes.Buffer
41+
matches []hashAndMask // A hash matches if one of these matches.
42+
yes, no bool
43+
fileSuffixOnly bool // for Pos hashes, remove the directory prefix.
44+
inlineSuffixOnly bool // for Pos hashes, remove all but the most inline position.
45+
}
46+
47+
// SetFileSuffixOnly controls whether hashing and reporting use the entire
48+
// file path name, just the basename. This makes hashing more consistent,
49+
// at the expense of being able to certainly locate the file.
50+
func (d *HashDebug) SetFileSuffixOnly(b bool) *HashDebug {
51+
d.fileSuffixOnly = b
52+
return d
53+
}
54+
55+
// SetInlineSuffixOnly controls whether hashing and reporting use the entire
56+
// inline position, or just the most-inline suffix. Compiler debugging tends
57+
// to want the whole inlining, debugging user problems (loopvarhash, e.g.)
58+
// typically does not need to see the entire inline tree, there is just one
59+
// copy of the source code.
60+
func (d *HashDebug) SetInlineSuffixOnly(b bool) *HashDebug {
61+
d.inlineSuffixOnly = b
62+
return d
4263
}
4364

4465
// The default compiler-debugging HashDebug, for "-d=gossahash=..."
4566
var hashDebug *HashDebug
46-
var FmaHash *HashDebug
67+
68+
var FmaHash *HashDebug // for debugging fused-multiply-add floating point changes
69+
var LoopVarHash *HashDebug // for debugging shared/private loop variable changes
4770

4871
// DebugHashMatch reports whether debug variable Gossahash
4972
//
@@ -56,10 +79,10 @@ var FmaHash *HashDebug
5679
// 4. is a suffix of the sha1 hash of pkgAndName (returns true)
5780
//
5881
// 5. OR
59-
// if the value is in the regular language "[01]+(;[01]+)+"
82+
// if the value is in the regular language "[01]+(/[01]+)+"
6083
// test the [01]+ substrings after in order returning true
6184
// for the first one that suffix-matches. The substrings AFTER
62-
// the first semicolon are numbered 0,1, etc and are named
85+
// the first slash are numbered 0,1, etc and are named
6386
// fmt.Sprintf("%s%d", varname, number)
6487
// Clause 5 is not really intended for human use and only
6588
// matters for failures that require multiple triggers.
@@ -235,13 +258,20 @@ func (d *HashDebug) DebugHashMatchParam(pkgAndName string, param uint64) bool {
235258
// package name and path. The output trigger string is prefixed with "POS=" so
236259
// that tools processing the output can reliably tell the difference. The mutex
237260
// locking is also more frequent and more granular.
261+
// Note that the default answer for no environment variable (d == nil)
262+
// is "yes", do the thing.
238263
func (d *HashDebug) DebugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool {
239264
if d == nil {
240265
return true
241266
}
242267
if d.no {
243268
return false
244269
}
270+
// Written this way to make inlining likely.
271+
return d.debugHashMatchPos(ctxt, pos)
272+
}
273+
274+
func (d *HashDebug) debugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool {
245275
d.mu.Lock()
246276
defer d.mu.Unlock()
247277

@@ -278,9 +308,17 @@ func (d *HashDebug) bytesForPos(ctxt *obj.Link, pos src.XPos) []byte {
278308
// Reverse posTmp to put outermost first.
279309
b := &d.bytesTmp
280310
b.Reset()
281-
for i := len(d.posTmp) - 1; i >= 0; i-- {
311+
start := len(d.posTmp) - 1
312+
if d.inlineSuffixOnly {
313+
start = 0
314+
}
315+
for i := start; i >= 0; i-- {
282316
p := &d.posTmp[i]
283-
fmt.Fprintf(b, "%s:%d:%d", p.Filename(), p.Line(), p.Col())
317+
f := p.Filename()
318+
if d.fileSuffixOnly {
319+
f = filepath.Base(f)
320+
}
321+
fmt.Fprintf(b, "%s:%d:%d", f, p.Line(), p.Col())
284322
if i != 0 {
285323
b.WriteByte(';')
286324
}

src/cmd/compile/internal/escape/stmt.go

+1-1
Original file line numberDiff line numberDiff line change
@@ -64,7 +64,7 @@ func (e *escape) stmt(n ir.Node) {
6464
}
6565
e.loopDepth++
6666
default:
67-
base.Fatalf("label missing tag")
67+
base.Fatalf("label %v missing tag", n.Label)
6868
}
6969
delete(e.labels, n.Label)
7070

src/cmd/compile/internal/gc/main.go

+44-1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import (
1616
"cmd/compile/internal/inline"
1717
"cmd/compile/internal/ir"
1818
"cmd/compile/internal/logopt"
19+
"cmd/compile/internal/loopvar"
1920
"cmd/compile/internal/noder"
2021
"cmd/compile/internal/pgo"
2122
"cmd/compile/internal/pkginit"
@@ -265,10 +266,12 @@ func Main(archInit func(*ssagen.ArchInfo)) {
265266
}
266267
noder.MakeWrappers(typecheck.Target) // must happen after inlining
267268

268-
// Devirtualize.
269+
// Devirtualize and get variable capture right in for loops
270+
var transformed []*ir.Name
269271
for _, n := range typecheck.Target.Decls {
270272
if n.Op() == ir.ODCLFUNC {
271273
devirtualize.Func(n.(*ir.Func))
274+
transformed = append(transformed, loopvar.ForCapture(n.(*ir.Func))...)
272275
}
273276
}
274277
ir.CurFunc = nil
@@ -293,6 +296,46 @@ func Main(archInit func(*ssagen.ArchInfo)) {
293296
base.Timer.Start("fe", "escapes")
294297
escape.Funcs(typecheck.Target.Decls)
295298

299+
if 2 <= base.Debug.LoopVar && base.Debug.LoopVar != 11 || logopt.Enabled() { // 11 is do them all, quietly, 12 includes debugging.
300+
fileToPosBase := make(map[string]*src.PosBase) // used to remove inline context for innermost reporting.
301+
for _, n := range transformed {
302+
pos := n.Pos()
303+
if logopt.Enabled() {
304+
// For automated checking of coverage of this transformation, include this in the JSON information.
305+
if n.Esc() == ir.EscHeap {
306+
logopt.LogOpt(pos, "transform-escape", "loopvar", ir.FuncName(n.Curfn))
307+
} else {
308+
logopt.LogOpt(pos, "transform-noescape", "loopvar", ir.FuncName(n.Curfn))
309+
}
310+
}
311+
inner := base.Ctxt.InnermostPos(pos)
312+
outer := base.Ctxt.OutermostPos(pos)
313+
if inner == outer {
314+
if n.Esc() == ir.EscHeap {
315+
base.WarnfAt(pos, "transformed loop variable %v escapes", n)
316+
} else {
317+
base.WarnfAt(pos, "transformed loop variable %v does not escape", n)
318+
}
319+
} else {
320+
// Report the problem at the line where it actually occurred.
321+
afn := inner.AbsFilename()
322+
pb, ok := fileToPosBase[afn]
323+
if !ok {
324+
pb = src.NewFileBase(inner.Filename(), afn)
325+
fileToPosBase[afn] = pb
326+
}
327+
inner.SetBase(pb) // rebasing w/o inline context makes it print correctly in WarnfAt; otherwise it prints as outer.
328+
innerXPos := base.Ctxt.PosTable.XPos(inner)
329+
330+
if n.Esc() == ir.EscHeap {
331+
base.WarnfAt(innerXPos, "transformed loop variable %v escapes (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
332+
} else {
333+
base.WarnfAt(innerXPos, "transformed loop variable %v does not escape (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
334+
}
335+
}
336+
}
337+
}
338+
296339
// Collect information for go:nowritebarrierrec
297340
// checking. This must happen before transforming closures during Walk
298341
// We'll do the final check after write barriers are

src/cmd/compile/internal/ir/stmt.go

+9
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,15 @@ func NewBranchStmt(pos src.XPos, op Op, label *types.Sym) *BranchStmt {
163163
return n
164164
}
165165

166+
func (n *BranchStmt) SetOp(op Op) {
167+
switch op {
168+
default:
169+
panic(n.no("SetOp " + op.String()))
170+
case OBREAK, OCONTINUE, OFALL, OGOTO:
171+
n.op = op
172+
}
173+
}
174+
166175
func (n *BranchStmt) Sym() *types.Sym { return n.Label }
167176

168177
// A CaseClause is a case statement in a switch or select: case List: Body.

0 commit comments

Comments
 (0)