From 440604b111c5929419fa3ab7510c699de3d70880 Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:21:53 -0700 Subject: [PATCH 1/3] transform: add allocation alias regression cases Add allocation diagnostics coverage for pointer-returning helpers, aggregate slice returns, and conditional pointer returns before changing the allocation optimizer. The golden files record the current heap-allocation behavior so later commits show exactly which diagnostics each optimizer improvement removes. --- transform/testdata/allocs2.go | 57 ++++++++++++++++++++++++++- transform/testdata/allocs2.out.cover | 24 ++++++----- transform/testdata/allocs2.out.reason | 24 ++++++----- 3 files changed, 83 insertions(+), 22 deletions(-) diff --git a/transform/testdata/allocs2.go b/transform/testdata/allocs2.go index 35ab7bf3fe..88c411493a 100644 --- a/transform/testdata/allocs2.go +++ b/transform/testdata/allocs2.go @@ -9,7 +9,6 @@ func main() { n1 := 5 derefInt(&n1) - // This should eventually be modified to not escape. n2 := 6 returnIntPtr(&n2) @@ -19,7 +18,6 @@ func main() { s2 := [3]int{} readIntSlice(s2[:]) - // This should also be modified to not escape. s3 := make([]int, 3) returnIntSlice(s3) @@ -78,6 +76,61 @@ func main() { keepAliveNoEscape(unsafe.Pointer(&dmaBuf2[0])) } +type vector3 [3]float32 + +func scaleVector3(vec *vector3, f float32) *vector3 { + vec[0] *= f + vec[1] *= f + vec[2] *= f + return vec +} + +func crossVector3(a, b *vector3) vector3 { + return vector3{ + a[1]*b[2] - a[2]*b[1], + a[2]*b[0] - a[0]*b[2], + a[0]*b[1] - a[1]*b[0], + } +} + +func nonEscapingReturnedPointer() vector3 { + a := vector3{1, 2, 3} + b := vector3{4, 5, 6} + + c := scaleVector3(&b, 0.5) + return crossVector3(&a, c) +} + +var escapedSlice []int + +func escapingReturnedSlice() { + s := make([]int, 3) + escapedSlice = returnIntSlice(s) +} + +var escapedVector3 *vector3 + +func escapingReturnedPointer() { + b := vector3{4, 5, 6} + + c := scaleVector3(&b, 0.5) + escapedVector3 = c +} + +func recursiveScaleVector3(vec *vector3, n int) *vector3 { + if n == 0 { + return vec + } + return recursiveScaleVector3(vec, n-1) +} + +func recursiveReturnedPointer() vector3 { + b := vector3{4, 5, 6} + + c := recursiveScaleVector3(&b, 1) + return *c +} + func derefInt(x *int) int { return *x } diff --git a/transform/testdata/allocs2.out.cover b/transform/testdata/allocs2.out.cover index a26c2a81d0..2d42ec7c82 100644 --- a/transform/testdata/allocs2.out.cover +++ b/transform/testdata/allocs2.out.cover @@ -1,10 +1,14 @@ -testdata/allocs2.go:13.1,13.9 1 0 -testdata/allocs2.go:23.1,23.22 1 0 -testdata/allocs2.go:26.1,26.43 1 0 -testdata/allocs2.go:28.1,28.25 1 0 -testdata/allocs2.go:31.1,31.22 1 0 -testdata/allocs2.go:38.1,38.23 1 0 -testdata/allocs2.go:46.1,46.23 1 0 -testdata/allocs2.go:48.1,48.22 1 0 -testdata/allocs2.go:51.1,51.9 1 0 -testdata/allocs2.go:52.1,52.9 1 0 +testdata/allocs2.go:12.1,12.9 1 0 +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 +testdata/allocs2.go:36.1,36.23 1 0 +testdata/allocs2.go:44.1,44.23 1 0 +testdata/allocs2.go:46.1,46.22 1 0 +testdata/allocs2.go:49.1,49.9 1 0 +testdata/allocs2.go:50.1,50.9 1 0 +testdata/allocs2.go:98.1,98.23 1 0 +testdata/allocs2.go:107.1,107.21 1 0 +testdata/allocs2.go:114.1,114.23 1 0 +testdata/allocs2.go:128.1,128.23 1 0 diff --git a/transform/testdata/allocs2.out.reason b/transform/testdata/allocs2.out.reason index 22f798e4be..82c2efd973 100644 --- a/transform/testdata/allocs2.out.reason +++ b/transform/testdata/allocs2.out.reason @@ -1,10 +1,14 @@ -testdata/allocs2.go:13:2: object allocated on the heap: escapes at line 14 -testdata/allocs2.go:23:12: object allocated on the heap: escapes at line 24 -testdata/allocs2.go:26:15: object allocated on the heap: size is not constant -testdata/allocs2.go:28:12: object allocated on the heap: object size 300 exceeds maximum stack allocation size 256 -testdata/allocs2.go:31:12: object allocated on the heap: escapes at line 32 -testdata/allocs2.go:38:21: object allocated on the heap: escapes at line 39 -testdata/allocs2.go:46:22: object allocated on the heap: escapes at line 46 -testdata/allocs2.go:48:13: object allocated on the heap: escapes at line 49 -testdata/allocs2.go:51:2: object allocated on the heap: escapes at line 53 -testdata/allocs2.go:52:2: object allocated on the heap: escapes at line 53 +testdata/allocs2.go:12:2: object allocated on the heap: escapes at line 13 +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 +testdata/allocs2.go:36:21: object allocated on the heap: escapes at line 37 +testdata/allocs2.go:44:22: object allocated on the heap: escapes at line 44 +testdata/allocs2.go:46:13: object allocated on the heap: escapes at line 47 +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:98:2: object allocated on the heap: escapes at line 100 +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 116 +testdata/allocs2.go:128:2: object allocated on the heap: escapes at line 130 From 44b55f7cc83480d8feb9e6978d7bfc8032b6cb7d Mon Sep 17 00:00:00 2001 From: Jake Bailey <5341706+jakebailey@users.noreply.github.com> Date: Fri, 12 Jun 2026 11:22:07 -0700 Subject: [PATCH 2/3] transform: handle returned pointer alloc aliases Follow returned pointer aliases when deciding whether runtime.alloc calls can be lowered to stack allocations. A returned parameter is not the same as nocapture: it still flows back to the caller and must be checked as an alias of the original allocation. Keep the analysis conservative for recursive returned-parameter chains and unknown operands. The existing golden test now shows that the non-escaping returned pointer cases no longer require heap allocation. --- transform/allocs.go | 74 +++++++++++++++++++++++++-- transform/testdata/allocs2.out.cover | 2 - transform/testdata/allocs2.out.reason | 4 +- 3 files changed, 71 insertions(+), 9 deletions(-) diff --git a/transform/allocs.go b/transform/allocs.go index 477afa0a3c..04dd988e6e 100644 --- a/transform/allocs.go +++ b/transform/allocs.go @@ -162,6 +162,21 @@ 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) +} + +func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Value]struct{}) llvm.Value { + 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 + } + visiting[value] = struct{}{} + defer delete(visiting, value) + uses := getUses(value) for _, use := range uses { if use.IsAInstruction().IsNil() { @@ -169,12 +184,12 @@ func valueEscapesAt(value llvm.Value) llvm.Value { } switch use.InstructionOpcode() { case llvm.GetElementPtr: - if at := valueEscapesAt(use); !at.IsNil() { + if at := valueEscapesAtImpl(use, allowReturn, visiting); !at.IsNil() { return at } case llvm.BitCast: // A bitcast escapes if the casted-to value escapes. - if at := valueEscapesAt(use); !at.IsNil() { + if at := valueEscapesAtImpl(use, allowReturn, visiting); !at.IsNil() { return at } case llvm.Load: @@ -186,12 +201,16 @@ func valueEscapesAt(value llvm.Value) llvm.Value { return use } case llvm.Call: - if !hasFlag(use, value, "nocapture") { - return use + if at := callValueEscapesAt(use, value, allowReturn, visiting); !at.IsNil() { + return at } 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 + } default: // Unknown instruction, might escape. return use @@ -202,6 +221,53 @@ func valueEscapesAt(value llvm.Value) llvm.Value { return llvm.Value{} } +// 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 { + called := call.CalledValue() + if called.IsAFunction().IsNil() { + return call + } + kindNoCapture := llvm.AttributeKindID("nocapture") + kindReturned := llvm.AttributeKindID("returned") + matched := false + returned := false + for i := 0; i < called.ParamsCount(); i++ { + if call.Operand(i) != value { + continue + } + matched = true + index := i + 1 // param attributes start at 1 + nocapture := !called.GetEnumAttributeAtIndex(index, kindNoCapture).IsNil() + returnedParam := !called.GetEnumAttributeAtIndex(index, kindReturned).IsNil() + if returnedParam { + returned = true + } + if nocapture { + continue + } + if !returnedParam || called.IsDeclaration() { + return call + } + if at := valueEscapesAtImpl(called.Param(i), true, visiting); !at.IsNil() { + return at + } + } + for i := called.ParamsCount(); i < call.OperandsCount(); i++ { + if call.Operand(i) == value { + return call + } + } + if !matched { + return llvm.Value{} + } + if returned { + return valueEscapesAtImpl(call, allowReturn, visiting) + } + return llvm.Value{} +} + func lineLengthAt(filename string, lineNumber int) int { f, err := os.Open(filename) if err != nil { diff --git a/transform/testdata/allocs2.out.cover b/transform/testdata/allocs2.out.cover index 2d42ec7c82..4a45ae0d72 100644 --- a/transform/testdata/allocs2.out.cover +++ b/transform/testdata/allocs2.out.cover @@ -1,4 +1,3 @@ -testdata/allocs2.go:12.1,12.9 1 0 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 @@ -8,7 +7,6 @@ testdata/allocs2.go:44.1,44.23 1 0 testdata/allocs2.go:46.1,46.22 1 0 testdata/allocs2.go:49.1,49.9 1 0 testdata/allocs2.go:50.1,50.9 1 0 -testdata/allocs2.go:98.1,98.23 1 0 testdata/allocs2.go:107.1,107.21 1 0 testdata/allocs2.go:114.1,114.23 1 0 testdata/allocs2.go:128.1,128.23 1 0 diff --git a/transform/testdata/allocs2.out.reason b/transform/testdata/allocs2.out.reason index 82c2efd973..6bb45f623e 100644 --- a/transform/testdata/allocs2.out.reason +++ b/transform/testdata/allocs2.out.reason @@ -1,4 +1,3 @@ -testdata/allocs2.go:12:2: object allocated on the heap: escapes at line 13 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 @@ -8,7 +7,6 @@ testdata/allocs2.go:44:22: object allocated on the heap: escapes at line 44 testdata/allocs2.go:46:13: object allocated on the heap: escapes at line 47 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:98:2: object allocated on the heap: escapes at line 100 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 116 +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 From bd6b10efb512540d5f884586c608fb5114958572 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 3/3] 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 04dd988e6e..5c1021a3fb 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 4a45ae0d72..afd5ed7bbf 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 6bb45f623e..adc83392dd 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