github.com/bir3/gocompiler@v0.9.2202/src/cmd/compile/internal/loopvar/loopvar.go (about)

     1  // Copyright 2023 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package loopvar applies the proper variable capture, according
     6  // to experiment, flags, language version, etc.
     7  package loopvar
     8  
     9  import (
    10  	"github.com/bir3/gocompiler/src/cmd/compile/internal/base"
    11  	"github.com/bir3/gocompiler/src/cmd/compile/internal/ir"
    12  	"github.com/bir3/gocompiler/src/cmd/compile/internal/logopt"
    13  	"github.com/bir3/gocompiler/src/cmd/compile/internal/typecheck"
    14  	"github.com/bir3/gocompiler/src/cmd/compile/internal/types"
    15  	"github.com/bir3/gocompiler/src/cmd/internal/src"
    16  	"fmt"
    17  )
    18  
    19  type VarAndLoop struct {
    20  	Name	*ir.Name
    21  	Loop	ir.Node		// the *ir.RangeStmt or *ir.ForStmt. Used for identity and position
    22  	LastPos	src.XPos	// the last position observed within Loop
    23  }
    24  
    25  // ForCapture transforms for and range loops that declare variables that might be
    26  // captured by a closure or escaped to the heap, using a syntactic check that
    27  // conservatively overestimates the loops where capture occurs, but still avoids
    28  // transforming the (large) majority of loops. It returns the list of names
    29  // subject to this change, that may (once transformed) be heap allocated in the
    30  // process. (This allows checking after escape analysis to call out any such
    31  // variables, in case it causes allocation/performance problems).
    32  //
    33  // The decision to transform loops is normally encoded in the For/Range loop node
    34  // field DistinctVars but is also dependent on base.LoopVarHash, and some values
    35  // of base.Debug.LoopVar (which is set per-package).  Decisions encoded in DistinctVars
    36  // are preserved across inlining, so if package a calls b.F and loops in b.F are
    37  // transformed, then they are always transformed, whether b.F is inlined or not.
    38  //
    39  // Per-package, the debug flag settings that affect this transformer:
    40  //
    41  // base.LoopVarHash != nil => use hash setting to govern transformation.
    42  // note that LoopVarHash != nil sets base.Debug.LoopVar to 1 (unless it is >= 11, for testing/debugging).
    43  //
    44  // base.Debug.LoopVar == 11 => transform ALL loops ignoring syntactic/potential escape. Do not log, can be in addition to GOEXPERIMENT.
    45  //
    46  // The effect of GOEXPERIMENT=loopvar is to change the default value (0) of base.Debug.LoopVar to 1 for all packages.
    47  func ForCapture(fn *ir.Func) []VarAndLoop {
    48  	// if a loop variable is transformed it is appended to this slice for later logging
    49  	var transformed []VarAndLoop
    50  
    51  	describe := func(n *ir.Name) string {
    52  		pos := n.Pos()
    53  		inner := base.Ctxt.InnermostPos(pos)
    54  		outer := base.Ctxt.OutermostPos(pos)
    55  		if inner == outer {
    56  			return fmt.Sprintf("loop variable %v now per-iteration", n)
    57  		}
    58  		return fmt.Sprintf("loop variable %v now per-iteration (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
    59  	}
    60  
    61  	forCapture := func() {
    62  		seq := 1
    63  
    64  		dclFixups := make(map[*ir.Name]ir.Stmt)
    65  
    66  		// possibly leaked includes names of declared loop variables that may be leaked;
    67  		// the mapped value is true if the name is *syntactically* leaked, and those loops
    68  		// will be transformed.
    69  		possiblyLeaked := make(map[*ir.Name]bool)
    70  
    71  		// these enable an optimization of "escape" under return statements
    72  		loopDepth := 0
    73  		returnInLoopDepth := 0
    74  
    75  		// noteMayLeak is called for candidate variables in for range/3-clause, and
    76  		// adds them (mapped to false) to possiblyLeaked.
    77  		noteMayLeak := func(x ir.Node) {
    78  			if n, ok := x.(*ir.Name); ok {
    79  				if n.Type().Kind() == types.TBLANK {
    80  					return
    81  				}
    82  				// default is false (leak candidate, not yet known to leak), but flag can make all variables "leak"
    83  				possiblyLeaked[n] = base.Debug.LoopVar >= 11
    84  			}
    85  		}
    86  
    87  		// For reporting, keep track of the last position within any loop.
    88  		// Loops nest, also need to be sensitive to inlining.
    89  		var lastPos src.XPos
    90  
    91  		updateLastPos := func(p src.XPos) {
    92  			pl, ll := p.Line(), lastPos.Line()
    93  			if p.SameFile(lastPos) &&
    94  				(pl > ll || pl == ll && p.Col() > lastPos.Col()) {
    95  				lastPos = p
    96  			}
    97  		}
    98  
    99  		// maybeReplaceVar unshares an iteration variable for a range loop,
   100  		// if that variable was actually (syntactically) leaked,
   101  		// subject to hash-variable debugging.
   102  		maybeReplaceVar := func(k ir.Node, x *ir.RangeStmt) ir.Node {
   103  			if n, ok := k.(*ir.Name); ok && possiblyLeaked[n] {
   104  				desc := func() string {
   105  					return describe(n)
   106  				}
   107  				if base.LoopVarHash.MatchPos(n.Pos(), desc) {
   108  					// Rename the loop key, prefix body with assignment from loop key
   109  					transformed = append(transformed, VarAndLoop{n, x, lastPos})
   110  					tk := typecheck.TempAt(base.Pos, fn, n.Type())
   111  					tk.SetTypecheck(1)
   112  					as := ir.NewAssignStmt(x.Pos(), n, tk)
   113  					as.Def = true
   114  					as.SetTypecheck(1)
   115  					x.Body.Prepend(as)
   116  					dclFixups[n] = as
   117  					return tk
   118  				}
   119  			}
   120  			return k
   121  		}
   122  
   123  		// scanChildrenThenTransform processes node x to:
   124  		//  1. if x is a for/range w/ DistinctVars, note declared iteration variables possiblyLeaked (PL)
   125  		//  2. search all of x's children for syntactically escaping references to v in PL,
   126  		//     meaning either address-of-v or v-captured-by-a-closure
   127  		//  3. for all v in PL that had a syntactically escaping reference, transform the declaration
   128  		//     and (in case of 3-clause loop) the loop to the unshared loop semantics.
   129  		//  This is all much simpler for range loops; 3-clause loops can have an arbitrary number
   130  		//  of iteration variables and the transformation is more involved, range loops have at most 2.
   131  		var scanChildrenThenTransform func(x ir.Node) bool
   132  		scanChildrenThenTransform = func(n ir.Node) bool {
   133  
   134  			if loopDepth > 0 {
   135  				updateLastPos(n.Pos())
   136  			}
   137  
   138  			switch x := n.(type) {
   139  			case *ir.ClosureExpr:
   140  				if returnInLoopDepth >= loopDepth {
   141  					// This expression is a child of a return, which escapes all loops above
   142  					// the return, but not those between this expression and the return.
   143  					break
   144  				}
   145  				for _, cv := range x.Func.ClosureVars {
   146  					v := cv.Canonical()
   147  					if _, ok := possiblyLeaked[v]; ok {
   148  						possiblyLeaked[v] = true
   149  					}
   150  				}
   151  
   152  			case *ir.AddrExpr:
   153  				if returnInLoopDepth >= loopDepth {
   154  					// This expression is a child of a return, which escapes all loops above
   155  					// the return, but not those between this expression and the return.
   156  					break
   157  				}
   158  				// Explicitly note address-taken so that return-statements can be excluded
   159  				y := ir.OuterValue(x.X)
   160  				if y.Op() != ir.ONAME {
   161  					break
   162  				}
   163  				z, ok := y.(*ir.Name)
   164  				if !ok {
   165  					break
   166  				}
   167  				switch z.Class {
   168  				case ir.PAUTO, ir.PPARAM, ir.PPARAMOUT, ir.PAUTOHEAP:
   169  					if _, ok := possiblyLeaked[z]; ok {
   170  						possiblyLeaked[z] = true
   171  					}
   172  				}
   173  
   174  			case *ir.ReturnStmt:
   175  				savedRILD := returnInLoopDepth
   176  				returnInLoopDepth = loopDepth
   177  				defer func() { returnInLoopDepth = savedRILD }()
   178  
   179  			case *ir.RangeStmt:
   180  				if !(x.Def && x.DistinctVars) {
   181  					// range loop must define its iteration variables AND have distinctVars.
   182  					x.DistinctVars = false
   183  					break
   184  				}
   185  				noteMayLeak(x.Key)
   186  				noteMayLeak(x.Value)
   187  				loopDepth++
   188  				savedLastPos := lastPos
   189  				lastPos = x.Pos()	// this sets the file.
   190  				ir.DoChildren(n, scanChildrenThenTransform)
   191  				loopDepth--
   192  				x.Key = maybeReplaceVar(x.Key, x)
   193  				x.Value = maybeReplaceVar(x.Value, x)
   194  				thisLastPos := lastPos
   195  				lastPos = savedLastPos
   196  				updateLastPos(thisLastPos)	// this will propagate lastPos if in the same file.
   197  				x.DistinctVars = false
   198  				return false
   199  
   200  			case *ir.ForStmt:
   201  				if !x.DistinctVars {
   202  					break
   203  				}
   204  				forAllDefInInit(x, noteMayLeak)
   205  				loopDepth++
   206  				savedLastPos := lastPos
   207  				lastPos = x.Pos()	// this sets the file.
   208  				ir.DoChildren(n, scanChildrenThenTransform)
   209  				loopDepth--
   210  				var leaked []*ir.Name
   211  				// Collect the leaking variables for the much-more-complex transformation.
   212  				forAllDefInInit(x, func(z ir.Node) {
   213  					if n, ok := z.(*ir.Name); ok && possiblyLeaked[n] {
   214  						desc := func() string {
   215  							return describe(n)
   216  						}
   217  						// Hash on n.Pos() for most precise failure location.
   218  						if base.LoopVarHash.MatchPos(n.Pos(), desc) {
   219  							leaked = append(leaked, n)
   220  						}
   221  					}
   222  				})
   223  
   224  				if len(leaked) > 0 {
   225  					// need to transform the for loop just so.
   226  
   227  					/* Contrived example, w/ numbered comments from the transformation:
   228  									BEFORE:
   229  										var escape []*int
   230  										for z := 0; z < n; z++ {
   231  											if reason() {
   232  												escape = append(escape, &z)
   233  												continue
   234  											}
   235  											z = z + z
   236  											stuff
   237  										}
   238  									AFTER:
   239  										for z', tmp_first := 0, true; ; { // (4)
   240  											                              // (5) body' follows:
   241  											z := z'                       // (1)
   242  											if tmp_first {tmp_first = false} else {z++} // (6)
   243  											if ! (z < n) { break }        // (7)
   244  											                              // (3, 8) body_continue
   245  											if reason() {
   246  					                            escape = append(escape, &z)
   247  												goto next                 // rewritten continue
   248  											}
   249  											z = z + z
   250  											stuff
   251  										next:                             // (9)
   252  											z' = z                       // (2)
   253  										}
   254  
   255  										In the case that the loop contains no increment (z++),
   256  										there is no need for step 6,
   257  										and thus no need to test, update, or declare tmp_first (part of step 4).
   258  										Similarly if the loop contains no exit test (z < n),
   259  										then there is no need for step 7.
   260  					*/
   261  
   262  					// Expressed in terms of the input ForStmt
   263  					//
   264  					// 	type ForStmt struct {
   265  					// 	init     Nodes
   266  					// 	Label    *types.Sym
   267  					// 	Cond     Node  // empty if OFORUNTIL
   268  					// 	Post     Node
   269  					// 	Body     Nodes
   270  					// 	HasBreak bool
   271  					// }
   272  
   273  					// OFOR: init; loop: if !Cond {break}; Body; Post; goto loop
   274  
   275  					// (1) prebody = {z := z' for z in leaked}
   276  					// (2) postbody = {z' = z for z in leaked}
   277  					// (3) body_continue = {body : s/continue/goto next}
   278  					// (4) init' = (init : s/z/z' for z in leaked) + tmp_first := true
   279  					// (5) body' = prebody +        // appears out of order below
   280  					// (6)         if tmp_first {tmp_first = false} else {Post} +
   281  					// (7)         if !cond {break} +
   282  					// (8)         body_continue (3) +
   283  					// (9)         next: postbody (2)
   284  					// (10) cond' = {}
   285  					// (11) post' = {}
   286  
   287  					// minor optimizations:
   288  					//   if Post is empty, tmp_first and step 6 can be skipped.
   289  					//   if Cond is empty, that code can also be skipped.
   290  
   291  					var preBody, postBody ir.Nodes
   292  
   293  					// Given original iteration variable z, what is the corresponding z'
   294  					// that carries the value from iteration to iteration?
   295  					zPrimeForZ := make(map[*ir.Name]*ir.Name)
   296  
   297  					// (1,2) initialize preBody and postBody
   298  					for _, z := range leaked {
   299  						transformed = append(transformed, VarAndLoop{z, x, lastPos})
   300  
   301  						tz := typecheck.TempAt(base.Pos, fn, z.Type())
   302  						tz.SetTypecheck(1)
   303  						zPrimeForZ[z] = tz
   304  
   305  						as := ir.NewAssignStmt(x.Pos(), z, tz)
   306  						as.Def = true
   307  						as.SetTypecheck(1)
   308  						preBody.Append(as)
   309  						dclFixups[z] = as
   310  
   311  						as = ir.NewAssignStmt(x.Pos(), tz, z)
   312  						as.SetTypecheck(1)
   313  						postBody.Append(as)
   314  
   315  					}
   316  
   317  					// (3) rewrite continues in body -- rewrite is inplace, so works for top level visit, too.
   318  					label := typecheck.Lookup(fmt.Sprintf(".3clNext_%d", seq))
   319  					seq++
   320  					labelStmt := ir.NewLabelStmt(x.Pos(), label)
   321  					labelStmt.SetTypecheck(1)
   322  
   323  					loopLabel := x.Label
   324  					loopDepth := 0
   325  					var editContinues func(x ir.Node) bool
   326  					editContinues = func(x ir.Node) bool {
   327  
   328  						switch c := x.(type) {
   329  						case *ir.BranchStmt:
   330  							// If this is a continue targeting the loop currently being rewritten, transform it to an appropriate GOTO
   331  							if c.Op() == ir.OCONTINUE && (loopDepth == 0 && c.Label == nil || loopLabel != nil && c.Label == loopLabel) {
   332  								c.Label = label
   333  								c.SetOp(ir.OGOTO)
   334  							}
   335  						case *ir.RangeStmt, *ir.ForStmt:
   336  							loopDepth++
   337  							ir.DoChildren(x, editContinues)
   338  							loopDepth--
   339  							return false
   340  						}
   341  						ir.DoChildren(x, editContinues)
   342  						return false
   343  					}
   344  					for _, y := range x.Body {
   345  						editContinues(y)
   346  					}
   347  					bodyContinue := x.Body
   348  
   349  					// (4) rewrite init
   350  					forAllDefInInitUpdate(x, func(z ir.Node, pz *ir.Node) {
   351  						// note tempFor[n] can be nil if hash searching.
   352  						if n, ok := z.(*ir.Name); ok && possiblyLeaked[n] && zPrimeForZ[n] != nil {
   353  							*pz = zPrimeForZ[n]
   354  						}
   355  					})
   356  
   357  					postNotNil := x.Post != nil
   358  					var tmpFirstDcl ir.Node
   359  					if postNotNil {
   360  						// body' = prebody +
   361  						// (6)     if tmp_first {tmp_first = false} else {Post} +
   362  						//         if !cond {break} + ...
   363  						tmpFirst := typecheck.TempAt(base.Pos, fn, types.Types[types.TBOOL])
   364  						tmpFirstDcl = typecheck.Stmt(ir.NewAssignStmt(x.Pos(), tmpFirst, ir.NewBool(base.Pos, true)))
   365  						tmpFirstSetFalse := typecheck.Stmt(ir.NewAssignStmt(x.Pos(), tmpFirst, ir.NewBool(base.Pos, false)))
   366  						ifTmpFirst := ir.NewIfStmt(x.Pos(), tmpFirst, ir.Nodes{tmpFirstSetFalse}, ir.Nodes{x.Post})
   367  						ifTmpFirst.PtrInit().Append(typecheck.Stmt(ir.NewDecl(base.Pos, ir.ODCL, tmpFirst)))	// declares tmpFirst
   368  						preBody.Append(typecheck.Stmt(ifTmpFirst))
   369  					}
   370  
   371  					// body' = prebody +
   372  					//         if tmp_first {tmp_first = false} else {Post} +
   373  					// (7)     if !cond {break} + ...
   374  					if x.Cond != nil {
   375  						notCond := ir.NewUnaryExpr(x.Cond.Pos(), ir.ONOT, x.Cond)
   376  						notCond.SetType(x.Cond.Type())
   377  						notCond.SetTypecheck(1)
   378  						newBreak := ir.NewBranchStmt(x.Pos(), ir.OBREAK, nil)
   379  						newBreak.SetTypecheck(1)
   380  						ifNotCond := ir.NewIfStmt(x.Pos(), notCond, ir.Nodes{newBreak}, nil)
   381  						ifNotCond.SetTypecheck(1)
   382  						preBody.Append(ifNotCond)
   383  					}
   384  
   385  					if postNotNil {
   386  						x.PtrInit().Append(tmpFirstDcl)
   387  					}
   388  
   389  					// (8)
   390  					preBody.Append(bodyContinue...)
   391  					// (9)
   392  					preBody.Append(labelStmt)
   393  					preBody.Append(postBody...)
   394  
   395  					// (5) body' = prebody + ...
   396  					x.Body = preBody
   397  
   398  					// (10) cond' = {}
   399  					x.Cond = nil
   400  
   401  					// (11) post' = {}
   402  					x.Post = nil
   403  				}
   404  				thisLastPos := lastPos
   405  				lastPos = savedLastPos
   406  				updateLastPos(thisLastPos)	// this will propagate lastPos if in the same file.
   407  				x.DistinctVars = false
   408  
   409  				return false
   410  			}
   411  
   412  			ir.DoChildren(n, scanChildrenThenTransform)
   413  
   414  			return false
   415  		}
   416  		scanChildrenThenTransform(fn)
   417  		if len(transformed) > 0 {
   418  			// editNodes scans a slice C of ir.Node, looking for declarations that
   419  			// appear in dclFixups.  Any declaration D whose "fixup" is an assignmnt
   420  			// statement A is removed from the C and relocated to the Init
   421  			// of A.  editNodes returns the modified slice of ir.Node.
   422  			editNodes := func(c ir.Nodes) ir.Nodes {
   423  				j := 0
   424  				for _, n := range c {
   425  					if d, ok := n.(*ir.Decl); ok {
   426  						if s := dclFixups[d.X]; s != nil {
   427  							switch a := s.(type) {
   428  							case *ir.AssignStmt:
   429  								a.PtrInit().Prepend(d)
   430  								delete(dclFixups, d.X)	// can't be sure of visit order, wouldn't want to visit twice.
   431  							default:
   432  								base.Fatalf("not implemented yet for node type %v", s.Op())
   433  							}
   434  							continue	// do not copy this node, and do not increment j
   435  						}
   436  					}
   437  					c[j] = n
   438  					j++
   439  				}
   440  				for k := j; k < len(c); k++ {
   441  					c[k] = nil
   442  				}
   443  				return c[:j]
   444  			}
   445  			// fixup all tagged declarations in all the statements lists in fn.
   446  			rewriteNodes(fn, editNodes)
   447  		}
   448  	}
   449  	ir.WithFunc(fn, forCapture)
   450  	return transformed
   451  }
   452  
   453  // forAllDefInInitUpdate applies "do" to all the defining assignments in the Init clause of a ForStmt.
   454  // This abstracts away some of the boilerplate from the already complex and verbose for-3-clause case.
   455  func forAllDefInInitUpdate(x *ir.ForStmt, do func(z ir.Node, update *ir.Node)) {
   456  	for _, s := range x.Init() {
   457  		switch y := s.(type) {
   458  		case *ir.AssignListStmt:
   459  			if !y.Def {
   460  				continue
   461  			}
   462  			for i, z := range y.Lhs {
   463  				do(z, &y.Lhs[i])
   464  			}
   465  		case *ir.AssignStmt:
   466  			if !y.Def {
   467  				continue
   468  			}
   469  			do(y.X, &y.X)
   470  		}
   471  	}
   472  }
   473  
   474  // forAllDefInInit is forAllDefInInitUpdate without the update option.
   475  func forAllDefInInit(x *ir.ForStmt, do func(z ir.Node)) {
   476  	forAllDefInInitUpdate(x, func(z ir.Node, _ *ir.Node) { do(z) })
   477  }
   478  
   479  // rewriteNodes applies editNodes to all statement lists in fn.
   480  func rewriteNodes(fn *ir.Func, editNodes func(c ir.Nodes) ir.Nodes) {
   481  	var forNodes func(x ir.Node) bool
   482  	forNodes = func(n ir.Node) bool {
   483  		if stmt, ok := n.(ir.InitNode); ok {
   484  			// process init list
   485  			stmt.SetInit(editNodes(stmt.Init()))
   486  		}
   487  		switch x := n.(type) {
   488  		case *ir.Func:
   489  			x.Body = editNodes(x.Body)
   490  		case *ir.InlinedCallExpr:
   491  			x.Body = editNodes(x.Body)
   492  
   493  		case *ir.CaseClause:
   494  			x.Body = editNodes(x.Body)
   495  		case *ir.CommClause:
   496  			x.Body = editNodes(x.Body)
   497  
   498  		case *ir.BlockStmt:
   499  			x.List = editNodes(x.List)
   500  
   501  		case *ir.ForStmt:
   502  			x.Body = editNodes(x.Body)
   503  		case *ir.RangeStmt:
   504  			x.Body = editNodes(x.Body)
   505  		case *ir.IfStmt:
   506  			x.Body = editNodes(x.Body)
   507  			x.Else = editNodes(x.Else)
   508  		case *ir.SelectStmt:
   509  			x.Compiled = editNodes(x.Compiled)
   510  		case *ir.SwitchStmt:
   511  			x.Compiled = editNodes(x.Compiled)
   512  		}
   513  		ir.DoChildren(n, forNodes)
   514  		return false
   515  	}
   516  	forNodes(fn)
   517  }
   518  
   519  func LogTransformations(transformed []VarAndLoop) {
   520  	print := 2 <= base.Debug.LoopVar && base.Debug.LoopVar != 11
   521  
   522  	if print || logopt.Enabled() {	// 11 is do them all, quietly, 12 includes debugging.
   523  		fileToPosBase := make(map[string]*src.PosBase)	// used to remove inline context for innermost reporting.
   524  
   525  		// trueInlinedPos rebases inner w/o inline context so that it prints correctly in WarnfAt; otherwise it prints as outer.
   526  		trueInlinedPos := func(inner src.Pos) src.XPos {
   527  			afn := inner.AbsFilename()
   528  			pb, ok := fileToPosBase[afn]
   529  			if !ok {
   530  				pb = src.NewFileBase(inner.Filename(), afn)
   531  				fileToPosBase[afn] = pb
   532  			}
   533  			inner.SetBase(pb)
   534  			return base.Ctxt.PosTable.XPos(inner)
   535  		}
   536  
   537  		type unit struct{}
   538  		loopsSeen := make(map[ir.Node]unit)
   539  		type loopPos struct {
   540  			loop	ir.Node
   541  			last	src.XPos
   542  			curfn	*ir.Func
   543  		}
   544  		var loops []loopPos
   545  		for _, lv := range transformed {
   546  			n := lv.Name
   547  			if _, ok := loopsSeen[lv.Loop]; !ok {
   548  				l := lv.Loop
   549  				loopsSeen[l] = unit{}
   550  				loops = append(loops, loopPos{l, lv.LastPos, n.Curfn})
   551  			}
   552  			pos := n.Pos()
   553  
   554  			inner := base.Ctxt.InnermostPos(pos)
   555  			outer := base.Ctxt.OutermostPos(pos)
   556  
   557  			if logopt.Enabled() {
   558  				// For automated checking of coverage of this transformation, include this in the JSON information.
   559  				var nString interface{} = n
   560  				if inner != outer {
   561  					nString = fmt.Sprintf("%v (from inline)", n)
   562  				}
   563  				if n.Esc() == ir.EscHeap {
   564  					logopt.LogOpt(pos, "iteration-variable-to-heap", "loopvar", ir.FuncName(n.Curfn), nString)
   565  				} else {
   566  					logopt.LogOpt(pos, "iteration-variable-to-stack", "loopvar", ir.FuncName(n.Curfn), nString)
   567  				}
   568  			}
   569  			if print {
   570  				if inner == outer {
   571  					if n.Esc() == ir.EscHeap {
   572  						base.WarnfAt(pos, "loop variable %v now per-iteration, heap-allocated", n)
   573  					} else {
   574  						base.WarnfAt(pos, "loop variable %v now per-iteration, stack-allocated", n)
   575  					}
   576  				} else {
   577  					innerXPos := trueInlinedPos(inner)
   578  					if n.Esc() == ir.EscHeap {
   579  						base.WarnfAt(innerXPos, "loop variable %v now per-iteration, heap-allocated (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
   580  					} else {
   581  						base.WarnfAt(innerXPos, "loop variable %v now per-iteration, stack-allocated (loop inlined into %s:%d)", n, outer.Filename(), outer.Line())
   582  					}
   583  				}
   584  			}
   585  		}
   586  		for _, l := range loops {
   587  			pos := l.loop.Pos()
   588  			last := l.last
   589  			loopKind := "range"
   590  			if _, ok := l.loop.(*ir.ForStmt); ok {
   591  				loopKind = "for"
   592  			}
   593  			if logopt.Enabled() {
   594  				// Intended to help with performance debugging, we record whole loop ranges
   595  				logopt.LogOptRange(pos, last, "loop-modified-"+loopKind, "loopvar", ir.FuncName(l.curfn))
   596  			}
   597  			if print && 4 <= base.Debug.LoopVar {
   598  				// TODO decide if we want to keep this, or not.  It was helpful for validating logopt, otherwise, eh.
   599  				inner := base.Ctxt.InnermostPos(pos)
   600  				outer := base.Ctxt.OutermostPos(pos)
   601  
   602  				if inner == outer {
   603  					base.WarnfAt(pos, "%s loop ending at %d:%d was modified", loopKind, last.Line(), last.Col())
   604  				} else {
   605  					pos = trueInlinedPos(inner)
   606  					last = trueInlinedPos(base.Ctxt.InnermostPos(last))
   607  					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())
   608  				}
   609  			}
   610  		}
   611  	}
   612  }