diff --git a/testdata/calls.go b/testdata/calls.go index fb681366fb..c28f4d6144 100644 --- a/testdata/calls.go +++ b/testdata/calls.go @@ -1,5 +1,7 @@ package main +import "runtime/volatile" + type Thing struct { name string } @@ -87,6 +89,180 @@ func main() { if testDeferElse(false) != 0 { println("else defer returned wrong value") } + + // See https://github.com/tinygo-org/tinygo/pull/5776. + err := multiReturnEscape() + clobberStack() + println("multiple return values:", err.(*multiReturnErr).x[1]) + err = singleArrayEscape() + clobberStack() + println("single array return:", err.(*multiReturnErr).x[1]) + err = forwardedReturnEscape() + clobberStack() + println("forwarded return:", err.(*multiReturnErr).x[1]) + s := sliceReturnEscape() + clobberStack() + println("slice return:", s[1]) + globalReturnEscape() + clobberStack() + println("global escape:", globalEscaped.(*multiReturnErr).x[1]) + fn := closureReturnEscape() + clobberStack() + println("closure escape:", fn()) + var phiCond uint32 + err = phiReturnEscape(volatile.LoadUint32(&phiCond) != 0) + clobberStack() + println("phi escape:", err.(*multiReturnErr).x[1]) + volatile.StoreUint32(&phiCond, 1) + println("phi nil:", phiReturnEscape(volatile.LoadUint32(&phiCond) != 0) == nil) + g := interfaceReturnEscape() + clobberStack() + println("interface return:", g.get()) + err = deepForwardEscape() + clobberStack() + println("deep forward:", err.(*multiReturnErr).x[1]) +} + +type multiReturnErr struct{ x [4]int } + +func (e *multiReturnErr) Error() string { return "multiReturnErr" } + +//go:noinline +func wrapMultiReturn(e *multiReturnErr) (int, error) { return 1, e } + +//go:noinline +func multiReturnEscape() error { + e := &multiReturnErr{} + e.x[1] = 42 + _, err := wrapMultiReturn(e) + return err +} + +//go:noinline +func wrapSingleArrayReturn(e *multiReturnErr) [1]error { return [1]error{e} } + +//go:noinline +func singleArrayEscape() error { + e := &multiReturnErr{} + e.x[1] = 43 + a := wrapSingleArrayReturn(e) + return a[0] +} + +//go:noinline +func forwardMultiReturn(e *multiReturnErr) error { + _, err := wrapMultiReturn(e) + return err +} + +//go:noinline +func forwardedReturnEscape() error { + e := &multiReturnErr{} + e.x[1] = 44 + return forwardMultiReturn(e) +} + +//go:noinline +func wrapSliceReturn(b []int) (int, []int) { return 1, b } + +// The allocation reaches the caller inside the {ptr, len, cap} slice aggregate +// of a multiple return value. +// +//go:noinline +func sliceReturnEscape() []int { + b := make([]int, 4) + b[1] = 45 + _, s := wrapSliceReturn(b) + return s +} + +var globalEscaped error + +// The allocation escapes to a package-level variable rather than through a +// return. +// +//go:noinline +func globalReturnEscape() { + e := &multiReturnErr{} + e.x[1] = 46 + _, err := wrapMultiReturn(e) + globalEscaped = err +} + +// The allocation escapes by being captured in a closure. +// +//go:noinline +func closureReturnEscape() func() int { + e := &multiReturnErr{} + e.x[1] = 47 + _, err := wrapMultiReturn(e) + return func() int { return err.(*multiReturnErr).x[1] } +} + +//go:noinline +func phiReturnEscape(cond bool) error { + e := &multiReturnErr{} + e.x[1] = 48 + _, err := wrapMultiReturn(e) + if cond { + err = nil + } + return err +} + +type multiReturnGetter interface{ get() int } + +func (e *multiReturnErr) get() int { return e.x[1] } + +//go:noinline +func wrapInterfaceReturn(e *multiReturnErr) (int, multiReturnGetter) { return 1, e } + +// The allocation reaches the caller inside a non-error interface value and is +// then called through. +// +//go:noinline +func interfaceReturnEscape() multiReturnGetter { + e := &multiReturnErr{} + e.x[1] = 49 + _, g := wrapInterfaceReturn(e) + return g +} + +//go:noinline +func deepForward1(e *multiReturnErr) (error, int) { + _, err := wrapMultiReturn(e) + return err, 2 +} + +//go:noinline +func deepForward2(e *multiReturnErr) [1]error { + err, _ := deepForward1(e) + return [1]error{err} +} + +//go:noinline +func deepForward3(e *multiReturnErr) error { + a := deepForward2(e) + return a[0] +} + +// The allocation is repacked into a different aggregate shape at each of four +// call levels before reaching the caller. +// +//go:noinline +func deepForwardEscape() error { + e := &multiReturnErr{} + e.x[1] = 50 + return deepForward3(e) +} + +//go:noinline +func clobberStack() { + var a [32]uint32 + for i := range a { + // Keep the stack writes. See https://llvm.org/docs/LangRef.html#volatile-memory-accesses. + volatile.StoreUint32(&a[i], uint32(1000+i)) + } } func runFunc(f func(int), arg int) { diff --git a/testdata/calls.txt b/testdata/calls.txt index 78c07caf4d..cdc786df2e 100644 --- a/testdata/calls.txt +++ b/testdata/calls.txt @@ -16,3 +16,13 @@ thing inside closure: foo inside fp closure: foo 3 Thing.Print: arg: functional args 1 Thing.Print: named thing arg: functional args 2 +multiple return values: 42 +single array return: 43 +forwarded return: 44 +slice return: 45 +global escape: 46 +closure escape: 47 +phi escape: 48 +phi nil: true +interface return: 49 +deep forward: 50 diff --git a/transform/allocs.go b/transform/allocs.go index c4c70be7f3..c646e6f4be 100644 --- a/transform/allocs.go +++ b/transform/allocs.go @@ -218,7 +218,7 @@ func valueEscapesAtImpl(value llvm.Value, allowReturn bool, visiting map[llvm.Va return result } case llvm.ExtractValue: - if use.Type().TypeKind() == llvm.PointerTypeKind { + if typeHasPointers(use.Type()) { if !result.merge(valueEscapesAtImpl(use, allowReturn, visiting)) { return result } diff --git a/transform/allocs_test.go b/transform/allocs_test.go index 83ba90ffe8..f042877175 100644 --- a/transform/allocs_test.go +++ b/transform/allocs_test.go @@ -9,6 +9,7 @@ import ( "strings" "testing" + "github.com/tinygo-org/tinygo/compiler/llvmutil" "github.com/tinygo-org/tinygo/transform" "tinygo.org/x/go-llvm" ) @@ -20,6 +21,144 @@ func TestAllocs(t *testing.T) { }) } +func TestAllocsAggregateEdges(t *testing.T) { + t.Parallel() + + const path = "testdata/allocs-aggregate.ll" + ctx := llvm.NewContext() + defer ctx.Dispose() + ensureTestCacheFreshness(t, path) + buf, err := llvm.NewMemoryBufferFromFile(path) + if err != nil { + t.Fatal(err) + } + mod, err := ctx.ParseIR(buf) + if err != nil { + t.Fatal(err) + } + defer mod.Dispose() + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + + transform.OptimizeAllocs(mod, nil, 256, nil) + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + + for name, wantHeap := range map[string]bool{ + "nestedStructReturn": true, + "nestedArrayReturn": true, + "arrayOfStructReturn": true, + "structOfArrayReturn": true, + "singleArrayReturn": true, + "aggregateStore": true, + "pointerStore": true, + "aggregateCall": true, + "unknownCall": true, + "indirectCall": true, + "repackReturn": true, + "forwardReturn": true, + "forwardLoad": false, + "gepReturn": true, + "gepLoad": false, + "aggregatePhi": true, + "aggregateSelect": true, + "recursiveReturn": true, + "duplicateArguments": true, + "nonEscapingScalar": false, + "nonEscapingLoad": false, + "nonEscapingNilCheck": false, + "nonEscapingDiscard": false, + "nonEscapingCall": false, + "pointerFreeStruct": false, + "pointerFreeArray": false, + "emptyPointerArray": false, + } { + t.Run(name, func(t *testing.T) { + fn := mod.NamedFunction(name) + if fn.IsNil() { + t.Fatal("function not found") + } + var heap, stack int + for bb := fn.FirstBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) { + for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if !inst.IsACallInst().IsNil() && inst.CalledValue() == mod.NamedFunction("runtime.alloc") { + heap++ + } + if !inst.IsAAllocaInst().IsNil() { + stack++ + } + } + } + want := 0 + if wantHeap { + want = 1 + } + if heap != want || stack != 1-want { + t.Errorf("got %d heap and %d stack allocations, want %d and %d:\n%s", heap, stack, want, 1-want, fn.String()) + } + }) + } +} + +func TestAllocsRuntimePhi(t *testing.T) { + t.Parallel() + + mod := compileGoFileForTesting(t, "../testdata/calls.go") + defer mod.Context().Dispose() + defer mod.Dispose() + po := llvm.NewPassBuilderOptions() + defer po.Dispose() + passes := "globalopt,ipsccp,instcombine,adce,function-attrs" + if llvmutil.Version() >= 18 { + passes = "globalopt,ipsccp,instcombine,adce,function-attrs" + } + if err := mod.RunPasses(passes, llvm.TargetMachine{}, po); err != nil { + t.Fatal(err) + } + fn := mod.NamedFunction("main.phiReturnEscape") + if fn.IsNil() { + t.Fatal("phiReturnEscape not found") + } + var returnedPhi llvm.Value + for bb := fn.FirstBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) { + for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if !inst.IsAReturnInst().IsNil() && !inst.Operand(0).IsAPHINode().IsNil() { + returnedPhi = inst.Operand(0) + } + } + } + if returnedPhi.IsNil() { + t.Fatalf("runtime regression does not return a phi before escape analysis:\n%s", fn.String()) + } + var extracted bool + for i := 0; i < returnedPhi.IncomingCount(); i++ { + if !returnedPhi.IncomingValue(i).IsAExtractValueInst().IsNil() { + extracted = true + } + } + if !extracted { + t.Fatalf("returned phi does not merge an extracted aggregate:\n%s", fn.String()) + } + + transform.OptimizeAllocs(mod, nil, 256, nil) + if err := llvm.VerifyModule(mod, llvm.ReturnStatusAction); err != nil { + t.Fatal(err) + } + var allocations int + for bb := fn.FirstBasicBlock(); !bb.IsNil(); bb = llvm.NextBasicBlock(bb) { + for inst := bb.FirstInstruction(); !inst.IsNil(); inst = llvm.NextInstruction(inst) { + if !inst.IsACallInst().IsNil() && inst.CalledValue() == mod.NamedFunction("runtime.alloc") { + allocations++ + } + } + } + if allocations != 1 { + t.Fatalf("got %d heap allocations, want 1:\n%s", allocations, fn.String()) + } +} + // Test with a Go file as input (for more accurate tests). func TestAllocs2(t *testing.T) { t.Parallel() diff --git a/transform/gc.go b/transform/gc.go index d5b08dabf4..824aa235b2 100644 --- a/transform/gc.go +++ b/transform/gc.go @@ -372,7 +372,7 @@ func makeGCGlobalRoots(mod llvm.Module) bool { if strings.HasPrefix(global.Name(), "llvm.") || global.IsGlobalConstant() || global.Initializer().IsNil() || - !gcTypeHasPointers(global.GlobalValueType()) { + !typeHasPointers(global.GlobalValueType()) { continue } roots = appendGCGlobalRootRanges(roots, global, global.GlobalValueType(), targetData, ctx.Int8Type(), uintptrType) @@ -483,14 +483,14 @@ func appendGCGlobalRootOffsets(offsets []uint64, typ llvm.Type, targetData llvm. return append(offsets, baseOffset) case llvm.StructTypeKind: for i, fieldType := range typ.StructElementTypes() { - if gcTypeHasPointers(fieldType) { + if typeHasPointers(fieldType) { fieldOffset := targetData.ElementOffset(typ, i) offsets = appendGCGlobalRootOffsets(offsets, fieldType, targetData, baseOffset+fieldOffset) } } case llvm.ArrayTypeKind: elemType := typ.ElementType() - if gcTypeHasPointers(elemType) { + if typeHasPointers(elemType) { elemSize := targetData.TypeAllocSize(elemType) for i := 0; i < typ.ArrayLength(); i++ { offsets = appendGCGlobalRootOffsets(offsets, elemType, targetData, baseOffset+uint64(i)*elemSize) @@ -500,22 +500,6 @@ func appendGCGlobalRootOffsets(offsets []uint64, typ llvm.Type, targetData llvm. return offsets } -func gcTypeHasPointers(typ llvm.Type) bool { - switch typ.TypeKind() { - case llvm.PointerTypeKind: - return true - case llvm.StructTypeKind: - for _, field := range typ.StructElementTypes() { - if gcTypeHasPointers(field) { - return true - } - } - case llvm.ArrayTypeKind: - return typ.ArrayLength() != 0 && gcTypeHasPointers(typ.ElementType()) - } - return false -} - // markParentFunctions traverses all parent function calls (recursively) and // adds them to the set of marked functions. It only considers function calls: // any other uses of such a function is ignored. diff --git a/transform/llvm.go b/transform/llvm.go index 8bb3749a6c..9575ca3fec 100644 --- a/transform/llvm.go +++ b/transform/llvm.go @@ -30,6 +30,30 @@ func hasUses(value llvm.Value) bool { return !value.FirstUse().IsNil() } +// typeHasPointers returns whether a value of this type can hold a pointer. +// +// Vectors report false. That is precise for the IR TinyGo produces: nothing +// lowers a Go value into a vector of pointers, and OptimizeAllocs runs long +// before any vectorizer. It is also why escape analysis may rely on this: a +// pointer can only reach a vector lane through insertelement, which +// valueEscapesAtImpl treats as an escape. A pass that starts feeding pointer +// vectors to either caller needs a case here first. +func typeHasPointers(typ llvm.Type) bool { + switch typ.TypeKind() { + case llvm.PointerTypeKind: + return true + case llvm.StructTypeKind: + for _, field := range typ.StructElementTypes() { + if typeHasPointers(field) { + return true + } + } + case llvm.ArrayTypeKind: + return typ.ArrayLength() != 0 && typeHasPointers(typ.ElementType()) + } + return false +} + // makeGlobalArray creates a new LLVM global with the given name and integers as // contents, and returns the global and initializer type. // Note that it is left with the default linkage etc., you should set diff --git a/transform/testdata/allocs-aggregate.ll b/transform/testdata/allocs-aggregate.ll new file mode 100644 index 0000000000..2152e376ff --- /dev/null +++ b/transform/testdata/allocs-aggregate.ll @@ -0,0 +1,297 @@ +target datalayout = "e-m:e-p:32:32-i64:64-v128:64:128-a:0:32-n32-S64" +target triple = "armv7m-none-eabi" + +%inner = type { ptr, i32 } +%outer = type { i32, %inner } +%deep = type { i32, { [2 x %inner], i32 } } +%mixed = type { ptr, { i32, i32 }, [2 x i32], [0 x ptr] } + +@sink = global %inner zeroinitializer +@pointerSink = global ptr null + +declare ptr @runtime.alloc(i32, ptr) +declare void @capture(%inner) +declare void @nocapture(ptr nocapture) + +define %outer @wrap(ptr %p) { + %inner = insertvalue %inner { ptr null, i32 7 }, ptr %p, 0 + %outer = insertvalue %outer { i32 3, %inner undef }, %inner %inner, 1 + ret %outer %outer +} + +define %deep @wrapDeep(ptr %p) { + %inner = insertvalue %inner { ptr null, i32 7 }, ptr %p, 0 + %array = insertvalue [2 x %inner] zeroinitializer, %inner %inner, 1 + %middle = insertvalue { [2 x %inner], i32 } zeroinitializer, [2 x %inner] %array, 0 + %outer = insertvalue %deep zeroinitializer, { [2 x %inner], i32 } %middle, 1 + ret %deep %outer +} + +define { i32, [2 x [2 x ptr]] } @wrapArrays(ptr %p) { + %inner = insertvalue [2 x ptr] zeroinitializer, ptr %p, 1 + %array = insertvalue [2 x [2 x ptr]] zeroinitializer, [2 x ptr] %inner, 1 + %outer = insertvalue { i32, [2 x [2 x ptr]] } zeroinitializer, [2 x [2 x ptr]] %array, 1 + ret { i32, [2 x [2 x ptr]] } %outer +} + +define [2 x %inner] @wrapSingleArray(ptr %p) { + %inner = insertvalue %inner zeroinitializer, ptr %p, 0 + %array = insertvalue [2 x %inner] zeroinitializer, %inner %inner, 1 + ret [2 x %inner] %array +} + +define %inner @forward(ptr %p) { + %outer = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %outer, 1 + ret %inner %inner +} + +define void @captureInner(%inner %value) { + store %inner %value, ptr @sink + ret void +} + +define %outer @recursive(ptr %p, i1 %again) { +entry: + br i1 %again, label %recurse, label %end +recurse: + %result = call %outer @recursive(ptr %p, i1 false) + ret %outer %result +end: + %wrapped = call %outer @wrap(ptr %p) + ret %outer %wrapped +} + +define void @captureSecond(%inner %ignored, %inner %captured) { + store %inner %captured, ptr @sink + ret void +} + +define { [2 x %inner], i32 } @nestedStructReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %deep @wrapDeep(ptr %p) + %middle = extractvalue %deep %result, 1 + ret { [2 x %inner], i32 } %middle +} + +define [2 x ptr] @nestedArrayReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call { i32, [2 x [2 x ptr]] } @wrapArrays(ptr %p) + %array = extractvalue { i32, [2 x [2 x ptr]] } %result, 1 + %inner = extractvalue [2 x [2 x ptr]] %array, 1 + ret [2 x ptr] %inner +} + +define %inner @arrayOfStructReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %deep @wrapDeep(ptr %p) + %middle = extractvalue %deep %result, 1 + %array = extractvalue { [2 x %inner], i32 } %middle, 0 + %inner = extractvalue [2 x %inner] %array, 1 + ret %inner %inner +} + +define [2 x %inner] @structOfArrayReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %deep @wrapDeep(ptr %p) + %array = extractvalue %deep %result, 1, 0 + ret [2 x %inner] %array +} + +define %inner @singleArrayReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %array = call [2 x %inner] @wrapSingleArray(ptr %p) + %inner = extractvalue [2 x %inner] %array, 1 + ret %inner %inner +} + +define void @aggregateStore() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + store %inner %inner, ptr @sink + ret void +} + +define void @pointerStore() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %ptr = extractvalue %inner %inner, 0 + store ptr %ptr, ptr @pointerSink + ret void +} + +define void @aggregateCall() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + call void @captureInner(%inner %inner) + ret void +} + +define void @unknownCall() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + call void @capture(%inner %inner) + ret void +} + +define void @indirectCall(ptr %fn) { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + call void %fn(%inner %inner) + ret void +} + +define %outer @repackReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %new = insertvalue %outer zeroinitializer, %inner %inner, 1 + ret %outer %new +} + +define %inner @forwardReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %inner = call %inner @forward(ptr %p) + ret %inner %inner +} + +define i32 @forwardLoad() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %inner = call %inner @forward(ptr %p) + %ptr = extractvalue %inner %inner, 0 + %value = load i32, ptr %ptr + ret i32 %value +} + +define ptr @gepReturn() { + %p = call ptr @runtime.alloc(i32 8, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %ptr = extractvalue %inner %inner, 0 + %element = getelementptr i32, ptr %ptr, i32 1 + ret ptr %element +} + +define i32 @gepLoad() { + %p = call ptr @runtime.alloc(i32 8, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %ptr = extractvalue %inner %inner, 0 + %element = getelementptr i32, ptr %ptr, i32 1 + store i32 42, ptr %element + %value = load i32, ptr %element + ret i32 %value +} + +define %inner @aggregatePhi(i1 %cond) { +entry: + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + br i1 %cond, label %yes, label %no +yes: + br label %end +no: + br label %end +end: + %value = phi %inner [ %inner, %yes ], [ zeroinitializer, %no ] + ret %inner %value +} + +define %inner @aggregateSelect(i1 %cond) { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %value = select i1 %cond, %inner %inner, %inner zeroinitializer + ret %inner %value +} + +define %inner @recursiveReturn() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @recursive(ptr %p, i1 true) + %inner = extractvalue %outer %result, 1 + ret %inner %inner +} + +define void @duplicateArguments() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + call void @captureSecond(%inner %inner, %inner %inner) + ret void +} + +define i32 @nonEscapingScalar() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %deep @wrapDeep(ptr %p) + %middle = extractvalue %deep %result, 1 + %array = extractvalue { [2 x %inner], i32 } %middle, 0 + %inner = extractvalue [2 x %inner] %array, 1 + %value = extractvalue %inner %inner, 1 + ret i32 %value +} + +define i32 @nonEscapingLoad() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %ptr = extractvalue %inner %inner, 0 + %value = load i32, ptr %ptr + ret i32 %value +} + +define i1 @nonEscapingNilCheck() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %ptr = extractvalue %inner %inner, 0 + %value = icmp eq ptr %ptr, null + ret i1 %value +} + +define void @nonEscapingDiscard() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + ret void +} + +define void @nonEscapingCall() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %outer @wrap(ptr %p) + %inner = extractvalue %outer %result, 1 + %ptr = extractvalue %inner %inner, 0 + call void @nocapture(ptr %ptr) + ret void +} + +define %mixed @wrapMixed(ptr %p) { + %result = insertvalue %mixed zeroinitializer, ptr %p, 0 + ret %mixed %result +} + +define { i32, i32 } @pointerFreeStruct() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %mixed @wrapMixed(ptr %p) + %scalar = extractvalue %mixed %result, 1 + ret { i32, i32 } %scalar +} + +define [2 x i32] @pointerFreeArray() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %mixed @wrapMixed(ptr %p) + %scalar = extractvalue %mixed %result, 2 + ret [2 x i32] %scalar +} + +define [0 x ptr] @emptyPointerArray() { + %p = call ptr @runtime.alloc(i32 4, ptr null) + %result = call %mixed @wrapMixed(ptr %p) + %empty = extractvalue %mixed %result, 3 + ret [0 x ptr] %empty +} diff --git a/transform/testdata/allocs.ll b/transform/testdata/allocs.ll index 65d941adf2..e4eb798397 100644 --- a/transform/testdata/allocs.ll +++ b/transform/testdata/allocs.ll @@ -85,6 +85,31 @@ define void @testZeroSizedAlloc() { ret void } +; Return the allocated value from a callee inside a nested aggregate, like a +; Go (int, error) result, and let it escape from the caller through the +; extracted inner aggregate. +define { ptr, ptr } @testEscapingReturnedAggregate() { + %alloc = call align 4 ptr @runtime.alloc(i32 4, ptr inttoptr (i32 3 to ptr)) + %result = call { i32, { ptr, ptr } } @wrapInAggregate(ptr %alloc) + %err = extractvalue { i32, { ptr, ptr } } %result, 1 + ret { ptr, ptr } %err +} + +; Same as above, but only a non-pointer part of the aggregate is used, so the +; allocation doesn't escape. +define i32 @testNonEscapingReturnedAggregate() { + %alloc = call align 4 ptr @runtime.alloc(i32 4, ptr inttoptr (i32 3 to ptr)) + %result = call { i32, { ptr, ptr } } @wrapInAggregate(ptr %alloc) + %n = extractvalue { i32, { ptr, ptr } } %result, 0 + ret i32 %n +} + +define { i32, { ptr, ptr } } @wrapInAggregate(ptr %p) { + %itf = insertvalue { ptr, ptr } { ptr null, ptr undef }, ptr %p, 1 + %result = insertvalue { i32, { ptr, ptr } } { i32 1, { ptr, ptr } undef }, { ptr, ptr } %itf, 1 + ret { i32, { ptr, ptr } } %result +} + declare ptr @escapeIntPtr(ptr) declare ptr @noescapeIntPtr(ptr nocapture) diff --git a/transform/testdata/allocs.out.ll b/transform/testdata/allocs.out.ll index 1353d927b6..42637c9c16 100644 --- a/transform/testdata/allocs.out.ll +++ b/transform/testdata/allocs.out.ll @@ -85,6 +85,27 @@ define void @testZeroSizedAlloc() { ret void } +define { ptr, ptr } @testEscapingReturnedAggregate() { + %alloc = call align 4 ptr @runtime.alloc(i32 4, ptr inttoptr (i32 3 to ptr)) + %result = call { i32, { ptr, ptr } } @wrapInAggregate(ptr %alloc) + %err = extractvalue { i32, { ptr, ptr } } %result, 1 + ret { ptr, ptr } %err +} + +define i32 @testNonEscapingReturnedAggregate() { + %stackalloc = alloca [4 x i8], align 4 + store [4 x i8] zeroinitializer, ptr %stackalloc, align 4 + %result = call { i32, { ptr, ptr } } @wrapInAggregate(ptr %stackalloc) + %n = extractvalue { i32, { ptr, ptr } } %result, 0 + ret i32 %n +} + +define { i32, { ptr, ptr } } @wrapInAggregate(ptr %p) { + %itf = insertvalue { ptr, ptr } { ptr null, ptr undef }, ptr %p, 1 + %result = insertvalue { i32, { ptr, ptr } } { i32 1, { ptr, ptr } undef }, { ptr, ptr } %itf, 1 + ret { i32, { ptr, ptr } } %result +} + declare ptr @escapeIntPtr(ptr) declare ptr @noescapeIntPtr(ptr nocapture) diff --git a/transform/testdata/allocs2.go b/transform/testdata/allocs2.go index b8131d821e..90ea57ec1d 100644 --- a/transform/testdata/allocs2.go +++ b/transform/testdata/allocs2.go @@ -174,3 +174,186 @@ func unsafeNoEscape(ptr unsafe.Pointer) uintptr func keepAliveNoEscape(ptr unsafe.Pointer) var pseudoVolatile volatile.Register32 + +type errT struct{ x [4]int } + +func (e *errT) Error() string { return "errT" } + +type ptrStruct struct { + n int + p *errT +} + +func wrapError(e *errT) (int, error) { return 1, e } + +func wrapErrorFirst(e *errT) (error, int) { return e, 1 } + +func wrapAny(e *errT) (int, any) { return 1, e } + +func wrapSlice(b []byte) (int, []byte) { return 1, b } + +func wrapStruct(e *errT) (int, ptrStruct) { return 1, ptrStruct{1, e} } + +func wrapArray(e *errT) (int, [2]*errT) { return 1, [2]*errT{e, nil} } + +var globalErr error + +// The pointer is returned as one of multiple return values, wrapped in an +// interface, slice, struct or array, and then escapes from the caller. +func escapingMultiReturn() error { + e := &errT{} // OUT: escapes at line 206 + _, err := wrapError(e) + return err +} + +func escapingMultiReturnFirst() error { + e := &errT{} // OUT: escapes at line 212 + err, _ := wrapErrorFirst(e) + return err +} + +func escapingMultiReturnAny() any { + e := &errT{} // OUT: escapes at line 218 + _, v := wrapAny(e) + return v +} + +func escapingMultiReturnSlice() []byte { + b := make([]byte, 8) // OUT: escapes at line 224 + _, s := wrapSlice(b) + return s +} + +func escapingMultiReturnStruct() *errT { + e := &errT{} // OUT: escapes at line 195 + _, s := wrapStruct(e) + return s.p +} + +func escapingMultiReturnArray() *errT { + e := &errT{} // OUT: escapes at line 236 + _, a := wrapArray(e) + return a[0] +} + +func escapingMultiReturnGlobal() { + e := &errT{} // OUT: escapes at line 242 + _, err := wrapError(e) + globalErr = err +} + +// The pointer is returned as one of multiple return values, but the caller +// does not let it escape, so it can stay on the stack. +func nonEscapingMultiReturnInt() int { + e := &errT{} + n, _ := wrapError(e) + return n +} + +func nonEscapingMultiReturnNilCheck() bool { + e := &errT{} + _, err := wrapError(e) + return err != nil +} + +func nonEscapingMultiReturnSliceLen() int { + b := make([]byte, 8) + _, s := wrapSlice(b) + return len(s) +} + +func nonEscapingMultiReturnArrayLoad() int { + e := &errT{} + e.x[1] = 42 + _, a := wrapArray(e) + return a[0].x[1] +} + +func wrapSingleArray(e *errT) [1]error { return [1]error{e} } + +func wrapNestedArray(e *errT) (int, [2][2]*errT) { return 1, [2][2]*errT{{}, {nil, e}} } + +func forwardError(e *errT) error { + _, err := wrapError(e) + return err +} + +func escapingSingleArray() error { + e := &errT{} // OUT: escapes at line 284 + a := wrapSingleArray(e) + return a[0] +} + +func escapingNestedArray() [2]*errT { + e := &errT{} // OUT: escapes at line 290 + _, a := wrapNestedArray(e) + return a[1] +} + +func escapingForwardedError() error { + e := &errT{} // OUT: escapes at line 295 + return forwardError(e) +} + +func escapingMultiReturnField() *int { + e := &errT{} // OUT: escapes at line 301 + _, a := wrapArray(e) + return &a[0].x[1] +} + +func escapingMultiReturnCall() { + e := &errT{} // OUT: escapes at line 307 + _, err := wrapError(e) + useInterface(err) +} + +func escapingMultiReturnClosure() func() error { + e := &errT{} // OUT: escapes at line 312 + _, err := wrapError(e) // OUT: escapes at line 313 + return func() error { return err } +} + +func escapingMultiReturnPhi(cond bool) error { + e := &errT{} // OUT: escapes at line 318 + _, err := wrapError(e) + if cond { + err = nil + } + return err +} + +func nonEscapingSingleArray() bool { + e := &errT{} + a := wrapSingleArray(e) + return a[0] != nil +} + +func nonEscapingNestedArray() int { + e := &errT{} + _, a := wrapNestedArray(e) + return a[1][1].x[0] +} + +func nonEscapingForwardedError() bool { + e := &errT{} + return forwardError(e) != nil +} + +func nonEscapingMultiReturnSliceCap() int { + b := make([]byte, 8) + _, s := wrapSlice(b) + return cap(s) +} + +func nonEscapingMultiReturnDiscard() { + e := &errT{} + wrapError(e) +} + +func wrapPointerFreeArray(e *errT) (error, [2]int) { return e, [2]int{e.x[0], 1} } + +func nonEscapingPointerFreeArray() [2]int { + e := &errT{} + _, a := wrapPointerFreeArray(e) + return a +} diff --git a/transform/testdata/allocs2.out.cover b/transform/testdata/allocs2.out.cover index e17f3d421e..77e2592af9 100644 --- a/transform/testdata/allocs2.out.cover +++ b/transform/testdata/allocs2.out.cover @@ -9,3 +9,18 @@ testdata/allocs2.go:50.1,50.36 1 0 testdata/allocs2.go:107.1,107.49 1 0 testdata/allocs2.go:114.1,114.51 1 0 testdata/allocs2.go:128.1,128.55 1 0 +testdata/allocs2.go:204.1,204.42 1 0 +testdata/allocs2.go:210.1,210.42 1 0 +testdata/allocs2.go:216.1,216.42 1 0 +testdata/allocs2.go:222.1,222.50 1 0 +testdata/allocs2.go:228.1,228.42 1 0 +testdata/allocs2.go:234.1,234.42 1 0 +testdata/allocs2.go:240.1,240.42 1 0 +testdata/allocs2.go:282.1,282.42 1 0 +testdata/allocs2.go:288.1,288.42 1 0 +testdata/allocs2.go:294.1,294.42 1 0 +testdata/allocs2.go:299.1,299.42 1 0 +testdata/allocs2.go:305.1,305.42 1 0 +testdata/allocs2.go:311.1,311.52 1 0 +testdata/allocs2.go:312.1,312.52 1 0 +testdata/allocs2.go:317.1,317.42 1 0