mirror of
https://github.com/golang/go
synced 2024-11-25 07:07:57 -07:00
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 <mdempsky@google.com> Run-TryBot: David Chase <drchase@google.com> TryBot-Result: Gopher Robot <gobot@golang.org>
This commit is contained in:
parent
dbdb3359b5
commit
c20d959163
@ -33,6 +33,8 @@ type DebugFlags struct {
|
||||
InlStaticInit int `help:"allow static initialization of inlined calls" concurrent:"ok"`
|
||||
InterfaceCycles int `help:"allow anonymous interface cycles"`
|
||||
Libfuzzer int `help:"enable coverage instrumentation for libfuzzer"`
|
||||
LoopVar int `help:"shared (0, default), 1 (private loop variables), 2, private + log"`
|
||||
LoopVarHash string `help:"for debugging changes in loop behavior. Overrides experiment and loopvar flag."`
|
||||
LocationLists int `help:"print information about DWARF location list creation"`
|
||||
Nil int `help:"print information about nil checks"`
|
||||
NoOpenDefer int `help:"disable open-coded defers" concurrent:"ok"`
|
||||
|
@ -186,7 +186,61 @@ func ParseFlags() {
|
||||
}
|
||||
|
||||
if Debug.Gossahash != "" {
|
||||
hashDebug = NewHashDebug("gosshash", Debug.Gossahash, nil)
|
||||
hashDebug = NewHashDebug("gossahash", Debug.Gossahash, nil)
|
||||
}
|
||||
|
||||
// Three inputs govern loop iteration variable rewriting, hash, experiment, flag.
|
||||
// The loop variable rewriting is:
|
||||
// IF non-empty hash, then hash determines behavior (function+line match) (*)
|
||||
// ELSE IF experiment and flag==0, then experiment (set flag=1)
|
||||
// ELSE flag (note that build sets flag per-package), with behaviors:
|
||||
// -1 => no change to behavior.
|
||||
// 0 => no change to behavior (unless non-empty hash, see above)
|
||||
// 1 => apply change to likely-iteration-variable-escaping loops
|
||||
// 2 => apply change, log results
|
||||
// 11 => apply change EVERYWHERE, do not log results (for debugging/benchmarking)
|
||||
// 12 => apply change EVERYWHERE, log results (for debugging/benchmarking)
|
||||
//
|
||||
// The expected uses of the these inputs are, in believed most-likely to least likely:
|
||||
// GOEXPERIMENT=loopvar -- apply change to entire application
|
||||
// -gcflags=some_package=-d=loopvar=1 -- apply change to some_package (**)
|
||||
// -gcflags=some_package=-d=loopvar=2 -- apply change to some_package, log it
|
||||
// GOEXPERIMENT=loopvar -gcflags=some_package=-d=loopvar=-1 -- apply change to all but one package
|
||||
// GOCOMPILEDEBUG=loopvarhash=... -- search for failure cause
|
||||
//
|
||||
// (*) For debugging purposes, providing loopvar flag >= 11 will expand the hash-eligible set of loops to all.
|
||||
// (**) Currently this applies to all code in the compilation of some_package, including
|
||||
// inlines from other packages that may have been compiled w/o the change.
|
||||
|
||||
if Debug.LoopVarHash != "" {
|
||||
// This first little bit controls the inputs for debug-hash-matching.
|
||||
basenameOnly := false
|
||||
mostInlineOnly := true
|
||||
if strings.HasPrefix(Debug.LoopVarHash, "FS") {
|
||||
// Magic handshake for testing, use file suffixes only when hashing on a position.
|
||||
// i.e., rather than /tmp/asdfasdfasdf/go-test-whatever/foo_test.go,
|
||||
// hash only on "foo_test.go", so that it will be the same hash across all runs.
|
||||
Debug.LoopVarHash = Debug.LoopVarHash[2:]
|
||||
basenameOnly = true
|
||||
}
|
||||
if strings.HasPrefix(Debug.LoopVarHash, "IL") {
|
||||
// When hash-searching on a position that is an inline site, default is to use the
|
||||
// most-inlined position only. This makes the hash faster, plus there's no point
|
||||
// reporting a problem with all the inlining; there's only one copy of the source.
|
||||
// However, if for some reason you wanted it per-site, you can get this. (The default
|
||||
// hash-search behavior for compiler debugging is at an inline site.)
|
||||
Debug.LoopVarHash = Debug.LoopVarHash[2:]
|
||||
mostInlineOnly = false
|
||||
}
|
||||
// end of testing trickiness
|
||||
LoopVarHash = NewHashDebug("loopvarhash", Debug.LoopVarHash, nil)
|
||||
if Debug.LoopVar < 11 { // >= 11 means all loops are rewrite-eligible
|
||||
Debug.LoopVar = 1 // 1 means those loops that syntactically escape their dcl vars are eligible.
|
||||
}
|
||||
LoopVarHash.SetInlineSuffixOnly(mostInlineOnly)
|
||||
LoopVarHash.SetFileSuffixOnly(basenameOnly)
|
||||
} else if buildcfg.Experiment.LoopVar && Debug.LoopVar == 0 {
|
||||
Debug.LoopVar = 1
|
||||
}
|
||||
|
||||
if Debug.Fmahash != "" {
|
||||
|
@ -12,6 +12,7 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -34,16 +35,38 @@ type HashDebug struct {
|
||||
name string // base name of the flag/variable.
|
||||
// what file (if any) receives the yes/no logging?
|
||||
// default is os.Stdout
|
||||
logfile writeSyncer
|
||||
posTmp []src.Pos
|
||||
bytesTmp bytes.Buffer
|
||||
matches []hashAndMask // A hash matches if one of these matches.
|
||||
yes, no bool
|
||||
logfile writeSyncer
|
||||
posTmp []src.Pos
|
||||
bytesTmp bytes.Buffer
|
||||
matches []hashAndMask // A hash matches if one of these matches.
|
||||
yes, no bool
|
||||
fileSuffixOnly bool // for Pos hashes, remove the directory prefix.
|
||||
inlineSuffixOnly bool // for Pos hashes, remove all but the most inline position.
|
||||
}
|
||||
|
||||
// SetFileSuffixOnly controls whether hashing and reporting use the entire
|
||||
// file path name, just the basename. This makes hashing more consistent,
|
||||
// at the expense of being able to certainly locate the file.
|
||||
func (d *HashDebug) SetFileSuffixOnly(b bool) *HashDebug {
|
||||
d.fileSuffixOnly = b
|
||||
return d
|
||||
}
|
||||
|
||||
// SetInlineSuffixOnly controls whether hashing and reporting use the entire
|
||||
// inline position, or just the most-inline suffix. Compiler debugging tends
|
||||
// to want the whole inlining, debugging user problems (loopvarhash, e.g.)
|
||||
// typically does not need to see the entire inline tree, there is just one
|
||||
// copy of the source code.
|
||||
func (d *HashDebug) SetInlineSuffixOnly(b bool) *HashDebug {
|
||||
d.inlineSuffixOnly = b
|
||||
return d
|
||||
}
|
||||
|
||||
// The default compiler-debugging HashDebug, for "-d=gossahash=..."
|
||||
var hashDebug *HashDebug
|
||||
var FmaHash *HashDebug
|
||||
|
||||
var FmaHash *HashDebug // for debugging fused-multiply-add floating point changes
|
||||
var LoopVarHash *HashDebug // for debugging shared/private loop variable changes
|
||||
|
||||
// DebugHashMatch reports whether debug variable Gossahash
|
||||
//
|
||||
@ -56,10 +79,10 @@ var FmaHash *HashDebug
|
||||
// 4. is a suffix of the sha1 hash of pkgAndName (returns true)
|
||||
//
|
||||
// 5. OR
|
||||
// if the value is in the regular language "[01]+(;[01]+)+"
|
||||
// if the value is in the regular language "[01]+(/[01]+)+"
|
||||
// test the [01]+ substrings after in order returning true
|
||||
// for the first one that suffix-matches. The substrings AFTER
|
||||
// the first semicolon are numbered 0,1, etc and are named
|
||||
// the first slash are numbered 0,1, etc and are named
|
||||
// fmt.Sprintf("%s%d", varname, number)
|
||||
// Clause 5 is not really intended for human use and only
|
||||
// matters for failures that require multiple triggers.
|
||||
@ -235,6 +258,8 @@ func (d *HashDebug) DebugHashMatchParam(pkgAndName string, param uint64) bool {
|
||||
// package name and path. The output trigger string is prefixed with "POS=" so
|
||||
// that tools processing the output can reliably tell the difference. The mutex
|
||||
// locking is also more frequent and more granular.
|
||||
// Note that the default answer for no environment variable (d == nil)
|
||||
// is "yes", do the thing.
|
||||
func (d *HashDebug) DebugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool {
|
||||
if d == nil {
|
||||
return true
|
||||
@ -242,6 +267,11 @@ func (d *HashDebug) DebugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool {
|
||||
if d.no {
|
||||
return false
|
||||
}
|
||||
// Written this way to make inlining likely.
|
||||
return d.debugHashMatchPos(ctxt, pos)
|
||||
}
|
||||
|
||||
func (d *HashDebug) debugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool {
|
||||
d.mu.Lock()
|
||||
defer d.mu.Unlock()
|
||||
|
||||
@ -278,9 +308,17 @@ func (d *HashDebug) bytesForPos(ctxt *obj.Link, pos src.XPos) []byte {
|
||||
// Reverse posTmp to put outermost first.
|
||||
b := &d.bytesTmp
|
||||
b.Reset()
|
||||
for i := len(d.posTmp) - 1; i >= 0; i-- {
|
||||
start := len(d.posTmp) - 1
|
||||
if d.inlineSuffixOnly {
|
||||
start = 0
|
||||
}
|
||||
for i := start; i >= 0; i-- {
|
||||
p := &d.posTmp[i]
|
||||
fmt.Fprintf(b, "%s:%d:%d", p.Filename(), p.Line(), p.Col())
|
||||
f := p.Filename()
|
||||
if d.fileSuffixOnly {
|
||||
f = filepath.Base(f)
|
||||
}
|
||||
fmt.Fprintf(b, "%s:%d:%d", f, p.Line(), p.Col())
|
||||
if i != 0 {
|
||||
b.WriteByte(';')
|
||||
}
|
||||
|
@ -64,7 +64,7 @@ func (e *escape) stmt(n ir.Node) {
|
||||
}
|
||||
e.loopDepth++
|
||||
default:
|
||||
base.Fatalf("label missing tag")
|
||||
base.Fatalf("label %v missing tag", n.Label)
|
||||
}
|
||||
delete(e.labels, n.Label)
|
||||
|
||||
|
@ -16,6 +16,7 @@ import (
|
||||
"cmd/compile/internal/inline"
|
||||
"cmd/compile/internal/ir"
|
||||
"cmd/compile/internal/logopt"
|
||||
"cmd/compile/internal/loopvar"
|
||||
"cmd/compile/internal/noder"
|
||||
"cmd/compile/internal/pgo"
|
||||
"cmd/compile/internal/pkginit"
|
||||
@ -265,10 +266,12 @@ func Main(archInit func(*ssagen.ArchInfo)) {
|
||||
}
|
||||
noder.MakeWrappers(typecheck.Target) // must happen after inlining
|
||||
|
||||
// Devirtualize.
|
||||
// Devirtualize and get variable capture right in for loops
|
||||
var transformed []*ir.Name
|
||||
for _, n := range typecheck.Target.Decls {
|
||||
if n.Op() == ir.ODCLFUNC {
|
||||
devirtualize.Func(n.(*ir.Func))
|
||||
transformed = append(transformed, loopvar.ForCapture(n.(*ir.Func))...)
|
||||
}
|
||||
}
|
||||
ir.CurFunc = nil
|
||||
@ -293,6 +296,46 @@ func Main(archInit func(*ssagen.ArchInfo)) {
|
||||
base.Timer.Start("fe", "escapes")
|
||||
escape.Funcs(typecheck.Target.Decls)
|
||||
|
||||
if 2 <= base.Debug.LoopVar && base.Debug.LoopVar != 11 || logopt.Enabled() { // 11 is do them all, quietly, 12 includes debugging.
|
||||
fileToPosBase := make(map[string]*src.PosBase) // used to remove inline context for innermost reporting.
|
||||
for _, n := range transformed {
|
||||
pos := n.Pos()
|
||||
if logopt.Enabled() {
|
||||
// For automated checking of coverage of this transformation, include this in the JSON information.
|
||||
if n.Esc() == ir.EscHeap {
|
||||
logopt.LogOpt(pos, "transform-escape", "loopvar", ir.FuncName(n.Curfn))
|
||||
} else {
|
||||
logopt.LogOpt(pos, "transform-noescape", "loopvar", ir.FuncName(n.Curfn))
|
||||
}
|
||||
}
|
||||
inner := base.Ctxt.InnermostPos(pos)
|
||||
outer := base.Ctxt.OutermostPos(pos)
|
||||
if inner == outer {
|
||||
if n.Esc() == ir.EscHeap {
|
||||
base.WarnfAt(pos, "transformed loop variable %v escapes", n)
|
||||
} else {
|
||||
base.WarnfAt(pos, "transformed loop variable %v does not escape", n)
|
||||
}
|
||||
} else {
|
||||
// Report the problem at the line where it actually occurred.
|
||||
afn := inner.AbsFilename()
|
||||
pb, ok := fileToPosBase[afn]
|
||||
if !ok {
|
||||
pb = src.NewFileBase(inner.Filename(), afn)
|
||||
fileToPosBase[afn] = pb
|
||||
}
|
||||
inner.SetBase(pb) // rebasing w/o inline context makes it print correctly in WarnfAt; otherwise it prints as outer.
|
||||
innerXPos := base.Ctxt.PosTable.XPos(inner)
|
||||
|
||||
if n.Esc() == ir.EscHeap {
|
||||
base.WarnfAt(innerXPos, "transformed loop variable %v escapes (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
|
||||
} else {
|
||||
base.WarnfAt(innerXPos, "transformed loop variable %v does not escape (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Collect information for go:nowritebarrierrec
|
||||
// checking. This must happen before transforming closures during Walk
|
||||
// We'll do the final check after write barriers are
|
||||
|
@ -163,6 +163,15 @@ func NewBranchStmt(pos src.XPos, op Op, label *types.Sym) *BranchStmt {
|
||||
return n
|
||||
}
|
||||
|
||||
func (n *BranchStmt) SetOp(op Op) {
|
||||
switch op {
|
||||
default:
|
||||
panic(n.no("SetOp " + op.String()))
|
||||
case OBREAK, OCONTINUE, OFALL, OGOTO:
|
||||
n.op = op
|
||||
}
|
||||
}
|
||||
|
||||
func (n *BranchStmt) Sym() *types.Sym { return n.Label }
|
||||
|
||||
// A CaseClause is a case statement in a switch or select: case List: Body.
|
||||
|
449
src/cmd/compile/internal/loopvar/loopvar.go
Normal file
449
src/cmd/compile/internal/loopvar/loopvar.go
Normal file
@ -0,0 +1,449 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package loopvar applies the proper variable capture, according
|
||||
// to experiment, flags, language version, etc.
|
||||
package loopvar
|
||||
|
||||
import (
|
||||
"cmd/compile/internal/base"
|
||||
"cmd/compile/internal/ir"
|
||||
"cmd/compile/internal/typecheck"
|
||||
"cmd/compile/internal/types"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ForCapture transforms for and range loops that declare variables that might be
|
||||
// captured by a closure or escaped to the heap, using a syntactic check that
|
||||
// conservatively overestimates the loops where capture occurs, but still avoids
|
||||
// transforming the (large) majority of loops. It returns the list of names
|
||||
// subject to this change, that may (once transformed) be heap allocated in the
|
||||
// process. (This allows checking after escape analysis to call out any such
|
||||
// variables, in case it causes allocation/performance problems).
|
||||
|
||||
// For this code, the meaningful debug and hash flag settings
|
||||
//
|
||||
// base.Debug.LoopVar <= 0 => do not transform
|
||||
//
|
||||
// base.LoopVarHash != nil => use hash setting to govern transformation.
|
||||
// note that LoopVarHash != nil sets base.Debug.LoopVar to 1 (unless it is >= 11, for testing/debugging).
|
||||
//
|
||||
// base.Debug.LoopVar == 11 => transform ALL loops ignoring syntactic/potential escape. Do not log, can be in addition to GOEXPERIMENT.
|
||||
//
|
||||
// The effect of GOEXPERIMENT=loopvar is to change the default value (0) of base.Debug.LoopVar to 1 for all packages.
|
||||
|
||||
func ForCapture(fn *ir.Func) []*ir.Name {
|
||||
if base.Debug.LoopVar <= 0 { // code in base:flags.go ensures >= 1 if loopvarhash != ""
|
||||
// TODO remove this when the transformation is made sensitive to inlining; this is least-risk for 1.21
|
||||
return nil
|
||||
}
|
||||
|
||||
// if a loop variable is transformed it is appended to this slice for later logging
|
||||
var transformed []*ir.Name
|
||||
|
||||
forCapture := func() {
|
||||
seq := 1
|
||||
|
||||
dclFixups := make(map[*ir.Name]ir.Stmt)
|
||||
|
||||
// possibly leaked includes names of declared loop variables that may be leaked;
|
||||
// the mapped value is true if the name is *syntactically* leaked, and those loops
|
||||
// will be transformed.
|
||||
possiblyLeaked := make(map[*ir.Name]bool)
|
||||
|
||||
// noteMayLeak is called for candidate variables in for range/3-clause, and
|
||||
// adds them (mapped to false) to possiblyLeaked.
|
||||
noteMayLeak := func(x ir.Node) {
|
||||
if n, ok := x.(*ir.Name); ok {
|
||||
if n.Type().Kind() == types.TBLANK {
|
||||
return
|
||||
}
|
||||
// default is false (leak candidate, not yet known to leak), but flag can make all variables "leak"
|
||||
possiblyLeaked[n] = base.Debug.LoopVar >= 11
|
||||
}
|
||||
}
|
||||
|
||||
// maybeReplaceVar unshares an iteration variable for a range loop,
|
||||
// if that variable was actually (syntactically) leaked,
|
||||
// subject to hash-variable debugging.
|
||||
maybeReplaceVar := func(k ir.Node, x *ir.RangeStmt) ir.Node {
|
||||
if n, ok := k.(*ir.Name); ok && possiblyLeaked[n] {
|
||||
if base.LoopVarHash.DebugHashMatchPos(base.Ctxt, n.Pos()) {
|
||||
// Rename the loop key, prefix body with assignment from loop key
|
||||
transformed = append(transformed, n)
|
||||
tk := typecheck.Temp(n.Type())
|
||||
tk.SetTypecheck(1)
|
||||
as := ir.NewAssignStmt(x.Pos(), n, tk)
|
||||
as.Def = true
|
||||
as.SetTypecheck(1)
|
||||
x.Body.Prepend(as)
|
||||
dclFixups[n] = as
|
||||
return tk
|
||||
}
|
||||
}
|
||||
return k
|
||||
}
|
||||
|
||||
// scanChildrenThenTransform processes node x to:
|
||||
// 1. if x is a for/range, note declared iteration variables possiblyLeaked (PL)
|
||||
// 2. search all of x's children for syntactically escaping references to v in PL,
|
||||
// meaning either address-of-v or v-captured-by-a-closure
|
||||
// 3. for all v in PL that had a syntactically escaping reference, transform the declaration
|
||||
// and (in case of 3-clause loop) the loop to the unshared loop semantics.
|
||||
// This is all much simpler for range loops; 3-clause loops can have an arbitrary number
|
||||
// of iteration variables and the transformation is more involved, range loops have at most 2.
|
||||
var scanChildrenThenTransform func(x ir.Node) bool
|
||||
scanChildrenThenTransform = func(n ir.Node) bool {
|
||||
switch x := n.(type) {
|
||||
case *ir.ClosureExpr:
|
||||
for _, cv := range x.Func.ClosureVars {
|
||||
v := cv.Canonical()
|
||||
if _, ok := possiblyLeaked[v]; ok {
|
||||
possiblyLeaked[v] = true
|
||||
}
|
||||
}
|
||||
|
||||
case *ir.AddrExpr:
|
||||
// Explicitly note address-taken so that return-statements can be excluded
|
||||
y := ir.OuterValue(x.X)
|
||||
if y.Op() != ir.ONAME {
|
||||
break
|
||||
}
|
||||
z, ok := y.(*ir.Name)
|
||||
if !ok {
|
||||
break
|
||||
}
|
||||
switch z.Class {
|
||||
case ir.PAUTO, ir.PPARAM, ir.PPARAMOUT, ir.PAUTOHEAP:
|
||||
if _, ok := possiblyLeaked[z]; ok {
|
||||
possiblyLeaked[z] = true
|
||||
}
|
||||
}
|
||||
|
||||
case *ir.RangeStmt:
|
||||
if !x.Def {
|
||||
break
|
||||
}
|
||||
noteMayLeak(x.Key)
|
||||
noteMayLeak(x.Value)
|
||||
ir.DoChildren(n, scanChildrenThenTransform)
|
||||
x.Key = maybeReplaceVar(x.Key, x)
|
||||
x.Value = maybeReplaceVar(x.Value, x)
|
||||
return false
|
||||
|
||||
case *ir.ForStmt:
|
||||
forAllDefInInit(x, noteMayLeak)
|
||||
ir.DoChildren(n, scanChildrenThenTransform)
|
||||
var leaked []*ir.Name
|
||||
// Collect the leaking variables for the much-more-complex transformation.
|
||||
forAllDefInInit(x, func(z ir.Node) {
|
||||
if n, ok := z.(*ir.Name); ok && possiblyLeaked[n] {
|
||||
// Hash on n.Pos() for most precise failure location.
|
||||
if base.LoopVarHash.DebugHashMatchPos(base.Ctxt, n.Pos()) {
|
||||
leaked = append(leaked, n)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
if len(leaked) > 0 {
|
||||
// need to transform the for loop just so.
|
||||
|
||||
/* Contrived example, w/ numbered comments from the transformation:
|
||||
BEFORE:
|
||||
var escape []*int
|
||||
for z := 0; z < n; z++ {
|
||||
if reason() {
|
||||
escape = append(escape, &z)
|
||||
continue
|
||||
}
|
||||
z = z + z
|
||||
stuff
|
||||
}
|
||||
AFTER:
|
||||
for z', tmp_first := 0, true; ; { // (4)
|
||||
// (5) body' follows:
|
||||
z := z' // (1)
|
||||
if tmp_first {tmp_first = false} else {z++} // (6)
|
||||
if ! (z < n) { break } // (7)
|
||||
// (3, 8) body_continue
|
||||
if reason() {
|
||||
escape = append(escape, &z)
|
||||
goto next // rewritten continue
|
||||
}
|
||||
z = z + z
|
||||
stuff
|
||||
next: // (9)
|
||||
z' = z // (2)
|
||||
}
|
||||
|
||||
In the case that the loop contains no increment (z++),
|
||||
there is no need for step 6,
|
||||
and thus no need to test, update, or declare tmp_first (part of step 4).
|
||||
Similarly if the loop contains no exit test (z < n),
|
||||
then there is no need for step 7.
|
||||
*/
|
||||
|
||||
// Expressed in terms of the input ForStmt
|
||||
//
|
||||
// type ForStmt struct {
|
||||
// init Nodes
|
||||
// Label *types.Sym
|
||||
// Cond Node // empty if OFORUNTIL
|
||||
// Post Node
|
||||
// Body Nodes
|
||||
// HasBreak bool
|
||||
// }
|
||||
|
||||
// OFOR: init; loop: if !Cond {break}; Body; Post; goto loop
|
||||
|
||||
// (1) prebody = {z := z' for z in leaked}
|
||||
// (2) postbody = {z' = z for z in leaked}
|
||||
// (3) body_continue = {body : s/continue/goto next}
|
||||
// (4) init' = (init : s/z/z' for z in leaked) + tmp_first := true
|
||||
// (5) body' = prebody + // appears out of order below
|
||||
// (6) if tmp_first {tmp_first = false} else {Post} +
|
||||
// (7) if !cond {break} +
|
||||
// (8) body_continue (3) +
|
||||
// (9) next: postbody (2)
|
||||
// (10) cond' = {}
|
||||
// (11) post' = {}
|
||||
|
||||
// minor optimizations:
|
||||
// if Post is empty, tmp_first and step 6 can be skipped.
|
||||
// if Cond is empty, that code can also be skipped.
|
||||
|
||||
var preBody, postBody ir.Nodes
|
||||
|
||||
// Given original iteration variable z, what is the corresponding z'
|
||||
// that carries the value from iteration to iteration?
|
||||
zPrimeForZ := make(map[*ir.Name]*ir.Name)
|
||||
|
||||
// (1,2) initialize preBody and postBody
|
||||
for _, z := range leaked {
|
||||
transformed = append(transformed, z)
|
||||
|
||||
tz := typecheck.Temp(z.Type())
|
||||
tz.SetTypecheck(1)
|
||||
zPrimeForZ[z] = tz
|
||||
|
||||
as := ir.NewAssignStmt(x.Pos(), z, tz)
|
||||
as.Def = true
|
||||
as.SetTypecheck(1)
|
||||
preBody.Append(as)
|
||||
dclFixups[z] = as
|
||||
|
||||
as = ir.NewAssignStmt(x.Pos(), tz, z)
|
||||
as.SetTypecheck(1)
|
||||
postBody.Append(as)
|
||||
|
||||
}
|
||||
|
||||
// (3) rewrite continues in body -- rewrite is inplace, so works for top level visit, too.
|
||||
label := typecheck.Lookup(fmt.Sprintf(".3clNext_%d", seq))
|
||||
seq++
|
||||
labelStmt := ir.NewLabelStmt(x.Pos(), label)
|
||||
labelStmt.SetTypecheck(1)
|
||||
|
||||
loopLabel := x.Label
|
||||
loopDepth := 0
|
||||
var editContinues func(x ir.Node) bool
|
||||
editContinues = func(x ir.Node) bool {
|
||||
|
||||
switch c := x.(type) {
|
||||
case *ir.BranchStmt:
|
||||
// If this is a continue targeting the loop currently being rewritten, transform it to an appropriate GOTO
|
||||
if c.Op() == ir.OCONTINUE && (loopDepth == 0 && c.Label == nil || loopLabel != nil && c.Label == loopLabel) {
|
||||
c.Label = label
|
||||
c.SetOp(ir.OGOTO)
|
||||
}
|
||||
case *ir.RangeStmt, *ir.ForStmt:
|
||||
loopDepth++
|
||||
ir.DoChildren(x, editContinues)
|
||||
loopDepth--
|
||||
return false
|
||||
}
|
||||
ir.DoChildren(x, editContinues)
|
||||
return false
|
||||
}
|
||||
for _, y := range x.Body {
|
||||
editContinues(y)
|
||||
}
|
||||
bodyContinue := x.Body
|
||||
|
||||
// (4) rewrite init
|
||||
forAllDefInInitUpdate(x, func(z ir.Node, pz *ir.Node) {
|
||||
// note tempFor[n] can be nil if hash searching.
|
||||
if n, ok := z.(*ir.Name); ok && possiblyLeaked[n] && zPrimeForZ[n] != nil {
|
||||
*pz = zPrimeForZ[n]
|
||||
}
|
||||
})
|
||||
|
||||
postNotNil := x.Post != nil
|
||||
var tmpFirstDcl *ir.AssignStmt
|
||||
if postNotNil {
|
||||
// body' = prebody +
|
||||
// (6) if tmp_first {tmp_first = false} else {Post} +
|
||||
// if !cond {break} + ...
|
||||
tmpFirst := typecheck.Temp(types.Types[types.TBOOL])
|
||||
|
||||
// tmpFirstAssign assigns val to tmpFirst
|
||||
tmpFirstAssign := func(val bool) *ir.AssignStmt {
|
||||
s := ir.NewAssignStmt(x.Pos(), tmpFirst, typecheck.OrigBool(tmpFirst, val))
|
||||
s.SetTypecheck(1)
|
||||
return s
|
||||
}
|
||||
|
||||
tmpFirstDcl = tmpFirstAssign(true)
|
||||
tmpFirstDcl.Def = true // also declares tmpFirst
|
||||
tmpFirstSetFalse := tmpFirstAssign(false)
|
||||
ifTmpFirst := ir.NewIfStmt(x.Pos(), tmpFirst, ir.Nodes{tmpFirstSetFalse}, ir.Nodes{x.Post})
|
||||
ifTmpFirst.SetTypecheck(1)
|
||||
preBody.Append(ifTmpFirst)
|
||||
}
|
||||
|
||||
// body' = prebody +
|
||||
// if tmp_first {tmp_first = false} else {Post} +
|
||||
// (7) if !cond {break} + ...
|
||||
if x.Cond != nil {
|
||||
notCond := ir.NewUnaryExpr(x.Cond.Pos(), ir.ONOT, x.Cond)
|
||||
notCond.SetType(x.Cond.Type())
|
||||
notCond.SetTypecheck(1)
|
||||
newBreak := ir.NewBranchStmt(x.Pos(), ir.OBREAK, nil)
|
||||
newBreak.SetTypecheck(1)
|
||||
ifNotCond := ir.NewIfStmt(x.Pos(), notCond, ir.Nodes{newBreak}, nil)
|
||||
ifNotCond.SetTypecheck(1)
|
||||
preBody.Append(ifNotCond)
|
||||
}
|
||||
|
||||
if postNotNil {
|
||||
x.PtrInit().Append(tmpFirstDcl)
|
||||
}
|
||||
|
||||
// (8)
|
||||
preBody.Append(bodyContinue...)
|
||||
// (9)
|
||||
preBody.Append(labelStmt)
|
||||
preBody.Append(postBody...)
|
||||
|
||||
// (5) body' = prebody + ...
|
||||
x.Body = preBody
|
||||
|
||||
// (10) cond' = {}
|
||||
x.Cond = nil
|
||||
|
||||
// (11) post' = {}
|
||||
x.Post = nil
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
ir.DoChildren(n, scanChildrenThenTransform)
|
||||
|
||||
return false
|
||||
}
|
||||
scanChildrenThenTransform(fn)
|
||||
if len(transformed) > 0 {
|
||||
// editNodes scans a slice C of ir.Node, looking for declarations that
|
||||
// appear in dclFixups. Any declaration D whose "fixup" is an assignmnt
|
||||
// statement A is removed from the C and relocated to the Init
|
||||
// of A. editNodes returns the modified slice of ir.Node.
|
||||
editNodes := func(c ir.Nodes) ir.Nodes {
|
||||
j := 0
|
||||
for _, n := range c {
|
||||
if d, ok := n.(*ir.Decl); ok {
|
||||
if s := dclFixups[d.X]; s != nil {
|
||||
switch a := s.(type) {
|
||||
case *ir.AssignStmt:
|
||||
a.PtrInit().Prepend(d)
|
||||
delete(dclFixups, d.X) // can't be sure of visit order, wouldn't want to visit twice.
|
||||
default:
|
||||
base.Fatalf("not implemented yet for node type %v", s.Op())
|
||||
}
|
||||
continue // do not copy this node, and do not increment j
|
||||
}
|
||||
}
|
||||
c[j] = n
|
||||
j++
|
||||
}
|
||||
for k := j; k < len(c); k++ {
|
||||
c[k] = nil
|
||||
}
|
||||
return c[:j]
|
||||
}
|
||||
// fixup all tagged declarations in all the statements lists in fn.
|
||||
rewriteNodes(fn, editNodes)
|
||||
}
|
||||
}
|
||||
ir.WithFunc(fn, forCapture)
|
||||
return transformed
|
||||
}
|
||||
|
||||
// forAllDefInInitUpdate applies "do" to all the defining assignemnts in the Init clause of a ForStmt.
|
||||
// This abstracts away some of the boilerplate from the already complex and verbose for-3-clause case.
|
||||
func forAllDefInInitUpdate(x *ir.ForStmt, do func(z ir.Node, update *ir.Node)) {
|
||||
for _, s := range x.Init() {
|
||||
switch y := s.(type) {
|
||||
case *ir.AssignListStmt:
|
||||
if !y.Def {
|
||||
continue
|
||||
}
|
||||
for i, z := range y.Lhs {
|
||||
do(z, &y.Lhs[i])
|
||||
}
|
||||
case *ir.AssignStmt:
|
||||
if !y.Def {
|
||||
continue
|
||||
}
|
||||
do(y.X, &y.X)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// forAllDefInInit is forAllDefInInitUpdate without the update option.
|
||||
func forAllDefInInit(x *ir.ForStmt, do func(z ir.Node)) {
|
||||
forAllDefInInitUpdate(x, func(z ir.Node, _ *ir.Node) { do(z) })
|
||||
}
|
||||
|
||||
// rewriteNodes applies editNodes to all statement lists in fn.
|
||||
func rewriteNodes(fn *ir.Func, editNodes func(c ir.Nodes) ir.Nodes) {
|
||||
var forNodes func(x ir.Node) bool
|
||||
forNodes = func(n ir.Node) bool {
|
||||
if stmt, ok := n.(ir.InitNode); ok {
|
||||
// process init list
|
||||
stmt.SetInit(editNodes(stmt.Init()))
|
||||
}
|
||||
switch x := n.(type) {
|
||||
case *ir.Func:
|
||||
x.Body = editNodes(x.Body)
|
||||
x.Enter = editNodes(x.Enter)
|
||||
x.Exit = editNodes(x.Exit)
|
||||
case *ir.InlinedCallExpr:
|
||||
x.Body = editNodes(x.Body)
|
||||
|
||||
case *ir.CaseClause:
|
||||
x.Body = editNodes(x.Body)
|
||||
case *ir.CommClause:
|
||||
x.Body = editNodes(x.Body)
|
||||
|
||||
case *ir.BlockStmt:
|
||||
x.List = editNodes(x.List)
|
||||
|
||||
case *ir.ForStmt:
|
||||
x.Body = editNodes(x.Body)
|
||||
case *ir.RangeStmt:
|
||||
x.Body = editNodes(x.Body)
|
||||
case *ir.IfStmt:
|
||||
x.Body = editNodes(x.Body)
|
||||
x.Else = editNodes(x.Else)
|
||||
case *ir.SelectStmt:
|
||||
x.Compiled = editNodes(x.Compiled)
|
||||
case *ir.SwitchStmt:
|
||||
x.Compiled = editNodes(x.Compiled)
|
||||
}
|
||||
ir.DoChildren(n, forNodes)
|
||||
return false
|
||||
}
|
||||
forNodes(fn)
|
||||
}
|
152
src/cmd/compile/internal/loopvar/loopvar_test.go
Normal file
152
src/cmd/compile/internal/loopvar/loopvar_test.go
Normal file
@ -0,0 +1,152 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package loopvar_test
|
||||
|
||||
import (
|
||||
"internal/testenv"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testcase struct {
|
||||
lvFlag string // ==-2, -1, 0, 1, 2
|
||||
buildExpect string // message, if any
|
||||
expectRC int
|
||||
files []string
|
||||
}
|
||||
|
||||
var for_files = []string{
|
||||
"for_esc_address.go", // address of variable
|
||||
"for_esc_closure.go", // closure of variable
|
||||
"for_esc_minimal_closure.go", // simple closure of variable
|
||||
"for_esc_method.go", // method value of variable
|
||||
"for_complicated_esc_address.go", // modifies loop index in body
|
||||
}
|
||||
|
||||
var range_files = []string{
|
||||
"range_esc_address.go", // address of variable
|
||||
"range_esc_closure.go", // closure of variable
|
||||
"range_esc_minimal_closure.go", // simple closure of variable
|
||||
"range_esc_method.go", // method value of variable
|
||||
}
|
||||
|
||||
var cases = []testcase{
|
||||
{"-1", "", 11, for_files[:1]},
|
||||
{"0", "", 0, for_files[:1]},
|
||||
{"1", "", 0, for_files[:1]},
|
||||
{"2", "transformed loop variable i ", 0, for_files},
|
||||
|
||||
{"-1", "", 11, range_files[:1]},
|
||||
{"0", "", 0, range_files[:1]},
|
||||
{"1", "", 0, range_files[:1]},
|
||||
{"2", "transformed loop variable i ", 0, range_files},
|
||||
|
||||
{"1", "", 0, []string{"for_nested.go"}},
|
||||
}
|
||||
|
||||
// TestLoopVar checks that the GOEXPERIMENT and debug flags behave as expected.
|
||||
func TestLoopVar(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
default:
|
||||
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
|
||||
}
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64":
|
||||
default:
|
||||
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
|
||||
}
|
||||
|
||||
testenv.MustHaveGoBuild(t)
|
||||
gocmd := testenv.GoToolPath(t)
|
||||
tmpdir := t.TempDir()
|
||||
output := filepath.Join(tmpdir, "foo.exe")
|
||||
|
||||
for i, tc := range cases {
|
||||
for _, f := range tc.files {
|
||||
source := f
|
||||
cmd := testenv.Command(t, gocmd, "build", "-o", output, "-gcflags=-d=loopvar="+tc.lvFlag, source)
|
||||
cmd.Env = append(cmd.Env, "GOEXPERIMENT=loopvar", "HOME="+tmpdir)
|
||||
cmd.Dir = "testdata"
|
||||
t.Logf("File %s loopvar=%s expect '%s' exit code %d", f, tc.lvFlag, tc.buildExpect, tc.expectRC)
|
||||
b, e := cmd.CombinedOutput()
|
||||
if e != nil {
|
||||
t.Error(e)
|
||||
}
|
||||
if tc.buildExpect != "" {
|
||||
s := string(b)
|
||||
if !strings.Contains(s, tc.buildExpect) {
|
||||
t.Errorf("File %s test %d expected to match '%s' with \n-----\n%s\n-----", f, i, tc.buildExpect, s)
|
||||
}
|
||||
}
|
||||
// run what we just built.
|
||||
cmd = testenv.Command(t, output)
|
||||
b, e = cmd.CombinedOutput()
|
||||
if tc.expectRC != 0 {
|
||||
if e == nil {
|
||||
t.Errorf("Missing expected error, file %s, case %d", f, i)
|
||||
} else if ee, ok := (e).(*exec.ExitError); !ok || ee.ExitCode() != tc.expectRC {
|
||||
t.Error(e)
|
||||
} else {
|
||||
// okay
|
||||
}
|
||||
} else if e != nil {
|
||||
t.Error(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoopVarHashes(t *testing.T) {
|
||||
switch runtime.GOOS {
|
||||
case "linux", "darwin":
|
||||
default:
|
||||
t.Skipf("Slow test, usually avoid it, os=%s not linux or darwin", runtime.GOOS)
|
||||
}
|
||||
switch runtime.GOARCH {
|
||||
case "amd64", "arm64":
|
||||
default:
|
||||
t.Skipf("Slow test, usually avoid it, arch=%s not amd64 or arm64", runtime.GOARCH)
|
||||
}
|
||||
|
||||
testenv.MustHaveGoBuild(t)
|
||||
gocmd := testenv.GoToolPath(t)
|
||||
tmpdir := t.TempDir()
|
||||
|
||||
root := "cmd/compile/internal/loopvar/testdata/inlines"
|
||||
|
||||
f := func(hash string) string {
|
||||
// This disables the loopvar change, except for the specified package.
|
||||
// The effect should follow the package, even though everything (except "c")
|
||||
// is inlined.
|
||||
cmd := testenv.Command(t, gocmd, "run", root)
|
||||
cmd.Env = append(cmd.Env, "GOCOMPILEDEBUG=loopvarhash=FS"+hash, "HOME="+tmpdir)
|
||||
cmd.Dir = filepath.Join("testdata", "inlines")
|
||||
|
||||
b, _ := cmd.CombinedOutput()
|
||||
// Ignore the error, sometimes it's supposed to fail, the output test will catch it.
|
||||
return string(b)
|
||||
}
|
||||
|
||||
m := f("000100000010011111101100")
|
||||
t.Logf(m)
|
||||
|
||||
mCount := strings.Count(m, "loopvarhash triggered POS=main.go:27:6")
|
||||
otherCount := strings.Count(m, "loopvarhash")
|
||||
if mCount < 1 {
|
||||
t.Errorf("Did not see expected value of m compile")
|
||||
}
|
||||
if mCount != otherCount {
|
||||
t.Errorf("Saw extraneous hash matches")
|
||||
}
|
||||
// This next test carefully dodges a bug-to-be-fixed with inlined locations for ir.Names.
|
||||
if !strings.Contains(m, ", 100, 100, 100, 100") {
|
||||
t.Errorf("Did not see expected value of m run")
|
||||
}
|
||||
|
||||
}
|
115
src/cmd/compile/internal/loopvar/testdata/for_complicated_esc_address.go
vendored
Normal file
115
src/cmd/compile/internal/loopvar/testdata/for_complicated_esc_address.go
vendored
Normal file
@ -0,0 +1,115 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
ss, sa := shared(23)
|
||||
ps, pa := private(23)
|
||||
es, ea := experiment(23)
|
||||
|
||||
fmt.Printf("shared s, a; private, s, a; experiment s, a = %d, %d; %d, %d; %d, %d\n", ss, sa, ps, pa, es, ea)
|
||||
|
||||
if ss != ps || ss != es || ea != pa || sa == pa {
|
||||
os.Exit(11)
|
||||
} else {
|
||||
fmt.Println("PASS")
|
||||
}
|
||||
}
|
||||
|
||||
func experiment(x int) (int, int) {
|
||||
sum := 0
|
||||
var is []*int
|
||||
for i := x; i != 1; i = i / 2 {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
i = i*3 + 1
|
||||
if i&1 == 0 {
|
||||
is = append(is, &i)
|
||||
for i&2 == 0 {
|
||||
i = i >> 1
|
||||
}
|
||||
} else {
|
||||
i = i + i
|
||||
}
|
||||
}
|
||||
|
||||
asum := 0
|
||||
for _, pi := range is {
|
||||
asum += *pi
|
||||
}
|
||||
|
||||
return sum, asum
|
||||
}
|
||||
|
||||
func private(x int) (int, int) {
|
||||
sum := 0
|
||||
var is []*int
|
||||
I := x
|
||||
for ; I != 1; I = I / 2 {
|
||||
i := I
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
I = i
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
i = i*3 + 1
|
||||
if i&1 == 0 {
|
||||
is = append(is, &i)
|
||||
for i&2 == 0 {
|
||||
i = i >> 1
|
||||
}
|
||||
} else {
|
||||
i = i + i
|
||||
}
|
||||
I = i
|
||||
}
|
||||
|
||||
asum := 0
|
||||
for _, pi := range is {
|
||||
asum += *pi
|
||||
}
|
||||
|
||||
return sum, asum
|
||||
}
|
||||
|
||||
func shared(x int) (int, int) {
|
||||
sum := 0
|
||||
var is []*int
|
||||
i := x
|
||||
for ; i != 1; i = i / 2 {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
i = i*3 + 1
|
||||
if i&1 == 0 {
|
||||
is = append(is, &i)
|
||||
for i&2 == 0 {
|
||||
i = i >> 1
|
||||
}
|
||||
} else {
|
||||
i = i + i
|
||||
}
|
||||
}
|
||||
|
||||
asum := 0
|
||||
for _, pi := range is {
|
||||
asum += *pi
|
||||
}
|
||||
return sum, asum
|
||||
}
|
45
src/cmd/compile/internal/loopvar/testdata/for_esc_address.go
vendored
Normal file
45
src/cmd/compile/internal/loopvar/testdata/for_esc_address.go
vendored
Normal file
@ -0,0 +1,45 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
var is []*int
|
||||
for i := 0; i < 10; i++ {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, &i)
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, pi := range is {
|
||||
sum += *pi
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
51
src/cmd/compile/internal/loopvar/testdata/for_esc_closure.go
vendored
Normal file
51
src/cmd/compile/internal/loopvar/testdata/for_esc_closure.go
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var is []func() int
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
for i := 0; i < 10; i++ {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, func() int {
|
||||
if i%17 == 15 {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, f := range is {
|
||||
sum += f()
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
51
src/cmd/compile/internal/loopvar/testdata/for_esc_method.go
vendored
Normal file
51
src/cmd/compile/internal/loopvar/testdata/for_esc_method.go
vendored
Normal file
@ -0,0 +1,51 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type I int
|
||||
|
||||
func (x *I) method() int {
|
||||
return int(*x)
|
||||
}
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
var is []func() int
|
||||
for i := I(0); int(i) < 10; i++ {
|
||||
for j := 0; j < 10; j++ {
|
||||
if int(i) == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, i.method)
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, m := range is {
|
||||
sum += m()
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
48
src/cmd/compile/internal/loopvar/testdata/for_esc_minimal_closure.go
vendored
Normal file
48
src/cmd/compile/internal/loopvar/testdata/for_esc_minimal_closure.go
vendored
Normal file
@ -0,0 +1,48 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var is []func() int
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
for i := 0; i < 10; i++ {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, func() int {
|
||||
return i
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, f := range is {
|
||||
sum += f()
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
47
src/cmd/compile/internal/loopvar/testdata/for_nested.go
vendored
Normal file
47
src/cmd/compile/internal/loopvar/testdata/for_nested.go
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func main() {
|
||||
x := f(60)
|
||||
fmt.Println(x)
|
||||
if x != 54 {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
||||
|
||||
var escape *int
|
||||
|
||||
func f(i int) int {
|
||||
a := 0
|
||||
outer:
|
||||
for {
|
||||
switch {
|
||||
case i > 55:
|
||||
i--
|
||||
continue
|
||||
case i == 55:
|
||||
for j := i; j != 1; j = j / 2 {
|
||||
a++
|
||||
if j == 4 {
|
||||
escape = &j
|
||||
i--
|
||||
continue outer
|
||||
}
|
||||
if j&1 == 1 {
|
||||
j = 2 * (3*j + 1)
|
||||
}
|
||||
}
|
||||
return a
|
||||
case i < 55:
|
||||
return i
|
||||
}
|
||||
}
|
||||
}
|
20
src/cmd/compile/internal/loopvar/testdata/inlines/a/a.go
vendored
Normal file
20
src/cmd/compile/internal/loopvar/testdata/inlines/a/a.go
vendored
Normal file
@ -0,0 +1,20 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package a
|
||||
|
||||
import "cmd/compile/internal/loopvar/testdata/inlines/b"
|
||||
|
||||
func F() []*int {
|
||||
var s []*int
|
||||
for i := 0; i < 10; i++ {
|
||||
s = append(s, &i)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func Fb() []*int {
|
||||
bf, _ := b.F()
|
||||
return bf
|
||||
}
|
21
src/cmd/compile/internal/loopvar/testdata/inlines/b/b.go
vendored
Normal file
21
src/cmd/compile/internal/loopvar/testdata/inlines/b/b.go
vendored
Normal file
@ -0,0 +1,21 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package b
|
||||
|
||||
var slice = []int{1, 2, 4, 8, 16, 32, 64, 128, 256, 512, 1024}
|
||||
|
||||
func F() ([]*int, []*int) {
|
||||
return g()
|
||||
}
|
||||
|
||||
func g() ([]*int, []*int) {
|
||||
var s []*int
|
||||
var t []*int
|
||||
for i, j := range slice {
|
||||
s = append(s, &i)
|
||||
t = append(t, &j)
|
||||
}
|
||||
return s[:len(s)-1], t
|
||||
}
|
14
src/cmd/compile/internal/loopvar/testdata/inlines/c/c.go
vendored
Normal file
14
src/cmd/compile/internal/loopvar/testdata/inlines/c/c.go
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package c
|
||||
|
||||
//go:noinline
|
||||
func F() []*int {
|
||||
var s []*int
|
||||
for i := 0; i < 10; i++ {
|
||||
s = append(s, &i)
|
||||
}
|
||||
return s
|
||||
}
|
53
src/cmd/compile/internal/loopvar/testdata/inlines/main.go
vendored
Normal file
53
src/cmd/compile/internal/loopvar/testdata/inlines/main.go
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"cmd/compile/internal/loopvar/testdata/inlines/a"
|
||||
"cmd/compile/internal/loopvar/testdata/inlines/b"
|
||||
"cmd/compile/internal/loopvar/testdata/inlines/c"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
func sum(s []*int) int {
|
||||
sum := 0
|
||||
for _, pi := range s {
|
||||
sum += *pi
|
||||
}
|
||||
return sum
|
||||
}
|
||||
|
||||
var t []*int
|
||||
|
||||
func F() []*int {
|
||||
var s []*int
|
||||
for i, j := 0, 0; j < 10; i, j = i+1, j+1 {
|
||||
s = append(s, &i)
|
||||
t = append(s, &j)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
func main() {
|
||||
f := F()
|
||||
af := a.F()
|
||||
bf, _ := b.F()
|
||||
abf := a.Fb()
|
||||
cf := c.F()
|
||||
|
||||
sf, saf, sbf, sabf, scf := sum(f), sum(af), sum(bf), sum(abf), sum(cf)
|
||||
|
||||
fmt.Printf("f, af, bf, abf, cf sums = %d, %d, %d, %d, %d\n", sf, saf, sbf, sabf, scf)
|
||||
|
||||
// Special failure just for use with hash searching, to prove it fires exactly once.
|
||||
// To test: `gossahash -e loopvarhash go run .` in this directory.
|
||||
// This is designed to fail in two different ways, because gossahash searches randomly
|
||||
// it will find both failures over time.
|
||||
if os.Getenv("GOCOMPILEDEBUG") != "" && (sabf == 45 || sf == 45) {
|
||||
os.Exit(11)
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
47
src/cmd/compile/internal/loopvar/testdata/range_esc_address.go
vendored
Normal file
47
src/cmd/compile/internal/loopvar/testdata/range_esc_address.go
vendored
Normal file
@ -0,0 +1,47 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var ints = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
var is []*int
|
||||
for _, i := range ints {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, &i)
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, pi := range is {
|
||||
sum += *pi
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
53
src/cmd/compile/internal/loopvar/testdata/range_esc_closure.go
vendored
Normal file
53
src/cmd/compile/internal/loopvar/testdata/range_esc_closure.go
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var is []func() int
|
||||
|
||||
var ints = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
for _, i := range ints {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, func() int {
|
||||
if i%17 == 15 {
|
||||
i++
|
||||
}
|
||||
return i
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, f := range is {
|
||||
sum += f()
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
53
src/cmd/compile/internal/loopvar/testdata/range_esc_method.go
vendored
Normal file
53
src/cmd/compile/internal/loopvar/testdata/range_esc_method.go
vendored
Normal file
@ -0,0 +1,53 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
type I int
|
||||
|
||||
func (x *I) method() int {
|
||||
return int(*x)
|
||||
}
|
||||
|
||||
var ints = []I{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
var is []func() int
|
||||
for _, i := range ints {
|
||||
for j := 0; j < 10; j++ {
|
||||
if int(i) == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, i.method)
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, m := range is {
|
||||
sum += m()
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
50
src/cmd/compile/internal/loopvar/testdata/range_esc_minimal_closure.go
vendored
Normal file
50
src/cmd/compile/internal/loopvar/testdata/range_esc_minimal_closure.go
vendored
Normal file
@ -0,0 +1,50 @@
|
||||
// Copyright 2023 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
var is []func() int
|
||||
|
||||
var ints = []int{0, 1, 2, 3, 4, 5, 6, 7, 8, 9}
|
||||
|
||||
func main() {
|
||||
sum := 0
|
||||
for _, i := range ints {
|
||||
for j := 0; j < 10; j++ {
|
||||
if i == j { // 10 skips
|
||||
continue
|
||||
}
|
||||
sum++
|
||||
}
|
||||
if i&1 == 0 {
|
||||
is = append(is, func() int {
|
||||
return i
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
bug := false
|
||||
if sum != 100-10 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 90, sum)
|
||||
bug = true
|
||||
}
|
||||
sum = 0
|
||||
for _, f := range is {
|
||||
sum += f()
|
||||
}
|
||||
if sum != 2+4+6+8 {
|
||||
fmt.Printf("wrong sum, expected %d, saw %d\n", 20, sum)
|
||||
bug = true
|
||||
}
|
||||
if !bug {
|
||||
fmt.Printf("PASS\n")
|
||||
} else {
|
||||
os.Exit(11)
|
||||
}
|
||||
}
|
@ -2366,7 +2366,7 @@ func (r *reader) expr() (res ir.Node) {
|
||||
if recv.Type().IsInterface() {
|
||||
// N.B., this happens currently for typeparam/issue51521.go
|
||||
// and typeparam/typeswitch3.go.
|
||||
if base.Flag.LowerM > 0 {
|
||||
if base.Flag.LowerM != 0 {
|
||||
base.WarnfAt(method.Pos(), "imprecise interface call")
|
||||
}
|
||||
}
|
||||
|
9
src/internal/goexperiment/exp_loopvar_off.go
Normal file
9
src/internal/goexperiment/exp_loopvar_off.go
Normal file
@ -0,0 +1,9 @@
|
||||
// Code generated by mkconsts.go. DO NOT EDIT.
|
||||
|
||||
//go:build !goexperiment.loopvar
|
||||
// +build !goexperiment.loopvar
|
||||
|
||||
package goexperiment
|
||||
|
||||
const LoopVar = false
|
||||
const LoopVarInt = 0
|
9
src/internal/goexperiment/exp_loopvar_on.go
Normal file
9
src/internal/goexperiment/exp_loopvar_on.go
Normal file
@ -0,0 +1,9 @@
|
||||
// Code generated by mkconsts.go. DO NOT EDIT.
|
||||
|
||||
//go:build goexperiment.loopvar
|
||||
// +build goexperiment.loopvar
|
||||
|
||||
package goexperiment
|
||||
|
||||
const LoopVar = true
|
||||
const LoopVarInt = 1
|
@ -101,4 +101,8 @@ type Flags struct {
|
||||
// When this experiment is enabled, cgo rule checks occur regardless
|
||||
// of the GODEBUG=cgocheck setting provided at runtime.
|
||||
CgoCheck2 bool
|
||||
|
||||
// LoopVar changes loop semantics so that each iteration gets its own
|
||||
// copy of the iteration variable.
|
||||
LoopVar bool
|
||||
}
|
||||
|
3
src/runtime/race/testdata/mop_test.go
vendored
3
src/runtime/race/testdata/mop_test.go
vendored
@ -331,7 +331,8 @@ func TestRaceRange(t *testing.T) {
|
||||
var x, y int
|
||||
_ = x + y
|
||||
done := make(chan bool, N)
|
||||
for i, v := range a {
|
||||
var i, v int // declare here (not in for stmt) so that i and v are shared w/ or w/o loop variable sharing change
|
||||
for i, v = range a {
|
||||
go func(i int) {
|
||||
// we don't want a write-vs-write race
|
||||
// so there is no array b here
|
||||
|
@ -73,7 +73,8 @@ func main() {
|
||||
|
||||
{
|
||||
var g func() int
|
||||
for i := range [2]int{} {
|
||||
var i int
|
||||
for i = range [2]int{} {
|
||||
if i == 0 {
|
||||
g = func() int {
|
||||
return i // test that we capture by ref here, i is mutated on every interaction
|
||||
|
@ -126,7 +126,8 @@ func range_escapes2(x, y int) (*int, *int) {
|
||||
var p [2]*int
|
||||
a[0] = x
|
||||
a[1] = y
|
||||
for k, v := range a {
|
||||
var k, v int
|
||||
for k, v = range a {
|
||||
p[k] = &v
|
||||
}
|
||||
return p[0], p[1]
|
||||
@ -136,7 +137,8 @@ func range_escapes2(x, y int) (*int, *int) {
|
||||
func for_escapes2(x int, y int) (*int, *int) {
|
||||
var p [2]*int
|
||||
n := 0
|
||||
for i := x; n < 2; i = y {
|
||||
i := x
|
||||
for ; n < 2; i = y {
|
||||
p[n] = &i
|
||||
n++
|
||||
}
|
||||
|
@ -45,8 +45,9 @@ func test1(iter int) {
|
||||
// Heap -> stack pointer eventually causes badness when stack reallocation
|
||||
// occurs.
|
||||
|
||||
var fn func() // ERROR "moved to heap: fn$"
|
||||
for i := 0; i < maxI; i++ { // ERROR "moved to heap: i$"
|
||||
var fn func() // ERROR "moved to heap: fn$"
|
||||
i := 0 // ERROR "moved to heap: i$"
|
||||
for ; i < maxI; i++ {
|
||||
// var fn func() // this makes it work, because fn stays off heap
|
||||
j := 0 // ERROR "moved to heap: j$"
|
||||
fn = func() { // ERROR "func literal escapes to heap$"
|
||||
|
Loading…
Reference in New Issue
Block a user