Skip to content

Commit af72ddf

Browse files
committed
cmd/compile: extend dump-to-file to handle "genssa" (asm) case.
Extend the existing dump-to-file to also do assembly output to make it easier to write debug-information tests that check for line-numbering in particular orders. Includes POC test (which is silent w/o -v): go test -v -run TestDebugLines cmd/compile/internal/ssa === RUN TestDebugLines Preserving temporary directory /var/folders/v6/xyzzy/T/debug_lines_test321 About to run (cd /var/folders/v6/xyzzy/T/debug_lines_test321; \ GOSSADIR=/var/folders/v6/xyzzy/T/debug_lines_test321 \ /Users/drchase/work/go/bin/go build -o foo.o \ '-gcflags=-N -l -d=ssa/genssa/dump=sayhi' \ /Users/drchase/work/go/src/cmd/compile/internal/ssa/testdata/sayhi.go ) Saw stmt# 8 for submatch '8' on dump line #7 = ' v107 00005 (+8) MOVQ AX, "".n(SP)' Saw stmt# 9 for submatch '9' on dump line #9 = ' v87 00007 (+9) MOVUPS X15, ""..autotmp_2-32(SP)' Saw stmt# 10 for submatch '10' on dump line #46 = ' v65 00044 (+10) MOVUPS X15, ""..autotmp_2-32(SP)' Saw stmt# 11 for submatch '11' on dump line #83 = ' v131 00081 (+11) MOVQ "".wg+8(SP), AX' --- PASS: TestDebugLines (4.95s) PASS ok cmd/compile/internal/ssa 5.685s Includes a test to ensure that inlining information is printed correctly. Updates #47880. Change-Id: I83b596476a88687d71d5b65dbb94641a576d747e Reviewed-on: https://go-review.googlesource.com/c/go/+/348970 Trust: David Chase <[email protected]> Run-TryBot: David Chase <[email protected]> TryBot-Result: Go Bot <[email protected]> Reviewed-by: Keith Randall <[email protected]>
1 parent 3c764ba commit af72ddf

File tree

8 files changed

+360
-23
lines changed

8 files changed

+360
-23
lines changed

src/cmd/compile/internal/ssa/compile.go

