github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/ssa/debug_lines_test.go (about) 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 "fmt" 12 "os" 13 "path/filepath" 14 "reflect" 15 "regexp" 16 "runtime" 17 "sort" 18 "strconv" 19 "strings" 20 "testing" 21 22 "github.com/go-asm/go/testenv" 23 ) 24 25 // Matches lines in genssa output that are marked "isstmt", and the parenthesized plus-prefixed line number is a submatch 26 var asmLine *regexp.Regexp = regexp.MustCompile(`^\s[vb]\d+\s+\d+\s\(\+(\d+)\)`) 27 28 // this matches e.g. ` v123456789 000007 (+9876654310) MOVUPS X15, ""..autotmp_2-32(SP)` 29 30 // Matches lines in genssa output that describe an inlined file. 31 // Note it expects an unadventurous choice of basename. 32 var sepRE = regexp.QuoteMeta(string(filepath.Separator)) 33 var inlineLine *regexp.Regexp = regexp.MustCompile(`^#\s.*` + sepRE + `[-\w]+\.go:(\d+)`) 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 hasRegisterABI() bool { 47 switch testGoArch() { 48 case "amd64", "arm64", "loong64", "ppc64", "ppc64le", "riscv": 49 return true 50 } 51 return false 52 } 53 54 func unixOnly(t *testing.T) { 55 if runtime.GOOS != "linux" && runtime.GOOS != "darwin" { // in particular, it could be windows. 56 t.Skip("this test depends on creating a file with a wonky name, only works for sure on Linux and Darwin") 57 } 58 } 59 60 // testDebugLinesDefault removes the first wanted statement on architectures that are not (yet) register ABI. 61 func testDebugLinesDefault(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) { 62 unixOnly(t) 63 if !hasRegisterABI() { 64 wantStmts = wantStmts[1:] 65 } 66 testDebugLines(t, gcflags, file, function, wantStmts, ignoreRepeats) 67 } 68 69 func TestDebugLinesSayHi(t *testing.T) { 70 // This test is potentially fragile, the goal is that debugging should step properly through "sayhi" 71 // If the blocks are reordered in a way that changes the statement order but execution flows correctly, 72 // then rearrange the expected numbers. Register abi and not-register-abi also have different sequences, 73 // at least for now. 74 75 testDebugLinesDefault(t, "-N -l", "sayhi.go", "sayhi", []int{8, 9, 10, 11}, false) 76 } 77 78 func TestDebugLinesPushback(t *testing.T) { 79 unixOnly(t) 80 81 switch testGoArch() { 82 default: 83 t.Skip("skipped for many architectures") 84 85 case "arm64", "amd64": // register ABI 86 fn := "(*List[go.shape.int_0]).PushBack" 87 if true /* was buildcfg.Experiment.Unified */ { 88 // Unified mangles differently 89 fn = "(*List[go.shape.int]).PushBack" 90 } 91 testDebugLines(t, "-N -l", "pushback.go", fn, []int{17, 18, 19, 20, 21, 22, 24}, true) 92 } 93 } 94 95 func TestDebugLinesConvert(t *testing.T) { 96 unixOnly(t) 97 98 switch testGoArch() { 99 default: 100 t.Skip("skipped for many architectures") 101 102 case "arm64", "amd64": // register ABI 103 fn := "G[go.shape.int_0]" 104 if true /* was buildcfg.Experiment.Unified */ { 105 // Unified mangles differently 106 fn = "G[go.shape.int]" 107 } 108 testDebugLines(t, "-N -l", "convertline.go", fn, []int{9, 10, 11}, true) 109 } 110 } 111 112 func TestInlineLines(t *testing.T) { 113 if runtime.GOARCH != "amd64" && *testGoArchFlag == "" { 114 // As of september 2021, works for everything except mips64, but still potentially fragile 115 t.Skip("only runs for amd64 unless -arch explicitly supplied") 116 } 117 118 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}} 119 testInlineStack(t, "inline-dump.go", "f", want) 120 } 121 122 func TestDebugLines_53456(t *testing.T) { 123 testDebugLinesDefault(t, "-N -l", "b53456.go", "(*T).Inc", []int{15, 16, 17, 18}, true) 124 } 125 126 func compileAndDump(t *testing.T, file, function, moreGCFlags string) []byte { 127 testenv.MustHaveGoBuild(t) 128 129 tmpdir, err := os.MkdirTemp("", "debug_lines_test") 130 if err != nil { 131 panic(fmt.Sprintf("Problem creating TempDir, error %v", err)) 132 } 133 if testing.Verbose() { 134 fmt.Printf("Preserving temporary directory %s\n", tmpdir) 135 } else { 136 defer os.RemoveAll(tmpdir) 137 } 138 139 source, err := filepath.Abs(filepath.Join("testdata", file)) 140 if err != nil { 141 panic(fmt.Sprintf("Could not get abspath of testdata directory and file, %v", err)) 142 } 143 144 cmd := testenv.Command(t, testenv.GoToolPath(t), "build", "-o", "foo.o", "-gcflags=-d=ssa/genssa/dump="+function+" "+moreGCFlags, source) 145 cmd.Dir = tmpdir 146 cmd.Env = replaceEnv(cmd.Env, "GOSSADIR", tmpdir) 147 testGoos := "linux" // default to linux 148 if testGoArch() == "wasm" { 149 testGoos = "js" 150 } 151 cmd.Env = replaceEnv(cmd.Env, "GOOS", testGoos) 152 cmd.Env = replaceEnv(cmd.Env, "GOARCH", testGoArch()) 153 154 if testing.Verbose() { 155 fmt.Printf("About to run %s\n", asCommandLine("", cmd)) 156 } 157 158 var stdout, stderr strings.Builder 159 cmd.Stdout = &stdout 160 cmd.Stderr = &stderr 161 162 if err := cmd.Run(); err != nil { 163 t.Fatalf("error running cmd %s: %v\nstdout:\n%sstderr:\n%s\n", asCommandLine("", cmd), err, stdout.String(), stderr.String()) 164 } 165 166 if s := stderr.String(); s != "" { 167 t.Fatalf("Wanted empty stderr, instead got:\n%s\n", s) 168 } 169 170 dumpFile := filepath.Join(tmpdir, function+"_01__genssa.dump") 171 dumpBytes, err := os.ReadFile(dumpFile) 172 if err != nil { 173 t.Fatalf("Could not read dump file %s, err=%v", dumpFile, err) 174 } 175 return dumpBytes 176 } 177 178 func sortInlineStacks(x [][]int) { 179 sort.Slice(x, func(i, j int) bool { 180 if len(x[i]) != len(x[j]) { 181 return len(x[i]) < len(x[j]) 182 } 183 for k := range x[i] { 184 if x[i][k] != x[j][k] { 185 return x[i][k] < x[j][k] 186 } 187 } 188 return false 189 }) 190 } 191 192 // testInlineStack ensures that inlining is described properly in the comments in the dump file 193 func testInlineStack(t *testing.T, file, function string, wantStacks [][]int) { 194 // this is an inlining reporting test, not an optimization test. -N makes it less fragile 195 dumpBytes := compileAndDump(t, file, function, "-N") 196 dump := bufio.NewScanner(bytes.NewReader(dumpBytes)) 197 dumpLineNum := 0 198 var gotStmts []int 199 var gotStacks [][]int 200 for dump.Scan() { 201 line := dump.Text() 202 dumpLineNum++ 203 matches := inlineLine.FindStringSubmatch(line) 204 if len(matches) == 2 { 205 stmt, err := strconv.ParseInt(matches[1], 10, 32) 206 if err != nil { 207 t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err) 208 } 209 if testing.Verbose() { 210 fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line) 211 } 212 gotStmts = append(gotStmts, int(stmt)) 213 } else if len(gotStmts) > 0 { 214 gotStacks = append(gotStacks, gotStmts) 215 gotStmts = nil 216 } 217 } 218 if len(gotStmts) > 0 { 219 gotStacks = append(gotStacks, gotStmts) 220 gotStmts = nil 221 } 222 sortInlineStacks(gotStacks) 223 sortInlineStacks(wantStacks) 224 if !reflect.DeepEqual(wantStacks, gotStacks) { 225 t.Errorf("wanted inlines %+v but got %+v\n%s", wantStacks, gotStacks, dumpBytes) 226 } 227 228 } 229 230 // testDebugLines compiles testdata/<file> with flags -N -l and -d=ssa/genssa/dump=<function> 231 // then verifies that the statement-marked lines in that file are the same as those in wantStmts 232 // These files must all be short because this is super-fragile. 233 // "go build" is run in a temporary directory that is normally deleted, unless -test.v 234 func testDebugLines(t *testing.T, gcflags, file, function string, wantStmts []int, ignoreRepeats bool) { 235 dumpBytes := compileAndDump(t, file, function, gcflags) 236 dump := bufio.NewScanner(bytes.NewReader(dumpBytes)) 237 var gotStmts []int 238 dumpLineNum := 0 239 for dump.Scan() { 240 line := dump.Text() 241 dumpLineNum++ 242 matches := asmLine.FindStringSubmatch(line) 243 if len(matches) == 2 { 244 stmt, err := strconv.ParseInt(matches[1], 10, 32) 245 if err != nil { 246 t.Fatalf("Expected to parse a line number but saw %s instead on dump line #%d, error %v", matches[1], dumpLineNum, err) 247 } 248 if testing.Verbose() { 249 fmt.Printf("Saw stmt# %d for submatch '%s' on dump line #%d = '%s'\n", stmt, matches[1], dumpLineNum, line) 250 } 251 gotStmts = append(gotStmts, int(stmt)) 252 } 253 } 254 if ignoreRepeats { // remove repeats from gotStmts 255 newGotStmts := []int{gotStmts[0]} 256 for _, x := range gotStmts { 257 if x != newGotStmts[len(newGotStmts)-1] { 258 newGotStmts = append(newGotStmts, x) 259 } 260 } 261 if !reflect.DeepEqual(wantStmts, newGotStmts) { 262 t.Errorf("wanted stmts %v but got %v (with repeats still in: %v)", wantStmts, newGotStmts, gotStmts) 263 } 264 265 } else { 266 if !reflect.DeepEqual(wantStmts, gotStmts) { 267 t.Errorf("wanted stmts %v but got %v", wantStmts, gotStmts) 268 } 269 } 270 }