github.com/go-asm/go@v1.21.1-0.20240213172139-40c5ead50c48/cmd/compile/inline/inlheur/funcprops_test.go (about)

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