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