1// Copyright 2023 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 inlheur
6
7import (
8	"bufio"
9	"encoding/json"
10	"flag"
11	"fmt"
12	"internal/testenv"
13	"os"
14	"path/filepath"
15	"regexp"
16	"strconv"
17	"strings"
18	"testing"
19	"time"
20)
21
22var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
23
24func TestFuncProperties(t *testing.T) {
25	td := t.TempDir()
26	// td = "/tmp/qqq"
27	// os.RemoveAll(td)
28	// os.Mkdir(td, 0777)
29	testenv.MustHaveGoBuild(t)
30
31	// NOTE: this testpoint has the unfortunate characteristic that it
32	// relies on the installed compiler, meaning that if you make
33	// changes to the inline heuristics code in your working copy and
34	// then run the test, it will test the installed compiler and not
35	// your local modifications. TODO: decide whether to convert this
36	// to building a fresh compiler on the fly, or using some other
37	// scheme.
38
39	testcases := []string{"funcflags", "returns", "params",
40		"acrosscall", "calls", "returns2"}
41	for _, tc := range testcases {
42		dumpfile, err := gatherPropsDumpForFile(t, tc, td)
43		if err != nil {
44			t.Fatalf("dumping func props for %q: error %v", tc, err)
45		}
46		// Read in the newly generated dump.
47		dentries, dcsites, derr := readDump(t, dumpfile)
48		if derr != nil {
49			t.Fatalf("reading func prop dump: %v", derr)
50		}
51		if *remasterflag {
52			updateExpected(t, tc, dentries, dcsites)
53			continue
54		}
55		// Generate expected dump.
56		epath, egerr := genExpected(td, tc)
57		if egerr != nil {
58			t.Fatalf("generating expected func prop dump: %v", egerr)
59		}
60		// Read in the expected result entries.
61		eentries, ecsites, eerr := readDump(t, epath)
62		if eerr != nil {
63			t.Fatalf("reading expected func prop dump: %v", eerr)
64		}
65		// Compare new vs expected.
66		n := len(dentries)
67		eidx := 0
68		for i := 0; i < n; i++ {
69			dentry := dentries[i]
70			dcst := dcsites[i]
71			if !interestingToCompare(dentry.fname) {
72				continue
73			}
74			if eidx >= len(eentries) {
75				t.Errorf("testcase %s missing expected entry for %s, skipping", tc, dentry.fname)
76				continue
77			}
78			eentry := eentries[eidx]
79			ecst := ecsites[eidx]
80			eidx++
81			if dentry.fname != eentry.fname {
82				t.Errorf("got fn %q wanted %q, skipping checks",
83					dentry.fname, eentry.fname)
84				continue
85			}
86			compareEntries(t, tc, &dentry, dcst, &eentry, ecst)
87		}
88	}
89}
90
91func propBitsToString[T interface{ String() string }](sl []T) string {
92	var sb strings.Builder
93	for i, f := range sl {
94		fmt.Fprintf(&sb, "%d: %s\n", i, f.String())
95	}
96	return sb.String()
97}
98
99func compareEntries(t *testing.T, tc string, dentry *fnInlHeur, dcsites encodedCallSiteTab, eentry *fnInlHeur, ecsites encodedCallSiteTab) {
100	dfp := dentry.props
101	efp := eentry.props
102	dfn := dentry.fname
103
104	// Compare function flags.
105	if dfp.Flags != efp.Flags {
106		t.Errorf("testcase %q: Flags mismatch for %q: got %s, wanted %s",
107			tc, dfn, dfp.Flags.String(), efp.Flags.String())
108	}
109	// Compare returns
110	rgot := propBitsToString[ResultPropBits](dfp.ResultFlags)
111	rwant := propBitsToString[ResultPropBits](efp.ResultFlags)
112	if rgot != rwant {
113		t.Errorf("testcase %q: Results mismatch for %q: got:\n%swant:\n%s",
114			tc, dfn, rgot, rwant)
115	}
116	// Compare receiver + params.
117	pgot := propBitsToString[ParamPropBits](dfp.ParamFlags)
118	pwant := propBitsToString[ParamPropBits](efp.ParamFlags)
119	if pgot != pwant {
120		t.Errorf("testcase %q: Params mismatch for %q: got:\n%swant:\n%s",
121			tc, dfn, pgot, pwant)
122	}
123	// Compare call sites.
124	for k, ve := range ecsites {
125		if vd, ok := dcsites[k]; !ok {
126			t.Errorf("testcase %q missing expected callsite %q in func %q", tc, k, dfn)
127			continue
128		} else {
129			if vd != ve {
130				t.Errorf("testcase %q callsite %q in func %q: got %+v want %+v",
131					tc, k, dfn, vd.String(), ve.String())
132			}
133		}
134	}
135	for k := range dcsites {
136		if _, ok := ecsites[k]; !ok {
137			t.Errorf("testcase %q unexpected extra callsite %q in func %q", tc, k, dfn)
138		}
139	}
140}
141
142type dumpReader struct {
143	s  *bufio.Scanner
144	t  *testing.T
145	p  string
146	ln int
147}
148
149// readDump reads in the contents of a dump file produced
150// by the "-d=dumpinlfuncprops=..." command line flag by the Go
151// compiler. It breaks the dump down into separate sections
152// by function, then deserializes each func section into a
153// fnInlHeur object and returns a slice of those objects.
154func readDump(t *testing.T, path string) ([]fnInlHeur, []encodedCallSiteTab, error) {
155	content, err := os.ReadFile(path)
156	if err != nil {
157		return nil, nil, err
158	}
159	dr := &dumpReader{
160		s:  bufio.NewScanner(strings.NewReader(string(content))),
161		t:  t,
162		p:  path,
163		ln: 1,
164	}
165	// consume header comment until preamble delimiter.
166	found := false
167	for dr.scan() {
168		if dr.curLine() == preambleDelimiter {
169			found = true
170			break
171		}
172	}
173	if !found {
174		return nil, nil, fmt.Errorf("malformed testcase file %s, missing preamble delimiter", path)
175	}
176	res := []fnInlHeur{}
177	csres := []encodedCallSiteTab{}
178	for {
179		dentry, dcst, err := dr.readEntry()
180		if err != nil {
181			t.Fatalf("reading func prop dump: %v", err)
182		}
183		if dentry.fname == "" {
184			break
185		}
186		res = append(res, dentry)
187		csres = append(csres, dcst)
188	}
189	return res, csres, nil
190}
191
192func (dr *dumpReader) scan() bool {
193	v := dr.s.Scan()
194	if v {
195		dr.ln++
196	}
197	return v
198}
199
200func (dr *dumpReader) curLine() string {
201	res := strings.TrimSpace(dr.s.Text())
202	if !strings.HasPrefix(res, "// ") {
203		dr.t.Fatalf("malformed line %s:%d, no comment: %s", dr.p, dr.ln, res)
204	}
205	return res[3:]
206}
207
208// readObjBlob reads in a series of commented lines until
209// it hits a delimiter, then returns the contents of the comments.
210func (dr *dumpReader) readObjBlob(delim string) (string, error) {
211	var sb strings.Builder
212	foundDelim := false
213	for dr.scan() {
214		line := dr.curLine()
215		if delim == line {
216			foundDelim = true
217			break
218		}
219		sb.WriteString(line + "\n")
220	}
221	if err := dr.s.Err(); err != nil {
222		return "", err
223	}
224	if !foundDelim {
225		return "", fmt.Errorf("malformed input %s, missing delimiter %q",
226			dr.p, delim)
227	}
228	return sb.String(), nil
229}
230
231// readEntry reads a single function's worth of material from
232// a file produced by the "-d=dumpinlfuncprops=..." command line
233// flag. It deserializes the json for the func properties and
234// returns the resulting properties and function name. EOF is
235// signaled by a nil FuncProps return (with no error
236func (dr *dumpReader) readEntry() (fnInlHeur, encodedCallSiteTab, error) {
237	var funcInlHeur fnInlHeur
238	var callsites encodedCallSiteTab
239	if !dr.scan() {
240		return funcInlHeur, callsites, nil
241	}
242	// first line contains info about function: file/name/line
243	info := dr.curLine()
244	chunks := strings.Fields(info)
245	funcInlHeur.file = chunks[0]
246	funcInlHeur.fname = chunks[1]
247	if _, err := fmt.Sscanf(chunks[2], "%d", &funcInlHeur.line); err != nil {
248		return funcInlHeur, callsites, fmt.Errorf("scanning line %q: %v", info, err)
249	}
250	// consume comments until and including delimiter
251	for {
252		if !dr.scan() {
253			break
254		}
255		if dr.curLine() == comDelimiter {
256			break
257		}
258	}
259
260	// Consume JSON for encoded props.
261	dr.scan()
262	line := dr.curLine()
263	fp := &FuncProps{}
264	if err := json.Unmarshal([]byte(line), fp); err != nil {
265		return funcInlHeur, callsites, err
266	}
267	funcInlHeur.props = fp
268
269	// Consume callsites.
270	callsites = make(encodedCallSiteTab)
271	for dr.scan() {
272		line := dr.curLine()
273		if line == csDelimiter {
274			break
275		}
276		// expected format: "// callsite: <expanded pos> flagstr <desc> flagval <flags> score <score> mask <scoremask> maskstr <scoremaskstring>"
277		fields := strings.Fields(line)
278		if len(fields) != 12 {
279			return funcInlHeur, nil, fmt.Errorf("malformed callsite (nf=%d) %s line %d: %s", len(fields), dr.p, dr.ln, line)
280		}
281		if fields[2] != "flagstr" || fields[4] != "flagval" || fields[6] != "score" || fields[8] != "mask" || fields[10] != "maskstr" {
282			return funcInlHeur, nil, fmt.Errorf("malformed callsite %s line %d: %s",
283				dr.p, dr.ln, line)
284		}
285		tag := fields[1]
286		flagstr := fields[5]
287		flags, err := strconv.Atoi(flagstr)
288		if err != nil {
289			return funcInlHeur, nil, fmt.Errorf("bad flags val %s line %d: %q err=%v",
290				dr.p, dr.ln, line, err)
291		}
292		scorestr := fields[7]
293		score, err2 := strconv.Atoi(scorestr)
294		if err2 != nil {
295			return funcInlHeur, nil, fmt.Errorf("bad score val %s line %d: %q err=%v",
296				dr.p, dr.ln, line, err2)
297		}
298		maskstr := fields[9]
299		mask, err3 := strconv.Atoi(maskstr)
300		if err3 != nil {
301			return funcInlHeur, nil, fmt.Errorf("bad mask val %s line %d: %q err=%v",
302				dr.p, dr.ln, line, err3)
303		}
304		callsites[tag] = propsAndScore{
305			props: CSPropBits(flags),
306			score: score,
307			mask:  scoreAdjustTyp(mask),
308		}
309	}
310
311	// Consume function delimiter.
312	dr.scan()
313	line = dr.curLine()
314	if line != fnDelimiter {
315		return funcInlHeur, nil, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter)
316	}
317
318	return funcInlHeur, callsites, nil
319}
320
321// gatherPropsDumpForFile builds the specified testcase 'testcase' from
322// testdata/props passing the "-d=dumpinlfuncprops=..." compiler option,
323// to produce a properties dump, then returns the path of the newly
324// created file. NB: we can't use "go tool compile" here, since
325// some of the test cases import stdlib packages (such as "os").
326// This means using "go build", which is problematic since the
327// Go command can potentially cache the results of the compile step,
328// causing the test to fail when being run interactively. E.g.
329//
330//	$ rm -f dump.txt
331//	$ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
332//	$ rm -f dump.txt foo.a
333//	$ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
334//	$ ls foo.a dump.txt > /dev/null
335//	ls : cannot access 'dump.txt': No such file or directory
336//	$
337//
338// For this reason, pick a unique filename for the dump, so as to
339// defeat the caching.
340func gatherPropsDumpForFile(t *testing.T, testcase string, td string) (string, error) {
341	t.Helper()
342	gopath := "testdata/props/" + testcase + ".go"
343	outpath := filepath.Join(td, testcase+".a")
344	salt := fmt.Sprintf(".p%dt%d", os.Getpid(), time.Now().UnixNano())
345	dumpfile := filepath.Join(td, testcase+salt+".dump.txt")
346	run := []string{testenv.GoToolPath(t), "build",
347		"-gcflags=-d=dumpinlfuncprops=" + dumpfile, "-o", outpath, gopath}
348	out, err := testenv.Command(t, run[0], run[1:]...).CombinedOutput()
349	if err != nil {
350		t.Logf("compile command: %+v", run)
351	}
352	if strings.TrimSpace(string(out)) != "" {
353		t.Logf("%s", out)
354	}
355	return dumpfile, err
356}
357
358// genExpected reads in a given Go testcase file, strips out all the
359// unindented (column 0) commands, writes them out to a new file, and
360// returns the path of that new file. By picking out just the comments
361// from the Go file we wind up with something that resembles the
362// output from a "-d=dumpinlfuncprops=..." compilation.
363func genExpected(td string, testcase string) (string, error) {
364	epath := filepath.Join(td, testcase+".expected")
365	outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
366	if err != nil {
367		return "", err
368	}
369	gopath := "testdata/props/" + testcase + ".go"
370	content, err := os.ReadFile(gopath)
371	if err != nil {
372		return "", err
373	}
374	lines := strings.Split(string(content), "\n")
375	for _, line := range lines[3:] {
376		if !strings.HasPrefix(line, "// ") {
377			continue
378		}
379		fmt.Fprintf(outf, "%s\n", line)
380	}
381	if err := outf.Close(); err != nil {
382		return "", err
383	}
384	return epath, nil
385}
386
387type upexState struct {
388	dentries   []fnInlHeur
389	newgolines []string
390	atline     map[uint]uint
391}
392
393func mkUpexState(dentries []fnInlHeur) *upexState {
394	atline := make(map[uint]uint)
395	for _, e := range dentries {
396		atline[e.line] = atline[e.line] + 1
397	}
398	return &upexState{
399		dentries: dentries,
400		atline:   atline,
401	}
402}
403
404// updateExpected takes a given Go testcase file X.go and writes out a
405// new/updated version of the file to X.go.new, where the column-0
406// "expected" comments have been updated using fresh data from
407// "dentries".
408//
409// Writing of expected results is complicated by closures and by
410// generics, where you can have multiple functions that all share the
411// same starting line. Currently we combine up all the dups and
412// closures into the single pre-func comment.
413func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur, dcsites []encodedCallSiteTab) {
414	nd := len(dentries)
415
416	ues := mkUpexState(dentries)
417
418	gopath := "testdata/props/" + testcase + ".go"
419	newgopath := "testdata/props/" + testcase + ".go.new"
420
421	// Read the existing Go file.
422	content, err := os.ReadFile(gopath)
423	if err != nil {
424		t.Fatalf("opening %s: %v", gopath, err)
425	}
426	golines := strings.Split(string(content), "\n")
427
428	// Preserve copyright.
429	ues.newgolines = append(ues.newgolines, golines[:4]...)
430	if !strings.HasPrefix(golines[0], "// Copyright") {
431		t.Fatalf("missing copyright from existing testcase")
432	}
433	golines = golines[4:]
434
435	clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`)
436
437	emitFunc := func(e *fnInlHeur, dcsites encodedCallSiteTab,
438		instance, atl uint) {
439		var sb strings.Builder
440		dumpFnPreamble(&sb, e, dcsites, instance, atl)
441		ues.newgolines = append(ues.newgolines,
442			strings.Split(strings.TrimSpace(sb.String()), "\n")...)
443	}
444
445	// Write file preamble with "DO NOT EDIT" message and such.
446	var sb strings.Builder
447	dumpFilePreamble(&sb)
448	ues.newgolines = append(ues.newgolines,
449		strings.Split(strings.TrimSpace(sb.String()), "\n")...)
450
451	// Helper to add a clump of functions to the output file.
452	processClump := func(idx int, emit bool) int {
453		// Process func itself, plus anything else defined
454		// on the same line
455		atl := ues.atline[dentries[idx].line]
456		for k := uint(0); k < atl; k++ {
457			if emit {
458				emitFunc(&dentries[idx], dcsites[idx], k, atl)
459			}
460			idx++
461		}
462		// now process any closures it contains
463		ncl := 0
464		for idx < nd {
465			nfn := dentries[idx].fname
466			if !clore.MatchString(nfn) {
467				break
468			}
469			ncl++
470			if emit {
471				emitFunc(&dentries[idx], dcsites[idx], 0, 1)
472			}
473			idx++
474		}
475		return idx
476	}
477
478	didx := 0
479	for _, line := range golines {
480		if strings.HasPrefix(line, "func ") {
481
482			// We have a function definition.
483			// Pick out the corresponding entry or entries in the dump
484			// and emit if interesting (or skip if not).
485			dentry := dentries[didx]
486			emit := interestingToCompare(dentry.fname)
487			didx = processClump(didx, emit)
488		}
489
490		// Consume all existing comments.
491		if strings.HasPrefix(line, "//") {
492			continue
493		}
494		ues.newgolines = append(ues.newgolines, line)
495	}
496
497	if didx != nd {
498		t.Logf("didx=%d wanted %d", didx, nd)
499	}
500
501	// Open new Go file and write contents.
502	of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
503	if err != nil {
504		t.Fatalf("opening %s: %v", newgopath, err)
505	}
506	fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n"))
507	if err := of.Close(); err != nil {
508		t.Fatalf("closing %s: %v", newgopath, err)
509	}
510
511	t.Logf("update-expected: emitted updated file %s", newgopath)
512	t.Logf("please compare the two files, then overwrite %s with %s\n",
513		gopath, newgopath)
514}
515
516// interestingToCompare returns TRUE if we want to compare results
517// for function 'fname'.
518func interestingToCompare(fname string) bool {
519	if strings.HasPrefix(fname, "init.") {
520		return true
521	}
522	if strings.HasPrefix(fname, "T_") {
523		return true
524	}
525	f := strings.Split(fname, ".")
526	if len(f) == 2 && strings.HasPrefix(f[1], "T_") {
527		return true
528	}
529	return false
530}
531