Lines changed: 43 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -10,9 +10,11 @@ import (
1010
"fmt"
1111
"hash/crc32"
1212
"internal/buildcfg"
13+
"io"
1314
"log"
1415
"math/rand"
1516
"os"
17+
"path/filepath"
1618
"regexp"
1719
"runtime"
1820
"sort"
@@ -59,7 +61,7 @@ func Compile(f *Func) {
5961
printFunc(f)
6062
}
6163
f.HTMLWriter.WritePhase("start", "start")
62-
if BuildDump != "" && BuildDump == f.Name {
64+
if BuildDump[f.Name] {
6365
f.dumpFile("build")
6466
}
6567
if checkEnabled {
@@ -163,25 +165,37 @@ func Compile(f *Func) {
163165
phaseName = ""
164166
}
165167

166-
// dumpFile creates a file from the phase name and function name
167-
// Dumping is done to files to avoid buffering huge strings before
168-
// output.
169-
func (f *Func) dumpFile(phaseName string) {
168+
// DumpFileForPhase creates a file from the function name and phase name,
169+
// warning and returning nil if this is not possible.
170+
func (f *Func) DumpFileForPhase(phaseName string) io.WriteCloser {
170171
f.dumpFileSeq++
171172
fname := fmt.Sprintf("%s_%02d__%s.dump", f.Name, int(f.dumpFileSeq), phaseName)
172173
fname = strings.Replace(fname, " ", "_", -1)
173174
fname = strings.Replace(fname, "/", "_", -1)
174175
fname = strings.Replace(fname, ":", "_", -1)
175176

177+
if ssaDir := os.Getenv("GOSSADIR"); ssaDir != "" {
178+
fname = filepath.Join(ssaDir, fname)
179+
}
180+
176181
fi, err := os.Create(fname)
177182
if err != nil {
178183
f.Warnl(src.NoXPos, "Unable to create after-phase dump file %s", fname)
179-
return
184+
return nil
180185
}
186+
return fi
187+
}
181188

182-
p := stringFuncPrinter{w: fi}
183-
fprintFunc(p, f)
184-
fi.Close()
189+
// dumpFile creates a file from the phase name and function name
190+
// Dumping is done to files to avoid buffering huge strings before
191+
// output.
192+
func (f *Func) dumpFile(phaseName string) {
193+
fi := f.DumpFileForPhase(phaseName)
194+
if fi != nil {
195+
p := stringFuncPrinter{w: fi}
196+
fprintFunc(p, f)
197+
fi.Close()
198+
}
185199
}
186200

187201
type pass struct {
@@ -224,7 +238,9 @@ var IntrinsicsDisable bool
224238
var BuildDebug int
225239
var BuildTest int
226240
var BuildStats int
227-
var BuildDump string // name of function to dump after initial build of ssa
241+
var BuildDump map[string]bool = make(map[string]bool) // names of functions to dump after initial build of ssa
242+
243+
var GenssaDump map[string]bool = make(map[string]bool) // names of functions to dump after ssa has been converted to asm
228244

229245
// PhaseOption sets the specified flag in the specified ssa phase,
230246
// returning empty string if this was successful or a string explaining
@@ -248,7 +264,7 @@ func PhaseOption(phase, flag string, val int, valString string) string {
248264
switch phase {
249265
case "", "help":
250266
lastcr := 0
251-
phasenames := " check, all, build, intrinsics"
267+
phasenames := " check, all, build, intrinsics, genssa"
252268
for _, p := range passes {
253269
pn := strings.Replace(p.name, " ", "_", -1)
254270
if len(pn)+len(phasenames)-lastcr > 70 {
@@ -278,6 +294,7 @@ where:
278294
279295
Phase "all" supports flags "time", "mem", and "dump".
280296
Phase "intrinsics" supports flags "on", "off", and "debug".
297+
Phase "genssa" (assembly generation) supports the flag "dump".
281298
282299
If the "dump" flag is specified, the output is written on a file named
283300
<phase>__<function_name>_<seq>.dump; otherwise it is directed to stdout.
@@ -339,10 +356,11 @@ commas. For example:
339356
case "dump":
340357
alldump = val != 0
341358
if alldump {
342-
BuildDump = valString
359+
BuildDump[valString] = true
360+
GenssaDump[valString] = true
343361
}
344362
default:
345-
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option", flag, phase)
363+
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/all/{time,mem,dump=function_name})", flag, phase)
346364
}
347365
}
348366

@@ -355,7 +373,7 @@ commas. For example:
355373
case "debug":
356374
IntrinsicsDebug = val
357375
default:
358-
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option", flag, phase)
376+
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/intrinsics/{on,off,debug})", flag, phase)
359377
}
360378
return ""
361379
}
@@ -368,9 +386,18 @@ commas. For example:
368386
case "stats":
369387
BuildStats = val
370388
case "dump":
371-
BuildDump = valString
389+
BuildDump[valString] = true
390+
default:
391+
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/build/{debug,test,stats,dump=function_name})", flag, phase)
392+
}
393+
return ""
394+
}
395+
if phase == "genssa" {
396+
switch flag {
397+
case "dump":
398+
GenssaDump[valString] = true
372399
default:
373-
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option", flag, phase)
400+
return fmt.Sprintf("Did not find a flag matching %s in -d=ssa/%s debug option (expected ssa/genssa/dump=function_name)", flag, phase)
374401
}
375402
return ""
376403
}
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Copyright 2021 The Go Authors. All rights reserved.
2+
// Use of this source code is governed by a BSD-style
3+
// license that can be found in the LICENSE file.
4+
5+
package ssa_test
6+
7+
import (
8+
"bufio"
9+
"bytes"
10+
"flag"
11+
"runtime"
12+
"sort"
13+
14+
// "flag"
15+
"fmt"
16+
"internal/testenv"
17+
"io/ioutil"
18+
"os"
19+
"os/exec"
20+
"path/filepath"
21+
"reflect"
22+
"regexp"
23+
"strconv"
24+
"testing"
25+
)
26+
27+
// Matches lines in genssa output that are marked "isstmt", and the parenthesized plus-prefixed line number is a submatch
28+
var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb][0-9]+\s+[0-9]+\s\(\+([0-9]+)\)`)
29+
30+
// this matches e.g. ` v123456789 000007 (+9876654310) MOVUPS X15, ""..autotmp_2-32(SP)`
31+
32+
// Matches lines in genssa output that describe an inlined file (on a Unix filesystem). Note it expects an unadventurous choice of basename.
33+
var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s/.*/[-a-zA-Z0-9_]+\.go:([0-9]+)`)
34+
35+
// this matches e.g. # /pa/inline-dumpxxxx.go:6
36+
37+
var testGoArchFlag = flag.String("arch", "", "run test for specified architecture")
38+
39+
func testGoArch() string {
40+
if *testGoArchFlag == "" {
41+
return runtime.GOARCH
42+
}
43+
return *testGoArchFlag
44+
}
45+
46+
func TestDebugLines(t *testing.T) {
47+
if runtime.GOOS == "windows" {
48+
t.Skip("Windows lacks $HOME which complicates workaround for 'missing $GOPATH'") // $HOME needed to work around #43938
49+
}
50+
// This test is potentially fragile, the goal is that debugging should step properly through "sayhi"
51+
// If the blocks are reordered in a way that changes the statement order but execution flows correctly,
52+
// then rearrange the expected numbers. Register abi and not-register-abi also have different sequences,
53+
// at least for now.
54+
55+
switch testGoArch() {
56+
case "arm64", "amd64": // register ABI
57+
testDebugLines(t, "sayhi.go", "sayhi", []int{8, 9, 10, 11})
58+
59+
case "arm", "386": // probably not register ABI for a while
60+
testDebugLines(t, "sayhi.go", "sayhi", []int{9, 10, 11})
61+
62+
default: // expect ppc64le and riscv will pick up register ABI soonish, not sure about others
63+
t.Skip("skipped for many architectures, also changes w/ register ABI")
64+
}
65+
}
66+
67+
func TestInlineLines(t *testing.T) {
68+
if runtime.GOOS == "windows" {
69+
t.Skip("Windows lacks $HOME which complicates workaround for 'missing $GOPATH'") // $HOME needed to work around #43938
70+
}
71+
if runtime.GOARCH != "amd64" && *testGoArchFlag == "" {
72+
// As of september 2021, works for everything except mips64, but still potentially fragile
73+
t.Skip("only runs for amd64 unless -arch explicitly supplied")
74+
}
75+
76+
want := [][]int{{3}, {4, 10}, {4, 10, 16}, {4, 10}, {4, 11, 16}, {4, 11}, {4}, {5, 10}, {5, 10, 16}, {5, 10}, {5, 11, 16}, {5, 11}, {5}}
77+
testInlineStack(t, "inline-dump.go", "f", want)
78+
}
79+
80+
func compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte {
81+
testenv.MustHaveGoBuild(t)
82+
83+
tmpdir, err := ioutil.TempDir("", "debug_lines_test")
84+
if err != nil {
85+
panic(fmt.Sprintf("Problem creating TempDir, error %v", err))
86+
}
87+
if testing.Verbose() {
88+
fmt.Printf("Preserving temporary directory %s\n", tmpdir)
89+
} else {
90+
defer os.RemoveAll(tmpdir)
91+
}
92+
93+
source, err := filepath.Abs(filepath.Join("testdata", file))
94+
if err != nil {
95+
panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err))
96+
}
97+
98+
cmd := exec.Command(testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source)
99+
cmd.Dir = tmpdir
100+
cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir)
101+
cmd.Env = replaceEnv(cmd.Env, "HOME", os.Getenv("HOME")) // workaround for #43938
102+
testGoos := "linux" // default to linux
103+
if testGoArch() == "wasm" {
104+
testGoos = "js"
105+
}
106+
cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos)
107+
cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch())
108+
109+
if testing.Verbose() {
110+
fmt.Printf("About to run %s\n", asCommandLine("", cmd))
111+
}
112+
113+
var stdout, stderr bytes.Buffer
114+
cmd.Stdout = &stdout
115+
cmd.Stderr = &stderr
116+
117+
if err := cmd.Run(); err != nil {
118+
t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String())
119+
}
120+
121+
if s := stderr.String(); s != "" {
122+
t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s)
123+
}
124+
125+
dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump")
126+
dumpBytes, err := os.ReadFile(dumpFile)
127+
if err != nil {
128+
t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err)
129+
}
130+
return dumpBytes
131+
}
132+
133+
func sortInlineStacks(x [][]int) {
134+
sort.Slice(x, func(i, j int) bool {
135+
if len(x[i]) != len(x[j]) {
136+
return len(x[i]) < len(x[j])
137+
}
138+
for k := range x[i] {
139+
if x[i][k] != x[j][k] {
140+
return x[i][k] < x[j][k]
141+
}
142+
}
143+
return false
144+
})
145+
}
146+
147+
// testInlineStack ensures that inlining is described properly in the comments in the dump file
148+
func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) {
149+
// this is an inlining reporting test, not an optimization test. -N makes it less fragile
150+
dumpBytes := compileAndDump(t, file, function, "-N")
151+
dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
152+
dumpLineNum := 0
153+
var gotStmts []int
154+
var gotStacks [][]int
155+
for dump.Scan() {
156+
line := dump.Text()
157+
dumpLineNum++
158+
matches := inlineLine.FindStringSubmatch(line)
159+
if len(matches) == 2 {
160+
stmt, err := strconv.ParseInt(matches[1], 10, 32)
161+
if err != nil {
162+
t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
163+
}
164+
if testing.Verbose() {
165+
fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
166+
}
167+
gotStmts = append(gotStmts, int(stmt))
168+
} else if len(gotStmts) > 0 {
169+
gotStacks = append(gotStacks, gotStmts)
170+
gotStmts = nil
171+
}
172+
}
173+
if len(gotStmts) > 0 {
174+
gotStacks = append(gotStacks, gotStmts)
175+
gotStmts = nil
176+
}
177+
sortInlineStacks(gotStacks)
178+
sortInlineStacks(wantStacks)
179+
if !reflect.DeepEqual(wantStacks, gotStacks) {
180+
t.Errorf("wanted inlines %+v but got %+v", wantStacks, gotStacks)
181+
}
182+
183+
}
184+
185+
// testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function>
186+
// then verifies that the statement-marked lines in that file are the same as those in wantStmts
187+
// These files must all be short because this is super-fragile.
188+
// "go build" is run in a temporary directory that is normally deleted, unless -test.v
189+
func testDebugLines(t *testing.T, file, function string, wantStmts []int) {
190+
dumpBytes := compileAndDump(t, file, function, "-N -l")
191+
dump := bufio.NewScanner(bytes.NewReader(dumpBytes))
192+
var gotStmts []int
193+
dumpLineNum := 0
194+
for dump.Scan() {
195+
line := dump.Text()
196+
dumpLineNum++
197+
matches := asmLine.FindStringSubmatch(line)
198+
if len(matches) == 2 {
199+
stmt, err := strconv.ParseInt(matches[1], 10, 32)
200+
if err != nil {
201+
t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err)
202+
}
203+
if testing.Verbose() {
204+
fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line)
205+
}
206+
gotStmts = append(gotStmts, int(stmt))
207+
}
208+
}
209+
if !reflect.DeepEqual(wantStmts, gotStmts) {
210+
t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts)
211+
}
212+
213+
}

