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 }