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