src/cmd/compile/internal/ssa/func.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ type Func struct {
4343
logfiles map[string]writeSyncer
4444
HTMLWriter *HTMLWriter // html writer, for debugging
4545
DebugTest bool // default true unless $GOSSAHASH != ""; as a debugging aid, make new code conditional on this and use GOSSAHASH to binary search for failing cases
46-
PrintOrHtmlSSA bool // true if GOSSAFUNC matches, true even if fe.Log() (spew phase results to stdout) is false.
46+
PrintOrHtmlSSA bool // true if GOSSAFUNC matches, true even if fe.Log() (spew phase results to stdout) is false. There's an odd dependence on this in debug.go for method logf.
4747
ruleMatches map[string]int // number of times countRule was called during compilation for any given string
4848
ABI0 *abi.ABIConfig // A copy, for no-sync access
4949
ABI1 *abi.ABIConfig // A copy, for no-sync access

src/cmd/compile/internal/ssa/print.go

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ package ssa
66

77
import (
88
"bytes"
9+
"cmd/internal/src"
910
"crypto/sha256"
1011
"fmt"
1112
"io"
@@ -83,13 +84,26 @@ func (p stringFuncPrinter) endBlock(b *Block, reachable bool) {
8384
fmt.Fprintln(p.w, " "+b.LongString())
8485
}
8586

87+
func StmtString(p src.XPos) string {
88+
linenumber := "(?) "
89+
if p.IsKnown() {
90+
pfx := ""
91+
if p.IsStmt() == src.PosIsStmt {
92+
pfx = "+"
93+
}
94+
if p.IsStmt() == src.PosNotStmt {
95+
pfx = "-"
96+
}
97+
linenumber = fmt.Sprintf("(%s%d) ", pfx, p.Line())
98+
}
99+
return linenumber
100+
}
101+
86102
func (p stringFuncPrinter) value(v *Value, live bool) {
87103
if !p.printDead && !live {
88104
return
89105
}
90-
fmt.Fprint(p.w, " ")
91-
//fmt.Fprint(p.w, v.Block.Func.fe.Pos(v.Pos))
92-
//fmt.Fprint(p.w, ": ")
106+
fmt.Fprintf(p.w, " %s", StmtString(v.Pos))
93107
fmt.Fprint(p.w, v.LongString())
94108
if !live {
95109
fmt.Fprint(p.w, " DEAD")

0 commit comments

Comments
 (0)