From 27f8ec17fc6c8ec2120eafa039b7609f2c60150b Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:22:20 -0700 Subject: [PATCH] transform: follow aggregate returned alloc aliases Track whether an allocation reaches a callee return separately from the instruction where it escapes. The extra result state is needed because LLVM's returned parameter attribute only covers scalar returns: a slice helper returns its data pointer inside a {ptr, len, cap} aggregate, so the alias is only visible by walking insertvalue and ret uses in the callee. This lets OptimizeAllocs keep the backing array for returnIntSlice(s) on the stack when the returned slice is ignored, matching gc's escape decision for the same pattern. The golden output still keeps the escaping returned-slice case on the heap. --- transform/allocs.go | 81 ++++++++++++++++++--------- transform/testdata/allocs2.out.cover | 1 - transform/testdata/allocs2.out.reason | 3 +- 3 files changed, 57 insertions(+), 28 deletions(-) diff --git a/transform/allocs.go b/transform/allocs.go index 04dd988e6..5c1021a3f 100644 --- a/transform/allocs.go +++ b/transform/allocs.go @@ -162,21 +162,41 @@ func FormatAllocCover(pos token.Position) string { // valueEscapesAt returns the instruction where the given value may escape and a // nil llvm.Value if it definitely doesn't. The value must be an instruction. func valueEscapesAt(value llvm.Value) llvm.Value { - return valueEscapesAtImpl(value, false, nil) + return valueEscapesAtImpl(value, false, nil).escapeAt } -func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) llvm.Value { +type escapeResult struct { + escapeAt llvm.Value + + // returned is separate from escapeAt because values can flow to a return + // through aggregate operations. LLVM can mark a scalar returned parameter, + // but a slice data pointer returned inside {ptr, len, cap} is only visible + // after walking insertvalue/ret uses in the callee. + returned bool +} + +func (r *escapeResult) merge(other escapeResult) bool { + if !other.escapeAt.IsNil() { + r.escapeAt = other.escapeAt + return false + } + r.returned = r.returned || other.returned + return true +} + +func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) escapeResult { if visiting == nil { visiting = make(map[llvm.Value]struct{}) } if _, ok := visiting[value]; ok { // Recursive call graph while following returned parameters. Treat this // as escaping to keep the analysis conservative and bounded. - return value + return escapeResult{escapeAt: value} } visiting[value] = struct{}{} defer delete(visiting, value) + var result escapeResult uses := getUses(value) for _, use := range uses { if use.IsAInstruction().IsNil() { @@ -184,13 +204,23 @@ func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Va } switch use.InstructionOpcode() { case llvm.GetElementPtr: - if at := valueEscapesAtImpl(use, allowReturn, visiting); !at.IsNil() { - return at + if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) { + return result } case llvm.BitCast: // A bitcast escapes if the casted-to value escapes. - if at := valueEscapesAtImpl(use, allowReturn, visiting); !at.IsNil() { - return at + if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) { + return result + } + case llvm.InsertValue: + if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) { + return result + } + case llvm.ExtractValue: + if use.Type().TypeKind() == llvm.PointerTypeKind { + if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) { + return result + } } case llvm.Load: // Load does not escape. @@ -198,41 +228,42 @@ func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Va // Store only escapes when the value is stored to, not when the // value is stored into another value. if use.Operand(0) == value { - return use + return escapeResult{escapeAt: use} } case llvm.Call: - if at := callValueEscapesAt(use, value, allowReturn, visiting); !at.IsNil() { - return at + if !result.merge(callValueEscapesAt(use, value, allowReturn, visiting)) { + return result } case llvm.ICmp: // Comparing pointers don't let the pointer escape. // This is often a compiler-inserted nil check. case llvm.Ret: if !allowReturn || use.Operand(0) != value { - return use + return escapeResult{escapeAt: use} } + result.returned = true default: // Unknown instruction, might escape. - return use + return escapeResult{escapeAt: use} } } // Checked all uses, and none let the pointer value escape. - return llvm.Value{} + return result } // callValueEscapesAt returns whether value escapes through this call. It also // handles calls that return value unchanged, as long as the called function does // not otherwise capture the parameter and the returned alias does not escape. -func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) llvm.Value { +func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) escapeResult { called := call.CalledValue() if called.IsAFunction().IsNil() { - return call + return escapeResult{escapeAt: call} } kindNoCapture := llvm.AttributeKindID("nocapture") kindReturned := llvm.AttributeKindID("returned") matched := false - returned := false + var result escapeResult for i := 0; i < called.ParamsCount(); i++ { if call.Operand(i) != value { continue @@ -242,30 +273,30 @@ func callValueEscapesAt(call, value llvm.Value, allowReturn bool, visiting map[l nocapture := !called.GetEnumAttributeAtIndex(index, kindNoCapture).IsNil() returnedParam := !called.GetEnumAttributeAtIndex(index, kindReturned).IsNil() if returnedParam { - returned = true + result.returned = true } if nocapture { continue } - if !returnedParam || called.IsDeclaration() { - return call + if called.IsDeclaration() { + return escapeResult{escapeAt: call} } - if at := valueEscapesAtImpl(called.Param(i), true, visiting); !at.IsNil() { - return at + if !result.merge(valueEscapesAtImpl(called.Param(i), true, visiting)) { + return result } } for i := called.ParamsCount(); i < call.OperandsCount(); i++ { if call.Operand(i) == value { - return call + return escapeResult{escapeAt: call} } } if !matched { - return llvm.Value{} + return escapeResult{} } - if returned { + if result.returned { return valueEscapesAtImpl(call, allowReturn, visiting) } - return llvm.Value{} + return escapeResult{} } func lineLengthAt(filename string, lineNumber int) int { diff --git a/transform/testdata/allocs2.out.cover b/transform/testdata/allocs2.out.cover index 4a45ae0d7..afd5ed7bb 100644 --- a/transform/testdata/allocs2.out.cover +++ b/transform/testdata/allocs2.out.cover @@ -1,4 +1,3 @@ -testdata/allocs2.go:21.1,21.22 1 0 testdata/allocs2.go:24.1,24.43 1 0 testdata/allocs2.go:26.1,26.25 1 0 testdata/allocs2.go:29.1,29.22 1 0 diff --git a/transform/testdata/allocs2.out.reason b/transform/testdata/allocs2.out.reason index 6bb45f623..adc83392d 100644 --- a/transform/testdata/allocs2.out.reason +++ b/transform/testdata/allocs2.out.reason @@ -1,4 +1,3 @@ -testdata/allocs2.go:21:12: object allocated on the heap: escapes at line 22 testdata/allocs2.go:24:15: object allocated on the heap: size is not constant testdata/allocs2.go:26:12: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256 testdata/allocs2.go:29:12: object allocated on the heap: escapes at line 30 @@ -9,4 +8,4 @@ testdata/allocs2.go:49:2: object allocated on the heap: escapes at line 51 testdata/allocs2.go:50:2: object allocated on the heap: escapes at line 51 testdata/allocs2.go:107:11: object allocated on the heap: escapes at line 108 testdata/allocs2.go:114:2: object allocated on the heap: escapes at line 117 -testdata/allocs2.go:128:2: object allocated on the heap: escapes at line 130 +testdata/allocs2.go:128:2: object allocated on the heap: escapes at unknown line