From 4465360f3d125bbed8404453a9fa709f6c384085 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piotr=20Boche=C5=84ski?= Date: Thu, 11 Jun 2026 21:33:05 +0200 Subject: [PATCH] transform: add -print-allocs-cover and restore -print-allocs reason output MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PR #5220 changed -print-allocs output to the go coverage tool format, which replaced the original human-readable explanation of why each object had to be heap allocated. That explanation is useful on its own, so this restores it as the default behavior of -print-allocs and moves the coverage format behind a -print-allocs-cover flag. Signed-off-by: Piotr Bocheński --- compileopts/options.go | 1 + main.go | 12 +++- transform/allocs.go | 45 +++++++-------- transform/allocs_test.go | 81 +++++++++++---------------- transform/optimizer.go | 24 +++++--- transform/testdata/allocs2.go | 20 +++---- transform/testdata/allocs2.out.cover | 10 ++++ transform/testdata/allocs2.out.reason | 10 ++++ transform/transform_test.go | 18 ++++++ 9 files changed, 130 insertions(+), 91 deletions(-) create mode 100644 transform/testdata/allocs2.out.cover create mode 100644 transform/testdata/allocs2.out.reason diff --git a/compileopts/options.go b/compileopts/options.go index c8f9d292b..d05cfb419 100644 --- a/compileopts/options.go +++ b/compileopts/options.go @@ -47,6 +47,7 @@ type Options struct { Nobounds bool PrintSizes string PrintAllocs *regexp.Regexp // regexp string + PrintAllocsCover bool // emit allocs in go coverage tool format PrintStacks bool Tags []string GlobalValues map[string]map[string]string // map[pkgpath]map[varname]value diff --git a/main.go b/main.go index 90760b04f..7a5e96846 100644 --- a/main.go +++ b/main.go @@ -1778,6 +1778,7 @@ func main() { printSize := flag.String("size", "", "print sizes (none, short, full, html)") printStacks := flag.Bool("print-stacks", false, "print stack sizes of goroutines") printAllocsString := flag.String("print-allocs", "", "regular expression of functions for which heap allocations should be printed") + printAllocsCoverString := flag.String("print-allocs-cover", "", "like -print-allocs, but in go coverage tool format") printCommands := flag.Bool("x", false, "Print commands") flagJSON := flag.Bool("json", false, "print output in JSON format") parallelism := flag.Int("p", runtime.GOMAXPROCS(0), "the number of build jobs that can run in parallel") @@ -1858,8 +1859,14 @@ func main() { } var printAllocs *regexp.Regexp - if *printAllocsString != "" { - printAllocs, err = regexp.Compile(*printAllocsString) + printAllocsCover := false + printAllocsPattern := *printAllocsString + if *printAllocsCoverString != "" { + printAllocsPattern = *printAllocsCoverString + printAllocsCover = true + } + if printAllocsPattern != "" { + printAllocs, err = regexp.Compile(printAllocsPattern) if err != nil { fmt.Fprintln(os.Stderr, err) os.Exit(1) @@ -1907,6 +1914,7 @@ func main() { PrintSizes: *printSize, PrintStacks: *printStacks, PrintAllocs: printAllocs, + PrintAllocsCover: printAllocsCover, Tags: []string(tags), TestConfig: testConfig, GlobalValues: globalVarValues, diff --git a/transform/allocs.go b/transform/allocs.go index 18b49998c..477afa0a3 100644 --- a/transform/allocs.go +++ b/transform/allocs.go @@ -46,10 +46,6 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u complex128Type := ctx.StructType([]llvm.Type{ctx.DoubleType(), ctx.DoubleType()}, false) maxAlign := int64(targetData.ABITypeAlignment(complex128Type)) - if printAllocs != nil { - fmt.Fprintln(os.Stderr, "mode: set") - } - // Find all allocator calls. var heapallocs []llvm.Value for _, allocator := range allocators { @@ -61,7 +57,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u if heapalloc.Operand(0).IsAConstantInt().IsNil() { // Do not allocate variable length arrays on the stack. if logAllocs { - logAlloc(logger, heapalloc, "size is not constant") + logger(getPosition(heapalloc), "size is not constant") } continue } @@ -70,7 +66,8 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u if size > maxStackAlloc { // The maximum size for a stack allocation. if logAllocs { - logAlloc(logger, heapalloc, fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc)) + logger(getPosition(heapalloc), + fmt.Sprintf("object size %d exceeds maximum stack allocation size %d", size, maxStackAlloc)) } continue } @@ -107,7 +104,7 @@ func OptimizeAllocs(mod llvm.Module, printAllocs *regexp.Regexp, maxStackAlloc u if atPos.Line != 0 { msg = fmt.Sprintf("escapes at line %d", atPos.Line) } - logAlloc(logger, heapalloc, msg) + logger(getPosition(heapalloc), msg) } continue } @@ -146,6 +143,22 @@ 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) +} + +// FormatAllocCover renders the heap allocation in the go coverage tool format. +func FormatAllocCover(pos token.Position) string { + if pos.Filename == "" || pos.Line <= 0 { + return "" // no position info; a blank line would corrupt the profile + } + // Highlight the whole line: column 1 to one past the last byte (the end + // column is exclusive, so add 1 to the line length). + endCol := max(lineLengthAt(pos.Filename, pos.Line), 1) + 1 + return fmt.Sprintf("%s:%d.1,%d.%d 1 0", pos.Filename, pos.Line, pos.Line, endCol) +} + // 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 { @@ -189,24 +202,6 @@ func valueEscapesAt(value llvm.Value) llvm.Value { return llvm.Value{} } -// logAlloc prints a message to stderr explaining why the given object had to be -// allocated on the heap. -func logAlloc(logger func(token.Position, string), allocCall llvm.Value, reason string) { - pos := getPosition(allocCall) - if pos.Filename == "" || pos.Line <= 0 { - logger(pos, "") - return - } - - endCol := lineLengthAt(pos.Filename, pos.Line) - if endCol < 1 { - endCol = 1 - } - - // Only emit the coverprofile line, without position prefix. - logger(token.Position{}, fmt.Sprintf("%s:%d.1,%d.%d 1 0", pos.Filename, pos.Line, pos.Line, endCol)) -} - func lineLengthAt(filename string, lineNumber int) int { f, err := os.Open(filename) if err != nil { diff --git a/transform/allocs_test.go b/transform/allocs_test.go index df39605b1..2b524392f 100644 --- a/transform/allocs_test.go +++ b/transform/allocs_test.go @@ -2,11 +2,8 @@ package transform_test import ( "go/token" - "os" - "path/filepath" "regexp" "sort" - "strconv" "strings" "testing" @@ -21,21 +18,16 @@ func TestAllocs(t *testing.T) { }) } -type allocsTestOutput struct { - filename string - line int - msg string -} - -func (out allocsTestOutput) String() string { - return out.filename + ":" + strconv.Itoa(out.line) + ": " + out.msg -} - // Test with a Go file as input (for more accurate tests). func TestAllocs2(t *testing.T) { t.Parallel() - mod := compileGoFileForTesting(t, "./testdata/allocs2.go") + const ( + basePath = "testdata/allocs2" + goFile = basePath + ".go" + goldenFile = basePath + ".out" + ) + mod := compileGoFileForTesting(t, goFile) // Run functionattrs pass, which is necessary for escape analysis. po := llvm.NewPassBuilderOptions() @@ -46,39 +38,34 @@ func TestAllocs2(t *testing.T) { } // Run heap to stack transform. - var testOutputs []allocsTestOutput - transform.OptimizeAllocs(mod, regexp.MustCompile("."), 256, func(pos token.Position, msg string) { - testOutputs = append(testOutputs, allocsTestOutput{ - filename: filepath.Base(pos.Filename), - line: pos.Line, - msg: msg, + type report struct { + pos token.Position + reason string + } + var reports []report + transform.OptimizeAllocs(mod, regexp.MustCompile("."), 256, func(pos token.Position, reason string) { + pos.Filename = goFile + reports = append(reports, report{pos, reason}) + }) + sort.Slice(reports, func(i, j int) bool { return reports[i].pos.Line < reports[j].pos.Line }) + + // Render every report in each format and diff against its golden file. + for _, format := range []struct { + name string + render func(report) string + }{ + {"reason", func(r report) string { return transform.FormatAllocReason(r.pos, r.reason) }}, + {"cover", func(r report) string { return transform.FormatAllocCover(r.pos) }}, + } { + t.Run(format.name, func(t *testing.T) { + var got strings.Builder + for _, r := range reports { + if line := format.render(r); line != "" { + got.WriteString(line) + got.WriteByte('\n') + } + } + checkGolden(t, goldenFile+"."+format.name, got.String()) }) - }) - sort.Slice(testOutputs, func(i, j int) bool { - return testOutputs[i].line < testOutputs[j].line - }) - testOutput := make([]string, 0) - for _, out := range testOutputs { - testOutput = append(testOutput, out.String()) - } - - // Load expected test output (the OUT: lines). - testInput, err := os.ReadFile("./testdata/allocs2.go") - if err != nil { - t.Fatal("could not read test input:", err) - } - var expectedTestOutput []string - for _, line := range strings.Split(strings.ReplaceAll(string(testInput), "\r\n", "\n"), "\n") { - if idx := strings.Index(line, " // OUT: "); idx > 0 { - msg := line[idx+len(" // OUT: "):] - expectedTestOutput = append(expectedTestOutput, msg) - } - } - - for i := range testOutput { - if !strings.HasSuffix(testOutput[i], expectedTestOutput[i]) { - t.Errorf("output does not match expected output:\n%s\n%s\n", testOutput[i], expectedTestOutput[i]) - return - } } } diff --git a/transform/optimizer.go b/transform/optimizer.go index ba8d1c12e..150a9a77c 100644 --- a/transform/optimizer.go +++ b/transform/optimizer.go @@ -86,13 +86,23 @@ func Optimize(mod llvm.Module, config *compileopts.Config) []error { } // Run TinyGo-specific interprocedural optimizations. - OptimizeAllocs(mod, config.Options.PrintAllocs, maxStackSize, func(pos token.Position, msg string) { - if pos.Filename != "" { - fmt.Fprintf(os.Stderr, "%s:%d:%d: %s\n", pos.Filename, pos.Line, pos.Column, msg) - } else { - fmt.Fprintln(os.Stderr, msg) // No prefix! - } - }) + if config.Options.PrintAllocs != nil && config.Options.PrintAllocsCover { + // The go coverage tool expects this header before any blocks. + fmt.Fprintln(os.Stderr, "mode: set") + } + OptimizeAllocs(mod, config.Options.PrintAllocs, maxStackSize, + func(pos token.Position, reason string) { + var line string + if config.Options.PrintAllocsCover { + line = FormatAllocCover(pos) + } else { + line = FormatAllocReason(pos, reason) + } + if line != "" { + fmt.Fprintln(os.Stderr, line) + } + }, + ) OptimizeStringToBytes(mod) OptimizeStringEqual(mod) diff --git a/transform/testdata/allocs2.go b/transform/testdata/allocs2.go index 2b0306db1..35ab7bf3f 100644 --- a/transform/testdata/allocs2.go +++ b/transform/testdata/allocs2.go @@ -10,7 +10,7 @@ func main() { derefInt(&n1) // This should eventually be modified to not escape. - n2 := 6 // OUT: allocs2.go:52.1,52.42 1 0 + n2 := 6 returnIntPtr(&n2) s1 := make([]int, 3) @@ -20,22 +20,22 @@ func main() { readIntSlice(s2[:]) // This should also be modified to not escape. - s3 := make([]int, 3) // OUT: allocs2.go:51.1,51.42 1 0 + s3 := make([]int, 3) returnIntSlice(s3) - useSlice(make([]int, getUnknownNumber())) // OUT: allocs2.go:48.1,48.55 1 0 + useSlice(make([]int, getUnknownNumber())) - s4 := make([]byte, 300) // OUT: allocs2.go:46.1,46.56 1 0 + s4 := make([]byte, 300) readByteSlice(s4) - s5 := make([]int, 4) // OUT: allocs2.go:38.1,38.56 1 0 + s5 := make([]int, 4) _ = append(s5, 5) s6 := make([]int, 3) s7 := []int{1, 2, 3} copySlice(s6, s7) - c1 := getComplex128() // OUT: allocs2.go:31.1,31.55 1 0 + c1 := getComplex128() useInterface(c1) n3 := 5 @@ -43,13 +43,13 @@ func main() { return n3 }() - callVariadic(3, 5, 8) // OUT: allocs2.go:28.1,28.58 1 0 + callVariadic(3, 5, 8) - s8 := []int{3, 5, 8} // OUT: allocs2.go:26.1,26.76 1 0 + s8 := []int{3, 5, 8} callVariadic(s8...) - n4 := 3 // OUT: allocs2.go:23.1,23.55 1 0 - n5 := 7 // OUT: allocs2.go:13.1,13.42 1 0 + n4 := 3 + n5 := 7 func() { n4 = n5 }() diff --git a/transform/testdata/allocs2.out.cover b/transform/testdata/allocs2.out.cover new file mode 100644 index 000000000..a26c2a81d --- /dev/null +++ b/transform/testdata/allocs2.out.cover @@ -0,0 +1,10 @@ +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 diff --git a/transform/testdata/allocs2.out.reason b/transform/testdata/allocs2.out.reason new file mode 100644 index 000000000..22f798e4b --- /dev/null +++ b/transform/testdata/allocs2.out.reason @@ -0,0 +1,10 @@ +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 diff --git a/transform/transform_test.go b/transform/transform_test.go index 40769dbe8..357d0cb46 100644 --- a/transform/transform_test.go +++ b/transform/transform_test.go @@ -24,6 +24,24 @@ var defaultTestConfig = &compileopts.Config{ Options: &compileopts.Options{Opt: "2"}, } +// checkGolden compares got against the golden file at path, or rewrites that +// file when the test is run with -update. +func checkGolden(t *testing.T, path, got string) { + if *update { + if err := os.WriteFile(path, []byte(got), 0o666); err != nil { + t.Fatal(err) + } + return + } + want, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + if got != string(want) { + t.Errorf("%s does not match expected output:\n%s", path, got) + } +} + // testTransform runs a transformation pass on an input file (pathPrefix+".ll") // and checks whether it matches the expected output (pathPrefix+".out.ll"). The // output is compared with a fuzzy match that ignores some irrelevant lines such