]> Cypherpunks.ru repositories - gostls13.git/blob - src/cmd/compile/internal/inline/inlheur/funcprops_test.go
ea2a3fc1ba546432b4548205693c4d8ba792b231
[gostls13.git] / src / cmd / compile / internal / inline / inlheur / funcprops_test.go
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
5 package inlheur
6
7 import (
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
22 var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
23
24 func 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
91 func 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
99 func 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
142 type 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.
154 func 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
192 func (dr *dumpReader) scan() bool {
193         v := dr.s.Scan()
194         if v {
195                 dr.ln++
196         }
197         return v
198 }
199
200 func (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.
210 func (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
236 func (dr *dumpReader) readEntry() (fnInlHeur, encodedCallSiteTab, error) {
237         var fih fnInlHeur
238         var callsites encodedCallSiteTab
239         if !dr.scan() {
240                 return fih, callsites, nil
241         }
242         // first line contains info about function: file/name/line
243         info := dr.curLine()
244         chunks := strings.Fields(info)
245         fih.file = chunks[0]
246         fih.fname = chunks[1]
247         if _, err := fmt.Sscanf(chunks[2], "%d", &fih.line); err != nil {
248                 return fih, 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 fih, callsites, err
266         }
267         fih.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 fih, 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 fih, 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 fih, 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 fih, 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 fih, 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 fih, nil, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter)
316         }
317
318         return fih, 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.
340 func 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 strings.TrimSpace(string(out)) != "" {
350                 t.Logf("%s", out)
351         }
352         return dumpfile, err
353 }
354
355 // genExpected reads in a given Go testcase file, strips out all the
356 // unindented (column 0) commands, writes them out to a new file, and
357 // returns the path of that new file. By picking out just the comments
358 // from the Go file we wind up with something that resembles the
359 // output from a "-d=dumpinlfuncprops=..." compilation.
360 func genExpected(td string, testcase string) (string, error) {
361         epath := filepath.Join(td, testcase+".expected")
362         outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
363         if err != nil {
364                 return "", err
365         }
366         gopath := "testdata/props/" + testcase + ".go"
367         content, err := os.ReadFile(gopath)
368         if err != nil {
369                 return "", err
370         }
371         lines := strings.Split(string(content), "\n")
372         for _, line := range lines[3:] {
373                 if !strings.HasPrefix(line, "// ") {
374                         continue
375                 }
376                 fmt.Fprintf(outf, "%s\n", line)
377         }
378         if err := outf.Close(); err != nil {
379                 return "", err
380         }
381         return epath, nil
382 }
383
384 type upexState struct {
385         dentries   []fnInlHeur
386         newgolines []string
387         atline     map[uint]uint
388 }
389
390 func mkUpexState(dentries []fnInlHeur) *upexState {
391         atline := make(map[uint]uint)
392         for _, e := range dentries {
393                 atline[e.line] = atline[e.line] + 1
394         }
395         return &upexState{
396                 dentries: dentries,
397                 atline:   atline,
398         }
399 }
400
401 // updateExpected takes a given Go testcase file X.go and writes out a
402 // new/updated version of the file to X.go.new, where the column-0
403 // "expected" comments have been updated using fresh data from
404 // "dentries".
405 //
406 // Writing of expected results is complicated by closures and by
407 // generics, where you can have multiple functions that all share the
408 // same starting line. Currently we combine up all the dups and
409 // closures into the single pre-func comment.
410 func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur, dcsites []encodedCallSiteTab) {
411         nd := len(dentries)
412
413         ues := mkUpexState(dentries)
414
415         gopath := "testdata/props/" + testcase + ".go"
416         newgopath := "testdata/props/" + testcase + ".go.new"
417
418         // Read the existing Go file.
419         content, err := os.ReadFile(gopath)
420         if err != nil {
421                 t.Fatalf("opening %s: %v", gopath, err)
422         }
423         golines := strings.Split(string(content), "\n")
424
425         // Preserve copyright.
426         ues.newgolines = append(ues.newgolines, golines[:4]...)
427         if !strings.HasPrefix(golines[0], "// Copyright") {
428                 t.Fatalf("missing copyright from existing testcase")
429         }
430         golines = golines[4:]
431
432         clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`)
433
434         emitFunc := func(e *fnInlHeur, dcsites encodedCallSiteTab,
435                 instance, atl uint) {
436                 var sb strings.Builder
437                 dumpFnPreamble(&sb, e, dcsites, instance, atl)
438                 ues.newgolines = append(ues.newgolines,
439                         strings.Split(strings.TrimSpace(sb.String()), "\n")...)
440         }
441
442         // Write file preamble with "DO NOT EDIT" message and such.
443         var sb strings.Builder
444         dumpFilePreamble(&sb)
445         ues.newgolines = append(ues.newgolines,
446                 strings.Split(strings.TrimSpace(sb.String()), "\n")...)
447
448         // Helper to add a clump of functions to the output file.
449         processClump := func(idx int, emit bool) int {
450                 // Process func itself, plus anything else defined
451                 // on the same line
452                 atl := ues.atline[dentries[idx].line]
453                 for k := uint(0); k < atl; k++ {
454                         if emit {
455                                 emitFunc(&dentries[idx], dcsites[idx], k, atl)
456                         }
457                         idx++
458                 }
459                 // now process any closures it contains
460                 ncl := 0
461                 for idx < nd {
462                         nfn := dentries[idx].fname
463                         if !clore.MatchString(nfn) {
464                                 break
465                         }
466                         ncl++
467                         if emit {
468                                 emitFunc(&dentries[idx], dcsites[idx], 0, 1)
469                         }
470                         idx++
471                 }
472                 return idx
473         }
474
475         didx := 0
476         for _, line := range golines {
477                 if strings.HasPrefix(line, "func ") {
478
479                         // We have a function definition.
480                         // Pick out the corresponding entry or entries in the dump
481                         // and emit if interesting (or skip if not).
482                         dentry := dentries[didx]
483                         emit := interestingToCompare(dentry.fname)
484                         didx = processClump(didx, emit)
485                 }
486
487                 // Consume all existing comments.
488                 if strings.HasPrefix(line, "//") {
489                         continue
490                 }
491                 ues.newgolines = append(ues.newgolines, line)
492         }
493
494         if didx != nd {
495                 t.Logf("didx=%d wanted %d", didx, nd)
496         }
497
498         // Open new Go file and write contents.
499         of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
500         if err != nil {
501                 t.Fatalf("opening %s: %v", newgopath, err)
502         }
503         fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n"))
504         if err := of.Close(); err != nil {
505                 t.Fatalf("closing %s: %v", newgopath, err)
506         }
507
508         t.Logf("update-expected: emitted updated file %s", newgopath)
509         t.Logf("please compare the two files, then overwrite %s with %s\n",
510                 gopath, newgopath)
511 }
512
513 // interestingToCompare returns TRUE if we want to compare results
514 // for function 'fname'.
515 func interestingToCompare(fname string) bool {
516         if strings.HasPrefix(fname, "init.") {
517                 return true
518         }
519         if strings.HasPrefix(fname, "T_") {
520                 return true
521         }
522         f := strings.Split(fname, ".")
523         if len(f) == 2 && strings.HasPrefix(f[1], "T_") {
524                 return true
525         }
526         return false
527 }