| // Copyright 2025 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 escape |
| |
| import ( |
| "cmd/compile/internal/base" |
| "cmd/compile/internal/ir" |
| "cmd/internal/src" |
| "fmt" |
| "maps" |
| "path/filepath" |
| ) |
| |
| type aliasAnalysis struct { |
| // fn is the function being analyzed. |
| fn *ir.Func |
| |
| // candidateSlices are declared slices that |
| // start unaliased and might still be unaliased. |
| candidateSlices map[*ir.Name]candidateSlice |
| |
| // noAliasAppends are appends that have been |
| // proven to use an unaliased slice. |
| noAliasAppends []*ir.CallExpr |
| |
| // loops is a stack of observed loops, |
| // each with a list of candidate appends. |
| loops [][]candidateAppend |
| |
| // State for optional validation checking (doubleCheck mode): |
| processed map[ir.Node]int // count of times each node was processed, for doubleCheck mode |
| doubleCheck bool // whether to do doubleCheck mode |
| } |
| |
| // candidateSlice tracks information about a declared slice |
| // that might be unaliased. |
| type candidateSlice struct { |
| loopDepth int // depth of loop when slice was declared |
| } |
| |
| // candidateAppend tracks information about an OAPPEND that |
| // might be using an unaliased slice. |
| type candidateAppend struct { |
| s *ir.Name // the slice argument in 's = append(s, ...)' |
| call *ir.CallExpr // the append call |
| } |
| |
| // aliasAnalysis looks for specific patterns of slice usage and proves |
| // that certain appends are operating on non-aliased slices. |
| // |
| // This allows us to emit calls to free the backing arrays for certain |
| // non-aliased slices at runtime when we know the memory is logically dead. |
| // |
| // The analysis is conservative, giving up on any operation we do not |
| // explicitly understand. |
| func (aa *aliasAnalysis) analyze(fn *ir.Func) { |
| // Walk the function body to discover slice declarations, their uses, |
| // and any append that we can prove is using an unaliased slice. |
| // |
| // An example is: |
| // |
| // var s []T |
| // for _, v := range input { |
| // f() |
| // s = append(s, g(v)) // s cannot be aliased here |
| // h() |
| // } |
| // return s |
| // |
| // Here, we can prove that the append to s is operating on an unaliased slice, |
| // and that conclusion is unaffected by s later being returned and escaping. |
| // |
| // In contrast, in this example, the aliasing of s in the loop body means the |
| // append can be operating on an aliased slice, so we do not record s as unaliased: |
| // |
| // var s []T |
| // var alias []T |
| // for _, v := range input { |
| // s = append(s, v) // s is aliased on second pass through loop body |
| // alias = s |
| // } |
| // |
| // Arbitrary uses of s after an append do not affect the aliasing conclusion |
| // for that append, but only if the append cannot be revisited at execution time |
| // via a loop or goto. |
| // |
| // We track the loop depth when a slice was declared and verify all uses of a slice |
| // are non-aliasing until we return to that depth. In other words, we make sure |
| // we have processed any possible execution-time revisiting of the slice prior |
| // to making our final determination. |
| // |
| // This approach helps for example with nested loops, such as: |
| // |
| // var s []int |
| // for range 10 { |
| // for range 10 { |
| // s = append(s, 0) // s is proven as non-aliased here |
| // } |
| // } |
| // alias = s // both loops are complete |
| // |
| // Or in contrast: |
| // |
| // var s []int |
| // for range 10 { |
| // for range 10 { |
| // s = append(s, 0) // s is treated as aliased here |
| // } |
| // alias = s // aliased, and outermost loop cycles back |
| // } |
| // |
| // As we walk the function, we look for things like: |
| // |
| // 1. Slice declarations (currently supporting 'var s []T', 's := make([]T, ...)', |
| // and 's := []T{...}'). |
| // 2. Appends to a slice of the form 's = append(s, ...)'. |
| // 3. Other uses of the slice, which we treat as potential aliasing outside |
| // of a few known safe cases. |
| // 4. A start of a loop, which we track in a stack so that |
| // any uses of a slice within a loop body are treated as potential |
| // aliasing, including statements in the loop body after an append. |
| // Candidate appends are stored in the loop stack at the loop depth of their |
| // corresponding slice declaration (rather than the loop depth of the append), |
| // which essentially postpones a decision about the candidate append. |
| // 5. An end of a loop, which pops the loop stack and allows us to |
| // conclusively treat candidate appends from the loop body based |
| // on the loop depth of the slice declaration. |
| // |
| // Note that as we pop a candidate append at the end of a loop, we know |
| // its corresponding slice was unaliased throughout the loop being popped |
| // if the slice is still in the candidate slice map (without having been |
| // removed for potential aliasing), and we know we can make a final decision |
| // about a candidate append if we have returned to the loop depth |
| // where its slice was declared. In other words, there is no unanalyzed |
| // control flow that could take us back at execution-time to the |
| // candidate append in the now analyzed loop. This helps for example |
| // with nested loops, such as in our examples just above. |
| // |
| // We give up on a particular candidate slice if we see any use of it |
| // that we don't explicitly understand, and we give up on all of |
| // our candidate slices if we see any goto or label, which could be |
| // unstructured control flow. (TODO(thepudds): we remove the goto/label |
| // restriction in a subsequent CL.) |
| // |
| // Note that the intended use is to indicate that a slice is safe to pass |
| // to runtime.freegc, which currently requires that the passed pointer |
| // point to the base of its heap object. |
| // |
| // Therefore, we currently do not allow any re-slicing of the slice, though we could |
| // potentially allow s[0:x] or s[:x] or similar. (Slice expressions that alter |
| // the capacity might be possible to allow with freegc changes, though they are |
| // currently disallowed here like all slice expressions). |
| // |
| // TODO(thepudds): we could support the slice being used as non-escaping function call parameter |
| // but to do that, we need to verify any creation of specials via user code triggers an escape, |
| // or mail better runtime.freegc support for specials, or have a temporary compile-time solution |
| // for specials. (Currently, this analysis side-steps specials because any use of a slice |
| // that might cause a user-created special will cause it to be treated as aliased, and |
| // separately, runtime.freegc handles profiling-related specials). |
| |
| // Initialize. |
| aa.fn = fn |
| aa.candidateSlices = make(map[*ir.Name]candidateSlice) // slices that might be unaliased |
| |
| // doubleCheck controls whether we do a sanity check of our processing logic |
| // by counting each node visited in our main pass, and then comparing those counts |
| // against a simple walk at the end. The main intent is to help catch missing |
| // any nodes squirreled away in some spot we forgot to examine in our main pass. |
| aa.doubleCheck = base.Debug.EscapeAliasCheck > 0 |
| aa.processed = make(map[ir.Node]int) |
| |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(fn.Pos(), fn, "====== starting func", "======") |
| } |
| |
| ir.DoChildren(fn, aa.visit) |
| |
| for _, call := range aa.noAliasAppends { |
| if base.Debug.EscapeAlias >= 1 { |
| base.WarnfAt(call.Pos(), "alias analysis: append using non-aliased slice: %v in func %v", |
| call, fn) |
| } |
| if base.Debug.FreeAppend > 0 { |
| call.AppendNoAlias = true |
| } |
| } |
| |
| if aa.doubleCheck { |
| doubleCheckProcessed(fn, aa.processed) |
| } |
| } |
| |
| func (aa *aliasAnalysis) visit(n ir.Node) bool { |
| if n == nil { |
| return false |
| } |
| |
| if base.Debug.EscapeAlias >= 3 { |
| fmt.Printf("%-25s alias analysis: visiting node: %12s %-18T %v\n", |
| fmtPosShort(n.Pos())+":", n.Op().String(), n, n) |
| } |
| |
| // As we visit nodes, we want to ensure we handle all children |
| // without missing any (through ignorance or future changes). |
| // We do this by counting nodes as we visit them or otherwise |
| // declare a node to be fully processed. |
| // |
| // In particular, we want to ensure we don't miss the use |
| // of a slice in some expression that might be an aliasing usage. |
| // |
| // When doubleCheck is enabled, we compare the counts |
| // accumulated in our analysis against counts from a trivial walk, |
| // failing if there is any mismatch. |
| // |
| // This call here counts that we have visited this node n |
| // via our main visit method. (In contrast, some nodes won't |
| // be visited by the main visit method, but instead will be |
| // manually marked via countProcessed when we believe we have fully |
| // dealt with the node). |
| aa.countProcessed(n) |
| |
| switch n.Op() { |
| case ir.ODCL: |
| decl := n.(*ir.Decl) |
| |
| if decl.X != nil && decl.X.Type().IsSlice() && decl.X.Class == ir.PAUTO { |
| s := decl.X |
| if _, ok := aa.candidateSlices[s]; ok { |
| base.FatalfAt(n.Pos(), "candidate slice already tracked as candidate: %v", s) |
| } |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(n.Pos(), s, "adding candidate slice", "(loop depth: %d)", len(aa.loops)) |
| } |
| aa.candidateSlices[s] = candidateSlice{loopDepth: len(aa.loops)} |
| } |
| // No children aside from the declared ONAME. |
| aa.countProcessed(decl.X) |
| return false |
| |
| case ir.ONAME: |
| |
| // We are seeing a name we have not already handled in another case, |
| // so remove any corresponding candidate slice. |
| if n.Type().IsSlice() { |
| name := n.(*ir.Name) |
| _, ok := aa.candidateSlices[name] |
| if ok { |
| delete(aa.candidateSlices, name) |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(n.Pos(), name, "removing candidate slice", "") |
| } |
| } |
| } |
| // No children. |
| return false |
| |
| case ir.OAS2: |
| n := n.(*ir.AssignListStmt) |
| aa.analyzeAssign(n, n.Lhs, n.Rhs) |
| return false |
| |
| case ir.OAS: |
| assign := n.(*ir.AssignStmt) |
| aa.analyzeAssign(n, []ir.Node{assign.X}, []ir.Node{assign.Y}) |
| return false |
| |
| case ir.OFOR, ir.ORANGE: |
| aa.visitList(n.Init()) |
| |
| if n.Op() == ir.ORANGE { |
| // TODO(thepudds): previously we visited this range expression |
| // in the switch just below, after pushing the loop. This current placement |
| // is more correct, but generate a test or find an example in stdlib or similar |
| // where it matters. (Our current tests do not complain.) |
| aa.visit(n.(*ir.RangeStmt).X) |
| } |
| |
| // Push a new loop. |
| aa.loops = append(aa.loops, nil) |
| |
| // Process the loop. |
| switch n.Op() { |
| case ir.OFOR: |
| forstmt := n.(*ir.ForStmt) |
| aa.visit(forstmt.Cond) |
| aa.visitList(forstmt.Body) |
| aa.visit(forstmt.Post) |
| case ir.ORANGE: |
| rangestmt := n.(*ir.RangeStmt) |
| aa.visit(rangestmt.Key) |
| aa.visit(rangestmt.Value) |
| aa.visitList(rangestmt.Body) |
| default: |
| base.Fatalf("loop not OFOR or ORANGE: %v", n) |
| } |
| |
| // Pop the loop. |
| var candidateAppends []candidateAppend |
| candidateAppends, aa.loops = aa.loops[len(aa.loops)-1], aa.loops[:len(aa.loops)-1] |
| for _, a := range candidateAppends { |
| // We are done with the loop, so we can validate any candidate appends |
| // that have not had their slice removed yet. We know a slice is unaliased |
| // throughout the loop if the slice is still in the candidate slice map. |
| if cs, ok := aa.candidateSlices[a.s]; ok { |
| if cs.loopDepth == len(aa.loops) { |
| // We've returned to the loop depth where the slice was declared and |
| // hence made it all the way through any loops that started after |
| // that declaration. |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(n.Pos(), a.s, "proved non-aliased append", |
| "(completed loop, decl at depth: %d)", cs.loopDepth) |
| } |
| aa.noAliasAppends = append(aa.noAliasAppends, a.call) |
| } else if cs.loopDepth < len(aa.loops) { |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(n.Pos(), a.s, "cannot prove non-aliased append", |
| "(completed loop, decl at depth: %d)", cs.loopDepth) |
| } |
| } else { |
| panic("impossible: candidate slice loopDepth > current loop depth") |
| } |
| } |
| } |
| return false |
| |
| case ir.OLEN, ir.OCAP: |
| n := n.(*ir.UnaryExpr) |
| if n.X.Op() == ir.ONAME { |
| // This does not disqualify a candidate slice. |
| aa.visitList(n.Init()) |
| aa.countProcessed(n.X) |
| } else { |
| ir.DoChildren(n, aa.visit) |
| } |
| return false |
| |
| case ir.OCLOSURE: |
| // Give up on all our in-progress slices. |
| closure := n.(*ir.ClosureExpr) |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(n.Pos(), closure.Func, "clearing all in-progress slices due to OCLOSURE", |
| "(was %d in-progress slices)", len(aa.candidateSlices)) |
| } |
| clear(aa.candidateSlices) |
| return ir.DoChildren(n, aa.visit) |
| |
| case ir.OLABEL, ir.OGOTO: |
| // Give up on all our in-progress slices. |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(n.Pos(), n, "clearing all in-progress slices due to label or goto", |
| "(was %d in-progress slices)", len(aa.candidateSlices)) |
| } |
| clear(aa.candidateSlices) |
| return false |
| |
| default: |
| return ir.DoChildren(n, aa.visit) |
| } |
| } |
| |
| func (aa *aliasAnalysis) visitList(nodes []ir.Node) { |
| for _, n := range nodes { |
| aa.visit(n) |
| } |
| } |
| |
| // analyzeAssign evaluates the assignment dsts... = srcs... |
| // |
| // assign is an *ir.AssignStmt or *ir.AssignListStmt. |
| func (aa *aliasAnalysis) analyzeAssign(assign ir.Node, dsts, srcs []ir.Node) { |
| aa.visitList(assign.Init()) |
| for i := range dsts { |
| dst := dsts[i] |
| src := srcs[i] |
| |
| if dst.Op() != ir.ONAME || !dst.Type().IsSlice() { |
| // Nothing for us to do aside from visiting the remaining children. |
| aa.visit(dst) |
| aa.visit(src) |
| continue |
| } |
| |
| // We have a slice being assigned to an ONAME. |
| |
| // Check for simple zero value assignments to an ONAME, which we ignore. |
| if src == nil { |
| aa.countProcessed(dst) |
| continue |
| } |
| |
| if base.Debug.EscapeAlias >= 4 { |
| srcfn := "" |
| if src.Op() == ir.ONAME { |
| srcfn = fmt.Sprintf("%v.", src.Name().Curfn) |
| } |
| aa.diag(assign.Pos(), assign, "visiting slice assignment", "%v.%v = %s%v (%s %T = %s %T)", |
| dst.Name().Curfn, dst, srcfn, src, dst.Op().String(), dst, src.Op().String(), src) |
| } |
| |
| // Now check what we have on the RHS. |
| switch src.Op() { |
| // Cases: |
| |
| // Check for s := make([]T, ...) or s := []T{...}, along with the '=' version |
| // of those which does not alias s as long as s is not used in the make. |
| // |
| // TODO(thepudds): we need to be sure that 's := []T{1,2,3}' does not end up backed by a |
| // global static. Ad-hoc testing indicates that example and similar seem to be |
| // stack allocated, but that was not exhaustive testing. We do have runtime.freegc |
| // able to throw if it finds a global static, but should test more. |
| // |
| // TODO(thepudds): could also possibly allow 's := append([]T(nil), ...)' |
| // and 's := append([]T{}, ...)'. |
| case ir.OMAKESLICE, ir.OSLICELIT: |
| name := dst.(*ir.Name) |
| if name.Class == ir.PAUTO { |
| if base.Debug.EscapeAlias > 1 { |
| aa.diag(assign.Pos(), assign, "assignment from make or slice literal", "") |
| } |
| // If this is Def=true, the ODCL in the init will causes this to be tracked |
| // as a candidate slice. We walk the init and RHS but avoid visiting the name |
| // in the LHS, which would remove the slice from the candidate list after it |
| // was just added. |
| aa.visit(src) |
| aa.countProcessed(name) |
| continue |
| } |
| |
| // Check for s = append(s, <...>). |
| case ir.OAPPEND: |
| s := dst.(*ir.Name) |
| call := src.(*ir.CallExpr) |
| if call.Args[0] == s { |
| // Matches s = append(s, <...>). |
| // First visit other arguments in case they use s. |
| aa.visitList(call.Args[1:]) |
| // Mark the call as processed, and s twice. |
| aa.countProcessed(s, call, s) |
| |
| // We have now examined all non-ONAME children of assign. |
| |
| // This is now the heart of the analysis. |
| // Check to see if this slice is a live candidate. |
| cs, ok := aa.candidateSlices[s] |
| if ok { |
| if cs.loopDepth == len(aa.loops) { |
| // No new loop has started after the declaration of s, |
| // so this is definitive. |
| if base.Debug.EscapeAlias >= 2 { |
| aa.diag(assign.Pos(), assign, "proved non-aliased append", |
| "(loop depth: %d, equals decl depth)", len(aa.loops)) |
| } |
| aa.noAliasAppends = append(aa.noAliasAppends, call) |
| } else if cs.loopDepth < len(aa.loops) { |
| // A new loop has started since the declaration of s, |
| // so we can't validate this append yet, but |
| // remember it in case we can validate it later when |
| // all loops using s are done. |
| aa.loops[cs.loopDepth] = append(aa.loops[cs.loopDepth], |
| candidateAppend{s: s, call: call}) |
| } else { |
| panic("impossible: candidate slice loopDepth > current loop depth") |
| } |
| } |
| continue |
| } |
| } // End of switch on src.Op(). |
| |
| // Reached bottom of the loop over assignments. |
| // If we get here, we need to visit the dst and src normally. |
| aa.visit(dst) |
| aa.visit(src) |
| } |
| } |
| |
| func (aa *aliasAnalysis) countProcessed(nodes ...ir.Node) { |
| if aa.doubleCheck { |
| for _, n := range nodes { |
| aa.processed[n]++ |
| } |
| } |
| } |
| |
| func (aa *aliasAnalysis) diag(pos src.XPos, n ir.Node, what string, format string, args ...any) { |
| fmt.Printf("%-25s alias analysis: %-30s %-20s %s\n", |
| fmtPosShort(pos)+":", |
| what+":", |
| fmt.Sprintf("%v", n), |
| fmt.Sprintf(format, args...)) |
| } |
| |
| // doubleCheckProcessed does a sanity check for missed nodes in our visit. |
| func doubleCheckProcessed(fn *ir.Func, processed map[ir.Node]int) { |
| // Do a trivial walk while counting the nodes |
| // to compare against the counts in processed. |
| |
| observed := make(map[ir.Node]int) |
| var walk func(n ir.Node) bool |
| walk = func(n ir.Node) bool { |
| observed[n]++ |
| return ir.DoChildren(n, walk) |
| } |
| ir.DoChildren(fn, walk) |
| |
| if !maps.Equal(processed, observed) { |
| // The most likely mistake might be something was missed while building processed, |
| // so print extra details in that direction. |
| for n, observedCount := range observed { |
| processedCount, ok := processed[n] |
| if processedCount != observedCount || !ok { |
| base.WarnfAt(n.Pos(), |
| "alias analysis: mismatch for %T: %v: processed %d times, observed %d times", |
| n, n, processedCount, observedCount) |
| } |
| } |
| base.FatalfAt(fn.Pos(), "alias analysis: mismatch in visited nodes") |
| } |
| } |
| |
| func fmtPosShort(xpos src.XPos) string { |
| // TODO(thepudds): I think I did this a simpler way a while ago? Or maybe add base.FmtPosShort |
| // or similar? Or maybe just use base.FmtPos and give up on nicely aligned log messages? |
| pos := base.Ctxt.PosTable.Pos(xpos) |
| shortLine := filepath.Base(pos.AbsFilename()) + ":" + pos.LineNumber() |
| return shortLine |
| } |