blob: 030fc04c1369d8da7c1e946dfcc64e709dca2a3d [file] [log] [blame]
// 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/logopt"
"cmd/compile/internal/typecheck"
"cmd/compile/internal/types"
"cmd/internal/src"
"fmt"
)
type VarAndLoop struct {
Name *ir.Name
Loop ir.Node // the *ir.RangeStmt or *ir.ForStmt. Used for identity and position
LastPos src.XPos // the last position observed within Loop
}
// 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).
//
// The decision to transform loops is normally encoded in the For/Range loop node
// field DistinctVars but is also dependent on base.LoopVarHash, and some values
// of base.Debug.LoopVar (which is set per-package). Decisions encoded in DistinctVars
// are preserved across inlining, so if package a calls b.F and loops in b.F are
// transformed, then they are always transformed, whether b.F is inlined or not.
//
// Per-package, the debug flag settings that affect this transformer:
//
// 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) []VarAndLoop {
// if a loop variable is transformed it is appended to this slice for later logging
var transformed []VarAndLoop
describe := func(n *ir.Name) string {
pos := n.Pos()
inner := base.Ctxt.InnermostPos(pos)
outer := base.Ctxt.OutermostPos(pos)
if inner == outer {
return fmt.Sprintf("loop variable %v now per-iteration", n)
}
return fmt.Sprintf("loop variable %v now per-iteration (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
}
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)
// these enable an optimization of "escape" under return statements
loopDepth := 0
returnInLoopDepth := 0
// 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
}
}
// For reporting, keep track of the last position within any loop.
// Loops nest, also need to be sensitive to inlining.
var lastPos src.XPos
updateLastPos := func(p src.XPos) {
pl, ll := p.Line(), lastPos.Line()
if p.SameFile(lastPos) &&
(pl > ll || pl == ll && p.Col() > lastPos.Col()) {
lastPos = p
}
}
// 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] {
desc := func() string {
return describe(n)
}
if base.LoopVarHash.MatchPos(n.Pos(), desc) {
// Rename the loop key, prefix body with assignment from loop key
transformed = append(transformed, VarAndLoop{n, x, lastPos})
tk := typecheck.TempAt(base.Pos, fn, 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 w/ DistinctVars, 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 {
if loopDepth > 0 {
updateLastPos(n.Pos())
}
switch x := n.(type) {
case *ir.ClosureExpr:
if returnInLoopDepth >= loopDepth {
// This expression is a child of a return, which escapes all loops above
// the return, but not those between this expression and the return.
break
}
for _, cv := range x.Func.ClosureVars {
v := cv.Canonical()
if _, ok := possiblyLeaked[v]; ok {
possiblyLeaked[v] = true
}
}
case *ir.AddrExpr:
if returnInLoopDepth >= loopDepth {
// This expression is a child of a return, which escapes all loops above
// the return, but not those between this expression and the return.
break
}
// 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.ReturnStmt:
savedRILD := returnInLoopDepth
returnInLoopDepth = loopDepth
defer func() { returnInLoopDepth = savedRILD }()
case *ir.RangeStmt:
if !(x.Def && x.DistinctVars) {
// range loop must define its iteration variables AND have distinctVars.
x.DistinctVars = false
break
}
noteMayLeak(x.Key)
noteMayLeak(x.Value)
loopDepth++
savedLastPos := lastPos
lastPos = x.Pos() // this sets the file.
ir.DoChildren(n, scanChildrenThenTransform)
loopDepth--
x.Key = maybeReplaceVar(x.Key, x)
x.Value = maybeReplaceVar(x.Value, x)
thisLastPos := lastPos
lastPos = savedLastPos
updateLastPos(thisLastPos) // this will propagate lastPos if in the same file.
x.DistinctVars = false
return false
case *ir.ForStmt:
if !x.DistinctVars {
break
}
forAllDefInInit(x, noteMayLeak)
loopDepth++
savedLastPos := lastPos
lastPos = x.Pos() // this sets the file.
ir.DoChildren(n, scanChildrenThenTransform)
loopDepth--
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] {
desc := func() string {
return describe(n)
}
// Hash on n.Pos() for most precise failure location.
if base.LoopVarHash.MatchPos(n.Pos(), desc) {
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, VarAndLoop{z, x, lastPos})
tz := typecheck.TempAt(base.Pos, fn, 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.Node
if postNotNil {
// body' = prebody +
// (6) if tmp_first {tmp_first = false} else {Post} +
// if !cond {break} + ...
tmpFirst := typecheck.TempAt(base.Pos, fn, types.Types[types.TBOOL])
tmpFirstDcl = typecheck.Stmt(ir.NewAssignStmt(x.Pos(), tmpFirst, ir.NewBool(base.Pos, true)))
tmpFirstSetFalse := typecheck.Stmt(ir.NewAssignStmt(x.Pos(), tmpFirst, ir.NewBool(base.Pos, false)))
ifTmpFirst := ir.NewIfStmt(x.Pos(), tmpFirst, ir.Nodes{tmpFirstSetFalse}, ir.Nodes{x.Post})
ifTmpFirst.PtrInit().Append(typecheck.Stmt(ir.NewDecl(base.Pos, ir.ODCL, tmpFirst))) // declares tmpFirst
preBody.Append(typecheck.Stmt(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
}
thisLastPos := lastPos
lastPos = savedLastPos
updateLastPos(thisLastPos) // this will propagate lastPos if in the same file.
x.DistinctVars = false
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 assignments 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)
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)
}
func LogTransformations(transformed []VarAndLoop) {
print := 2 <= base.Debug.LoopVar && base.Debug.LoopVar != 11
if print || 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.
// trueInlinedPos rebases inner w/o inline context so that it prints correctly in WarnfAt; otherwise it prints as outer.
trueInlinedPos := func(inner src.Pos) src.XPos {
afn := inner.AbsFilename()
pb, ok := fileToPosBase[afn]
if !ok {
pb = src.NewFileBase(inner.Filename(), afn)
fileToPosBase[afn] = pb
}
inner.SetBase(pb)
return base.Ctxt.PosTable.XPos(inner)
}
type unit struct{}
loopsSeen := make(map[ir.Node]unit)
type loopPos struct {
loop ir.Node
last src.XPos
curfn *ir.Func
}
var loops []loopPos
for _, lv := range transformed {
n := lv.Name
if _, ok := loopsSeen[lv.Loop]; !ok {
l := lv.Loop
loopsSeen[l] = unit{}
loops = append(loops, loopPos{l, lv.LastPos, n.Curfn})
}
pos := n.Pos()
inner := base.Ctxt.InnermostPos(pos)
outer := base.Ctxt.OutermostPos(pos)
if logopt.Enabled() {
// For automated checking of coverage of this transformation, include this in the JSON information.
var nString interface{} = n
if inner != outer {
nString = fmt.Sprintf("%v (from inline)", n)
}
if n.Esc() == ir.EscHeap {
logopt.LogOpt(pos, "iteration-variable-to-heap", "loopvar", ir.FuncName(n.Curfn), nString)
} else {
logopt.LogOpt(pos, "iteration-variable-to-stack", "loopvar", ir.FuncName(n.Curfn), nString)
}
}
if print {
if inner == outer {
if n.Esc() == ir.EscHeap {
base.WarnfAt(pos, "loop variable %v now per-iteration, heap-allocated", n)
} else {
base.WarnfAt(pos, "loop variable %v now per-iteration, stack-allocated", n)
}
} else {
innerXPos := trueInlinedPos(inner)
if n.Esc() == ir.EscHeap {
base.WarnfAt(innerXPos, "loop variable %v now per-iteration, heap-allocated (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
} else {
base.WarnfAt(innerXPos, "loop variable %v now per-iteration, stack-allocated (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
}
}
}
}
for _, l := range loops {
pos := l.loop.Pos()
last := l.last
loopKind := "range"
if _, ok := l.loop.(*ir.ForStmt); ok {
loopKind = "for"
}
if logopt.Enabled() {
// Intended to help with performance debugging, we record whole loop ranges
logopt.LogOptRange(pos, last, "loop-modified-"+loopKind, "loopvar", ir.FuncName(l.curfn))
}
if print && 4 <= base.Debug.LoopVar {
// TODO decide if we want to keep this, or not. It was helpful for validating logopt, otherwise, eh.
inner := base.Ctxt.InnermostPos(pos)
outer := base.Ctxt.OutermostPos(pos)
if inner == outer {
base.WarnfAt(pos, "%s loop ending at %d:%d was modified", loopKind, last.Line(), last.Col())
} else {
pos = trueInlinedPos(inner)
last = trueInlinedPos(base.Ctxt.InnermostPos(last))
base.WarnfAt(pos, "%s loop ending at %d:%d was modified (loop inlined into %s:%d)", loopKind, last.Line(), last.Col(), outer.Filename(), outer.Line())
}
}
}
}
}