diff --git a/transform/allocs.go b/transform/allocs.go index 5c1021a3fb..bd8eaa17df 100644 --- a/transform/allocs.go +++ b/transform/allocs.go @@ -52,12 +52,15 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u heapallocs = append(heapallocs, getUses(allocator)...) } + idMiner := makeIdMiner() for _, heapalloc := range heapallocs { logAllocs := printAllocs != nil && printAllocs.MatchString(heapalloc.InstructionParent().Parent().Name()) if heapalloc.Operand(0).IsAConstantInt().IsNil() { // Do not allocate variable length arrays on the stack. if logAllocs { - logger(getPosition(heapalloc), "size is not constant") + logger( + getPosition(heapalloc), + fmt.Sprintf("%s size is not constant", idMiner.get(heapalloc))) } continue } @@ -67,7 +70,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u // The maximum size for a stack allocation. if logAllocs { logger(getPosition(heapalloc), - fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc)) + fmt.Sprintf("%s size %d exceeds maximum stack allocation size %d", idMiner.get(heapalloc), size, maxStackAlloc)) } continue } @@ -104,7 +107,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u if atPos.Line != 0 { msg = fmt.Sprintf("escapes at line %d", atPos.Line) } - logger(getPosition(heapalloc), msg) + logger(getPosition(heapalloc), fmt.Sprintf("%s %s", idMiner.get(heapalloc), msg)) } continue } @@ -145,7 +148,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u // FormatAllocReason renders the heap allocation in a human-readable format. func FormatAllocReason(pos token.Position, reason string) string { - return fmt.Sprintf("%s: object allocated on the heap: %s", pos.String(), reason) + return fmt.Sprintf("%s: heap allocation: %s", pos.String(), reason) } // FormatAllocCover renders the heap allocation in the go coverage tool format. diff --git a/transform/allocs_test.go b/transform/allocs_test.go index 83ba90ffe8..794d717dde 100644 --- a/transform/allocs_test.go +++ b/transform/allocs_test.go @@ -60,8 +60,24 @@ func TestAllocs2(t *testing.T) { for i, line := range strings.Split(strings.ReplaceAll(string(testInput), "\r\n", "\n"), "\n") { const prefix = " // OUT: " if idx := strings.Index(line, prefix); idx > 0 { - msg := line[idx+len(prefix):] - fmt.Fprintf(&expectedTestOutput, "allocs2.go:%d: %s\n", i+1, msg) + msgLine := strings.TrimSpace(line[idx+len(prefix):]) + msgs := make([]string, 0, 2) + const separator = " && " + for true { + idx = strings.Index(msgLine, separator) + if idx < 0 { + break + } + msg := strings.TrimSpace(msgLine[:idx]) + msgs = append(msgs, msg) + msgLine = strings.TrimSpace(msgLine[idx+len(separator):]) + } + if len(msgLine) > 0 { + msgs = append(msgs, msgLine) + } + for _, msg := range msgs { + fmt.Fprintf(&expectedTestOutput, "allocs2.go:%d: %s\n", i+1, msg) + } } } diff --git a/transform/idminer.go b/transform/idminer.go new file mode 100644 index 0000000000..9b92ab03fe --- /dev/null +++ b/transform/idminer.go @@ -0,0 +1,352 @@ +package transform + +import ( + "fmt" + "regexp" + "strings" + "tinygo.org/x/go-llvm" +) + +type idMiner struct { + complitReg *regexp.Regexp + v llvm.Value +} + +func makeIdMiner() idMiner { + return idMiner{complitReg: regexp.MustCompile(`^complit\d*$`)} +} + +func (this *idMiner) get(v llvm.Value) string { + this.v = v + id := v.Name() + if len(id) == 0 { + id = this.lookForAllocatedType() + } + var handled bool + id, handled = this.handleCompositLiteral(id) + if handled { + return id + } + if id == "FloatType" { + // When tv.AllocatedType() gives "FloatType" it often means packing something into `interface`. + id, handled = this.lookForInterfaceWrapping(id) + if handled { + return id + } + } + if len(id) == 0 { + id = "unidentified" + } + return id +} + +func (this *idMiner) lookForAllocatedType() string { + if !this.v.IsACallInst().IsNil() { + return this.v.AllocatedType().String() + } + return "" +} + +func (this *idMiner) handleCompositLiteral(id string) (newId string, handled bool) { + if this.complitReg.MatchString(id) { + // Handle case like: c := scaleVector3(&vector3{4, 5, 6}, 0.5) + /* + %complit = call align 4 dereferenceable(12) ptr @runtime.alloc(i32 12, ptr nonnull inttoptr (i32 3 to ptr), ptr undef) #1, !dbg !53 + %0 = getelementptr inbounds nuw i8, ptr %complit, i32 4, !dbg !53 + %1 = getelementptr inbounds nuw i8, ptr %complit, i32 8, !dbg !53 + store float 4.000000e+00, ptr %complit, align 4, !dbg !54 + store float 5.000000e+00, ptr %0, align 4, !dbg !55 + store float 6.000000e+00, ptr %1, align 4, !dbg !56 + %2 = call ptr @main.scaleVector3(ptr nonnull %complit, float 5.000000e-01, ptr undef), !dbg !57 + store ptr %2, ptr @main.escapedVector3, align 4, !dbg !60 + ret void, !dbg !61 + */ + /* + * 1. Find the next "call", where %complit is passed as parameter. + * 2. Produce ID: "Arg 0 of main.scaleVector3() call" // escapes at ... + */ + n := this.v + for _ = range 64 { + n = llvm.NextInstruction(n) + if n.IsNil() { + break + } + if call := n.IsACallInst(); !call.IsNil() { + args := getArgs(call) + for idx, arg := range args { + if arg.arg.Name() == id { + return getId(call, idx, &arg), true + } + } + } + } + } + return id, false +} + +// TODO: make a testcase where a function which have multiple escaping interface and not interface values, +// is called and check that the arg indicies displayed correctly +func (this *idMiner) lookForInterfaceWrapping(id string) (newId string, handled bool) { + /* --- The costumError -> error, struct -> interface --- + * %10 = call align 4 dereferenceable(16) ptr @runtime.alloc(i32 16, ptr null, ptr undef) #1, !dbg !193 + * %.elt = extractvalue { double, double } %9, 0, !dbg !193 + * store double %.elt, ptr %10, align 4, !dbg !193 + * %.repack18 = getelementptr inbounds nuw i8, ptr %10, i32 8, !dbg !193 + * %.elt19 = extractvalue { double, double } %9, 1, !dbg !193 + * store double %.elt19, ptr %.repack18, align 4, !dbg !193 + * call void @main.useInterface(ptr nonnull @"reflect/types.type:basic:complex128", ptr nonnull %10, ptr undef) #1, !dbg !196 + * + * %11 = call align 4 dereferenceable(12) ptr @runtime.alloc(i32 12, ptr null, ptr undef) #1, !dbg !218 + * store i32 10, ptr %11, align 4, !dbg !218 + * %.repack25 = getelementptr inbounds nuw i8, ptr %11, i32 4, !dbg !218 + * store i32 11, ptr %.repack25, align 4, !dbg !218 + * %.repack26 = getelementptr inbounds nuw i8, ptr %11, i32 8, !dbg !218 + * store i32 12, ptr %.repack26, align 4, !dbg !218 + * call void @main.useInterface(ptr nonnull @"reflect/types.type:array:3:basic:int32", ptr nonnull %11, ptr undef) #1, !dbg !219 + * + * %0 = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr null, ptr undef) #1, !dbg !134 + * store ptr @"main$string.1", ptr %0, align 4, !dbg !134 + * %.repack5 = getelementptr inbounds nuw i8, ptr %0, i32 4, !dbg !134 + * store i32 5, ptr %.repack5, align 4, !dbg !134 + * %1 = insertvalue %runtime._interface { ptr getelementptr ({ ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x ptr] }, [14 x i8] }, ptr @"reflect/types.type:named:main.theError", i32 0, i32 1), ptr undef }, ptr %0, 1, !dbg !134 + * ret %runtime._interface %1, !dbg !135 + * + * %1 = call align 4 dereferenceable(8) ptr @runtime.alloc(i32 8, ptr null, ptr undef) #1, !dbg !133 + * %2 = extractvalue %main.theError %0, 0, !dbg !133 + * %.elt = extractvalue %runtime._string %2, 0, !dbg !133 + * store ptr %.elt, ptr %1, align 4, !dbg !133 + * %.repack1 = getelementptr inbounds nuw i8, ptr %1, i32 4, !dbg !133 + * %.elt2 = extractvalue %runtime._string %2, 1, !dbg !133 + * store i32 %.elt2, ptr %.repack1, align 4, !dbg !133 + * %3 = insertvalue %runtime._interface { ptr getelementptr ({ ptr, i8, i16, ptr, ptr, ptr, { i32, [1 x ptr] }, [14 x i8] }, ptr @"reflect/types.type:named:main.theError", i32 0, i32 1), ptr undef }, ptr %1, 1, !dbg !133 + * ret %runtime._interface %3, !dbg !134 + * + * pattern: + * %reg = alloc + * store reg/var -> ptr %reg // write to struct, array or slice + * call with a (*, ptr nonnull @"reflect/types.type:...", ptr %reg arg, *) arg pair + * - Error message: "Arg x of fnName() call with type array:3:basic:int32" + " escapes at ..." + * or + * insertvalue %runtime._interface with ptr %reg param and ptr @"reflect/types.type:named:main.theError" (name of struct) + * - @"reflect/types.type:array:3:basic:int32": [3]int32 + * - Error message: "array:3:basic:int32" + " escapes at ..." + * + */ + // dumpIR("--- FloatType ----", 32, this.v) + ptrToAllocated := this.v + n := this.v + for _ = range 64 { + n = llvm.NextInstruction(n) + if n.IsNil() { + break + } + if !n.IsACallInst().IsNil() { + args := getArgs(n) + for idx, arg := range args { + if len(arg.descTypeName) > 0 && arg.arg == ptrToAllocated { + return getId(n, idx, &arg), true + } + } + } else if !n.IsAInsertValueInst().IsNil() { // interface wrapping + insertedValue := n.Operand(1) + if insertedValue == ptrToAllocated { + typeName, found := getTypeNameFromInsertValue(n) + if found { + return typeName, true + } + } + } + } + // %10 = call align 4 dereferenceable(16) ptr @runtime.alloc(i32 16, ptr null, ptr undef) #1, !dbg !193 + // As a fallback produce a message something like: "16 bytes escapes..." + if !this.v.IsACallInst().IsNil() { + if cv := this.v.CalledValue(); cv.Name() == "runtime.alloc" && this.v.OperandsCount() > 0 { + allocSizeArg := this.v.Operand(0) + allocSize := allocSizeArg.ZExtValue() + return fmt.Sprint(allocSize, " bytes"), true + } + } + return id, false +} + +type arg struct { + descTypeName string // It is set when arg is an interface arg, + // this is the type name which is wrapped by the interface. + name string // sometimes the name is available + arg llvm.Value +} + +func getId(callInst llvm.Value, idx int, a *arg) string { + ta := len(a.descTypeName) > 0 + na := len(a.name) > 0 + fn := callInst.CalledValue().Name() + if ta && na { + return fmt.Sprintf("Arg %d (%s %s) of %s()", idx, a.name, a.descTypeName, fn) + } else if ta { + return fmt.Sprintf("Arg %d (type: %s) of %s()", idx, a.descTypeName, fn) + } else if na { + return fmt.Sprintf("Arg %d (name: %s) of %s()", idx, a.name, fn) + } + return fmt.Sprintf("Arg %d of %s()", idx, fn) +} + +func getArgs(callInst llvm.Value) []arg { + /* + func useInterfaces(int, interface{}, reflect.Type, int, interface{}) + call void @main.useInterfaces( + i32 16, + ptr nonnull @"reflect/types.type:pointer:named:main.vector3", ptr nonnull %complit, + ptr %5, ptr %6, + i32 32, + ptr nonnull @"reflect/types.type:pointer:named:main.vector3", ptr nonnull %complit1, + ptr undef) #1, !dbg !191 + */ + /* Scanning backward could identify which ptr pairs are interfaces: + * + * %5 = extractvalue %runtime._interface %2, 0, !dbg !541 + * %6 = extractvalue %runtime._interface %2, 1, !dbg !541 + * call void @main.useInterfaces(i32 16, ptr nonnull @"reflect/types.type:pointer:named:main.vector3", ptr nonnull %complit, ptr %5, ptr %6, i32 32, ptr nonnull @"reflect/types.type:pointer:named:main.vector3", ptr nonnull %complit1, ptr undef) #1, !dbg !541 + * + * When the passed interface is the argument of the called function: + * define hidden void @main.passAnInterface( + * ptr %t.typecode, ptr %t.value, ptr nocapture readnone %context + * ) unnamed_addr #1 !dbg !553 { + * call void @main.useInterfaces( + * i32 16, + * ptr nonnull @"reflect/types.type:pointer:named:main.vector3", ptr nonnull %complit, + * ptr %t.typecode, ptr %t.value, + * i32 32, + * ptr nonnull @"reflect/types.type:pointer:named:main.vector3", ptr nonnull %complit1, + * ptr undef) #1, !dbg !569 + ret void, !dbg !570 + */ + typeSuffix := ".typecode" + valueSuffix := ".value" + res := make([]arg, 0, 4) + opCnt := callInst.OperandsCount() + idx := 0 + for idx < opCnt { + argV := callInst.Operand(idx) + placed := false + idx++ + var nextArgV llvm.Value + if idx < opCnt { + nextArgV = callInst.Operand(idx) + if argV.Type().TypeKind() == llvm.PointerTypeKind && nextArgV.Type().TypeKind() == llvm.PointerTypeKind { + // Check pointer pair + ptrName := argV.Name() + nextPtrName := nextArgV.Name() + if len(ptrName) > 0 { + typeName, found := removeReflexPrefix(ptrName) + if found { + res = append(res, arg{descTypeName: typeName, arg: nextArgV}) + placed = true + idx++ + } + } + // The easier case + if !placed { + argName, found := strings.CutSuffix(ptrName, typeSuffix) + if found { + _, valueFound := strings.CutSuffix(nextPtrName, valueSuffix) + if valueFound { + res = append(res, + arg{name: argName, arg: nextArgV}) + placed = true + idx++ + } + } + } + // Step back on the instruction list to look for 'extractvalue' + if !placed { + p := callInst + p0Found := false + p1Found := false + for _ = range 64 { + p = llvm.PrevInstruction(p) + if p.IsNil() { + break + } + if !p.IsAExtractValueInst().IsNil() { + operand := p.Operand(0) + tp := operand.Type() + tpKind := tp.TypeKind() + if tpKind == llvm.StructTypeKind && tp.StructName() == "runtime._interface" { + p0Found = p0Found || argV == p + p1Found = p1Found || nextArgV == p + } + if p0Found && p1Found { + res = append(res, + arg{descTypeName: "unknown", arg: nextArgV}) + placed = true + idx++ + break + } + } + } + } + } + } + if !placed { + res = append(res, arg{arg: argV}) + } + } + return res +} + +func removeReflexPrefix(typeName string) (newTypeName string, removed bool) { + prefixs := []string{"reflect/types.type:named:", "reflect/types.type:"} + for _, prefix := range prefixs { + newTypeName, removed = strings.CutPrefix(typeName, prefix) + if removed { + return + } + } + return +} + +func getTypeNameFromInsertValue(insertValueInst llvm.Value) (typeName string, found bool) { + // %3 = insertvalue %runtime._interface { + // ptr getelementptr ( + // { ptr, i8, i16, ptr, ptr, ptr, { + // i32, [1 x ptr] }, [14 x i8] + // }, + // ptr @"reflect/types.type:named:main.theError", + // i32 0, i32 1 + // ), ptr undef + // }, + // ptr %1, 1, !dbg !133 + baseAggregate := insertValueInst.Operand(0) + + // Since it's an inline constant structure, let's look at its elements. + // The type descriptor is the first element of this constant struct. + if !baseAggregate.IsAConstantStruct().IsNil() || !baseAggregate.IsAConstantAggregateZero().IsNil() { + // Extract the first element (the GEP expression) + gepExpr := baseAggregate.Operand(0) + + // Drill down through the GEP/Casts to find the Global Variable symbol + for !gepExpr.IsAConstantExpr().IsNil() { + // In a GEP or bitcast expression, Operand(0) is the source pointer + gepExpr = gepExpr.Operand(0) + } + + // If we successfully drilled down to the Global Variable, grab its name + if !gepExpr.IsAGlobalVariable().IsNil() { + typeName, found = removeReflexPrefix(gepExpr.Name()) + } + } + return +} + +func dumpIR(hdr string, instCnt int, fstInst llvm.Value) { + fmt.Println("---", hdr, "---") + n := fstInst + fmt.Println(n.String()) + for _ = range instCnt { + n = llvm.NextInstruction(n) + if n.IsNil() { + break + } + fmt.Println(n.String()) + } +} diff --git a/transform/testdata/allocs2.go b/transform/testdata/allocs2.go index b8131d821e..ca11f6c423 100644 --- a/transform/testdata/allocs2.go +++ b/transform/testdata/allocs2.go @@ -1,6 +1,8 @@ package main import ( + "fmt" + "reflect" "runtime/volatile" "unsafe" ) @@ -21,33 +23,37 @@ func main() { s3 := make([]int, 3) returnIntSlice(s3) - useSlice(make([]int, getUnknownNumber())) // OUT: size is not constant + useSlice(make([]int, getUnknownNumber())) // OUT: makeslice.buf size is not constant - s4 := make([]byte, 300) // OUT: object size 300 exceeds maximum stack allocation size 256 + s4 := make([]byte, 300) // OUT: makeslice4 size 300 exceeds maximum stack allocation size 256 readByteSlice(s4) - s5 := make([]int, 4) // OUT: escapes at line 30 + s5 := make([]int, 4) // OUT: makeslice6 escapes at line 32 _ = append(s5, 5) s6 := make([]int, 3) s7 := []int{1, 2, 3} copySlice(s6, s7) - c1 := getComplex128() // OUT: escapes at line 37 + c1 := getComplex128() // OUT: Arg 0 (type: basic:complex128) of main.useInterface() escapes at line 40 + useInterface(c1) + a1 := [3]int32{10, 11, 12} + useInterface(a1) // OUT: Arg 0 (type: array:3:basic:int32) of main.useInterface() escapes at line 43 + n3 := 5 func() int { return n3 }() - callVariadic(3, 5, 8) // OUT: escapes at line 44 + callVariadic(3, 5, 8) // OUT: varargs12 escapes at line 50 - s8 := []int{3, 5, 8} // OUT: escapes at line 47 + s8 := []int{3, 5, 8} // OUT: slicelit14 escapes at line 53 callVariadic(s8...) - n4 := 3 // OUT: escapes at line 51 - n5 := 7 // OUT: escapes at line 51 + n4 := 3 // OUT: n4 escapes at line 57 + n5 := 7 // OUT: n5 escapes at line 57 func() { n4 = n5 }() @@ -74,6 +80,11 @@ func main() { pseudoVolatile.Set(uint32(unsafeNoEscape(unsafe.Pointer(&dmaBuf2[0])))) // ...use the buffer in the DMA peripheral keepAliveNoEscape(unsafe.Pointer(&dmaBuf2[0])) + // Doesn't escape. + theErr := theError{} + var err error = theErr + theErr.set("hello") + println(err.Error()) } type vector3 [3]float32 @@ -93,6 +104,20 @@ func crossVector3(a, b *vector3) vector3 { } } +type msgSetter interface { + set(msg string) +} + +type theError struct { + msg string +} + +func makeTheError(msg string) theError { return theError{msg: msg} } + +func (this theError) Error() string { return this.msg } + +func (this theError) set(msg string) { this.msg = msg } + func nonEscapingReturnedPointer() vector3 { a := vector3{1, 2, 3} b := vector3{4, 5, 6} @@ -104,19 +129,37 @@ func nonEscapingReturnedPointer() vector3 { var escapedSlice []int func escapingReturnedSlice() { - s := make([]int, 3) // OUT: escapes at line 108 + s := make([]int, 3) // OUT: makeslice escapes at line 133 escapedSlice = returnIntSlice(s) } var escapedVector3 *vector3 func escapingReturnedPointer() { - b := vector3{4, 5, 6} // OUT: escapes at line 117 + b := vector3{4, 5, 6} // OUT: b escapes at line 142 c := scaleVector3(&b, 0.5) escapedVector3 = c } +var escapedArray [2](*vector3) + +func escapingCompositeLiterals() { + c := scaleVector3(&vector3{4, 5, 6}, 0.5) // OUT: Arg 0 of main.scaleVector3() escapes at line 149 + escapedVector3 = c + a := [2](*vector3){&vector3{7, 8, 9}, &vector3{2, 3, 4}} // OUT: complit1 escapes at line 151 && complit2 escapes at line 151 + escapedArray = a +} + +func escapingInterfaceArguments() { + // Check that the argument indices in the escape messages are right. + useInterfaces(16, &vector3{7, 8, 9}, reflect.TypeFor[int](), 32, &vector3{6, 7, 8}) // OUT: Arg 1 (type: pointer:named:main.vector3) of main.useInterfaces() escapes at line 156 && Arg 4 (type: pointer:named:main.vector3) of main.useInterfaces() escapes at line 156 +} + +func passAnInterface(t reflect.Type) { + useInterfaces(16, &vector3{7, 8, 9}, t, 32, &vector3{6, 7, 8}) // OUT: Arg 1 (type: pointer:named:main.vector3) of main.useInterfaces() escapes at line 160 && Arg 4 (type: pointer:named:main.vector3) of main.useInterfaces() escapes at line 160 +} + func recursiveScaleVector3(vec *vector3, n int) *vector3 { if n == 0 { return vec @@ -125,12 +168,31 @@ func recursiveScaleVector3(vec *vector3, n int) *vector3 { } func recursiveReturnedPointer() vector3 { - b := vector3{4, 5, 6} // OUT: escapes at unknown line + b := vector3{4, 5, 6} // OUT: b escapes at unknown line c := recursiveScaleVector3(&b, 1) return *c } +func theFmt() string { + v := 48 + res := fmt.Sprintf("The number is %d", v) // OUT: varargs escapes at line 179 + return res +} + +func giveAnError() error { + return theError{msg: "hello"} // OUT: main.theError escapes at line 184 +} + +func giveOtherError() error { + return makeTheError("wellcome") // OUT: main.theError escapes at line 188 +} + +func giveAnInterface() interface{} { + a := [3]int32{1, 2, 3} + return a // OUT: array:3:basic:int32 escapes at line 193 +} + func derefInt(x *int) int { return *x } @@ -174,3 +236,5 @@ func unsafeNoEscape(ptr unsafe.Pointer) uintptr func keepAliveNoEscape(ptr unsafe.Pointer) var pseudoVolatile volatile.Register32 + +func useInterfaces(int, interface{}, reflect.Type, int, interface{}) diff --git a/transform/testdata/allocs2.out.cover b/transform/testdata/allocs2.out.cover index e17f3d421e..e90cca42a0 100644 --- a/transform/testdata/allocs2.out.cover +++ b/transform/testdata/allocs2.out.cover @@ -1,11 +1,23 @@ -testdata/allocs2.go:24.1,24.72 1 0 -testdata/allocs2.go:26.1,26.91 1 0 -testdata/allocs2.go:29.1,29.49 1 0 -testdata/allocs2.go:36.1,36.50 1 0 -testdata/allocs2.go:44.1,44.50 1 0 -testdata/allocs2.go:46.1,46.49 1 0 -testdata/allocs2.go:49.1,49.36 1 0 -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:26.1,26.86 1 0 +testdata/allocs2.go:28.1,28.95 1 0 +testdata/allocs2.go:31.1,31.60 1 0 +testdata/allocs2.go:38.1,38.104 1 0 +testdata/allocs2.go:43.1,43.102 1 0 +testdata/allocs2.go:50.1,50.60 1 0 +testdata/allocs2.go:52.1,52.60 1 0 +testdata/allocs2.go:55.1,55.39 1 0 +testdata/allocs2.go:56.1,56.39 1 0 +testdata/allocs2.go:132.1,132.59 1 0 +testdata/allocs2.go:139.1,139.53 1 0 +testdata/allocs2.go:148.1,148.100 1 0 +testdata/allocs2.go:150.1,150.127 1 0 +testdata/allocs2.go:150.1,150.127 1 0 +testdata/allocs2.go:156.1,156.266 1 0 +testdata/allocs2.go:156.1,156.266 1 0 +testdata/allocs2.go:160.1,160.245 1 0 +testdata/allocs2.go:160.1,160.245 1 0 +testdata/allocs2.go:171.1,171.57 1 0 +testdata/allocs2.go:179.1,179.79 1 0 +testdata/allocs2.go:184.1,184.73 1 0 +testdata/allocs2.go:188.1,188.75 1 0 +testdata/allocs2.go:193.1,193.58 1 0