]> Cypherpunks.ru repositories - gostls13.git/commitdiff
cmd/compile/internal/inline: add framework to compute func "properties"
authorThan McIntosh <thanm@google.com>
Thu, 29 Jun 2023 19:37:26 +0000 (15:37 -0400)
committerThan McIntosh <thanm@google.com>
Thu, 10 Aug 2023 18:53:06 +0000 (18:53 +0000)
Add some machinery to support computing function "properties" for use
in driving inlining heuristics, and a unit testing framework to check
to see if the property computations are correct for a given set of
canned Go source files. This CL is mainly the analysis skeleton and a
testing framework; the code to compute the actual props will arrive in
a later patch.

Updates #61502.

Change-Id: I7970b64f713d17d7fdd7e8e9ccc7d9b0490571bf
Reviewed-on: https://go-review.googlesource.com/c/go/+/511557
Reviewed-by: Matthew Dempsky <mdempsky@google.com>
Run-TryBot: Than McIntosh <thanm@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>

src/cmd/compile/internal/base/debug.go
src/cmd/compile/internal/inline/inl.go
src/cmd/compile/internal/inline/inlheur/analyze.go [new file with mode: 0644]
src/cmd/compile/internal/inline/inlheur/funcprops_test.go [new file with mode: 0644]
src/cmd/compile/internal/inline/inlheur/testdata/props/README.txt [new file with mode: 0644]
src/cmd/compile/internal/inline/inlheur/testdata/props/stub.go [new file with mode: 0644]
src/cmd/compile/internal/inline/inlheur/trace_off.go [new file with mode: 0644]
src/cmd/compile/internal/inline/inlheur/trace_on.go [new file with mode: 0644]

index 1f05ed98318876f0645f170d59981970a9b1914b..36a75ae8e502d3fa48f5c5c19b9b0ec4c193a434 100644 (file)
@@ -21,6 +21,7 @@ type DebugFlags struct {
        Closure               int    `help:"print information about closure compilation"`
        Defer                 int    `help:"print information about defer compilation"`
        DisableNil            int    `help:"disable nil checks" concurrent:"ok"`
+       DumpInlFuncProps      string `help:"dump function properties from inl heuristics to specified file"`
        DumpPtrs              int    `help:"show Node pointers values in dump output"`
        DwarfInl              int    `help:"print information about DWARF inlined function creation"`
        Export                int    `help:"print export data"`
index 4ae7fa95d20c3733ddffff1226aed8edfacf233a..00a8bb52e3f5ec19d40c966fd2b0f9f63a76042c 100644 (file)
@@ -33,6 +33,7 @@ import (
        "strconv"
 
        "cmd/compile/internal/base"
+       "cmd/compile/internal/inline/inlheur"
        "cmd/compile/internal/ir"
        "cmd/compile/internal/logopt"
        "cmd/compile/internal/pgo"
@@ -166,6 +167,10 @@ func InlinePackage(p *pgo.Profile) {
        // are no longer reachable from top-level functions following
        // inlining. See #59404 and #59638 for more context.
        garbageCollectUnreferencedHiddenClosures()
+
+       if base.Debug.DumpInlFuncProps != "" {
+               inlheur.DumpFuncProps(nil, base.Debug.DumpInlFuncProps)
+       }
 }
 
 // InlineDecls applies inlining to the given batch of declarations.
@@ -269,6 +274,10 @@ func CanInline(fn *ir.Func, profile *pgo.Profile) {
                base.Fatalf("CanInline no nname %+v", fn)
        }
 
+       if base.Debug.DumpInlFuncProps != "" {
+               defer inlheur.DumpFuncProps(fn, base.Debug.DumpInlFuncProps)
+       }
+
        var reason string // reason, if any, that the function was not inlined
        if base.Flag.LowerM > 1 || logopt.Enabled() {
                defer func() {
diff --git a/src/cmd/compile/internal/inline/inlheur/analyze.go b/src/cmd/compile/internal/inline/inlheur/analyze.go
new file mode 100644 (file)
index 0000000..29ca956
--- /dev/null
@@ -0,0 +1,168 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package inlheur
+
+import (
+       "cmd/compile/internal/base"
+       "cmd/compile/internal/ir"
+       "encoding/json"
+       "fmt"
+       "io"
+       "os"
+       "path/filepath"
+       "sort"
+       "strings"
+)
+
+const (
+       debugTraceFuncs = 1 << iota
+)
+
+// fnInlHeur contains inline heuristics state information about
+// a specific Go function being analyzed/considered by the inliner.
+type fnInlHeur struct {
+       fname string
+       file  string
+       line  uint
+       props *FuncProps
+}
+
+// computeFuncProps examines the Go function 'fn' and computes for it
+// a function "properties" object, to be used to drive inlining
+// heuristics. See comments on the FuncProps type for more info.
+func computeFuncProps(fn *ir.Func) *FuncProps {
+       if debugTrace&debugTraceFuncs != 0 {
+               fmt.Fprintf(os.Stderr, "=-= starting analysis of func %v:\n%+v\n",
+                       fn.Sym().Name, fn)
+       }
+       // implementation stubbed out for now
+       return &FuncProps{}
+}
+
+func fnFileLine(fn *ir.Func) (string, uint) {
+       p := base.Ctxt.InnermostPos(fn.Pos())
+       return filepath.Base(p.Filename()), p.Line()
+}
+
+// DumpFuncProps computes and caches function properties for the func
+// 'fn', or if fn is nil, writes out the cached set of properties to
+// the file given in 'dumpfile'. Used for the "-d=dumpinlfuncprops=..."
+// command line flag, intended for use primarily in unit testing.
+func DumpFuncProps(fn *ir.Func, dumpfile string) {
+       if fn != nil {
+               captureFuncDumpEntry(fn)
+       } else {
+               emitDumpToFile(dumpfile)
+       }
+}
+
+// emitDumpToFile writes out the buffer function property dump entries
+// to a file, for unit testing. Dump entries need to be sorted by
+// definition line, and due to generics we need to account for the
+// possibility that several ir.Func's will have the same def line.
+func emitDumpToFile(dumpfile string) {
+       outf, err := os.OpenFile(dumpfile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+       if err != nil {
+               base.Fatalf("opening function props dump file %q: %v\n", dumpfile, err)
+       }
+       defer outf.Close()
+       dumpFilePreamble(outf)
+
+       atline := map[uint]uint{}
+       sl := make([]fnInlHeur, 0, len(dumpBuffer))
+       for _, e := range dumpBuffer {
+               sl = append(sl, e)
+               atline[e.line] = atline[e.line] + 1
+       }
+       sl = sortFnInlHeurSlice(sl)
+
+       prevline := uint(0)
+       for _, entry := range sl {
+               idx := uint(0)
+               if prevline == entry.line {
+                       idx++
+               }
+               prevline = entry.line
+               atl := atline[entry.line]
+               if err := dumpFnPreamble(outf, &entry, idx, atl); err != nil {
+                       base.Fatalf("function props dump: %v\n", err)
+               }
+       }
+       dumpBuffer = nil
+}
+
+// captureFuncDumpEntry analyzes function 'fn' and adds a entry
+// for it to 'dumpBuffer'. Used for unit testing.
+func captureFuncDumpEntry(fn *ir.Func) {
+       // avoid capturing compiler-generated equality funcs.
+       if strings.HasPrefix(fn.Sym().Name, ".eq.") {
+               return
+       }
+       if dumpBuffer == nil {
+               dumpBuffer = make(map[*ir.Func]fnInlHeur)
+       }
+       if _, ok := dumpBuffer[fn]; ok {
+               // we can wind up seeing closures multiple times here,
+               // so don't add them more than once.
+               return
+       }
+       fp := computeFuncProps(fn)
+       file, line := fnFileLine(fn)
+       entry := fnInlHeur{
+               fname: fn.Sym().Name,
+               file:  file,
+               line:  line,
+               props: fp,
+       }
+       dumpBuffer[fn] = entry
+}
+
+// dumpFilePreamble writes out a file-level preamble for a given
+// Go function as part of a function properties dump.
+func dumpFilePreamble(w io.Writer) {
+       fmt.Fprintf(w, "// DO NOT EDIT (use 'go test -v -update-expected' instead.)\n")
+       fmt.Fprintf(w, "// See cmd/compile/internal/inline/inlheur/testdata/props/README.txt\n")
+       fmt.Fprintf(w, "// for more information on the format of this file.\n")
+       fmt.Fprintf(w, "// %s\n", preambleDelimiter)
+}
+
+// dumpFilePreamble writes out a function-level preamble for a given
+// Go function as part of a function properties dump. See the
+// README.txt file in testdata/props for more on the format of
+// this preamble.
+func dumpFnPreamble(w io.Writer, fih *fnInlHeur, idx, atl uint) error {
+       fmt.Fprintf(w, "// %s %s %d %d %d\n",
+               fih.file, fih.fname, fih.line, idx, atl)
+       // emit props as comments, followed by delimiter
+       fmt.Fprintf(w, "%s// %s\n", fih.props.ToString("// "), comDelimiter)
+       data, err := json.Marshal(fih.props)
+       if err != nil {
+               return fmt.Errorf("marshall error %v\n", err)
+       }
+       fmt.Fprintf(w, "// %s\n// %s\n", string(data), fnDelimiter)
+       return nil
+}
+
+// sortFnInlHeurSlice sorts a slice of fnInlHeur based on
+// the starting line of the function definition, then by name.
+func sortFnInlHeurSlice(sl []fnInlHeur) []fnInlHeur {
+       sort.SliceStable(sl, func(i, j int) bool {
+               if sl[i].line != sl[j].line {
+                       return sl[i].line < sl[j].line
+               }
+               return sl[i].fname < sl[j].fname
+       })
+       return sl
+}
+
+// delimiters written to various preambles to make parsing of
+// dumps easier.
+const preambleDelimiter = "<endfilepreamble>"
+const fnDelimiter = "<endfuncpreamble>"
+const comDelimiter = "<endpropsdump>"
+
+// dumpBuffer stores up function properties dumps when
+// "-d=dumpinlfuncprops=..." is in effect.
+var dumpBuffer map[*ir.Func]fnInlHeur
diff --git a/src/cmd/compile/internal/inline/inlheur/funcprops_test.go b/src/cmd/compile/internal/inline/inlheur/funcprops_test.go
new file mode 100644 (file)
index 0000000..47e3418
--- /dev/null
@@ -0,0 +1,462 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+package inlheur
+
+import (
+       "bufio"
+       "encoding/json"
+       "flag"
+       "fmt"
+       "internal/testenv"
+       "os"
+       "path/filepath"
+       "regexp"
+       "strings"
+       "testing"
+       "time"
+)
+
+var remasterflag = flag.Bool("update-expected", false, "if true, generate updated golden results in testcases for all props tests")
+
+func TestFuncProperties(t *testing.T) {
+       td := t.TempDir()
+       //td = "/tmp/qqq"
+       //os.RemoveAll(td)
+       //os.Mkdir(td, 0777)
+       testenv.MustHaveGoBuild(t)
+
+       // NOTE: this testpoint has the unfortunate characteristic that it
+       // relies on the installed compiler, meaning that if you make
+       // changes to the inline heuristics code in your working copy and
+       // then run the test, it will test the installed compiler and not
+       // your local modifications. TODO: decide whether to convert this
+       // to building a fresh compiler on the fly, or using some other
+       // scheme.
+
+       testcases := []string{"stub"}
+
+       for _, tc := range testcases {
+               dumpfile, err := gatherPropsDumpForFile(t, tc, td)
+               if err != nil {
+                       t.Fatalf("dumping func props for %q: error %v", tc, err)
+               }
+               // Read in the newly generated dump.
+               dentries, derr := readDump(t, dumpfile)
+               if derr != nil {
+                       t.Fatalf("reading func prop dump: %v", derr)
+               }
+               if *remasterflag {
+                       updateExpected(t, tc, dentries)
+                       continue
+               }
+               // Generate expected dump.
+               epath, gerr := genExpected(td, tc)
+               if gerr != nil {
+                       t.Fatalf("generating expected func prop dump: %v", gerr)
+               }
+               // Read in the expected result entries.
+               eentries, eerr := readDump(t, epath)
+               if eerr != nil {
+                       t.Fatalf("reading expected func prop dump: %v", eerr)
+               }
+               // Compare new vs expected.
+               n := len(dentries)
+               eidx := 0
+               for i := 0; i < n; i++ {
+                       dentry := dentries[i]
+                       if !interestingToCompare(dentry.fname) {
+                               continue
+                       }
+                       if eidx >= len(eentries) {
+                               t.Errorf("missing expected entry for %s, skipping",
+                                       dentry.fname)
+                               continue
+                       }
+                       eentry := eentries[eidx]
+                       eidx++
+                       if dentry.fname != eentry.fname {
+                               t.Errorf("got fn %q wanted %q, skipping checks",
+                                       dentry.fname, eentry.fname)
+                               continue
+                       }
+                       compareEntries(t, tc, &dentry, &eentry)
+               }
+       }
+}
+
+func propBitsToString[T interface{ String() string }](sl []T) string {
+       var sb strings.Builder
+       for i, f := range sl {
+               fmt.Fprintf(&sb, "%d: %s\n", i, f.String())
+       }
+       return sb.String()
+}
+
+func compareEntries(t *testing.T, tc string, dentry *fnInlHeur, eentry *fnInlHeur) {
+       dfp := dentry.props
+       efp := eentry.props
+       dfn := dentry.fname
+
+       // Compare function flags.
+       if dfp.Flags != efp.Flags {
+               t.Errorf("testcase %s: Flags mismatch for %q: got %s, wanted %s",
+                       tc, dfn, dfp.Flags.String(), efp.Flags.String())
+       }
+       // Compare returns
+       rgot := propBitsToString[ResultPropBits](dfp.ResultFlags)
+       rwant := propBitsToString[ResultPropBits](efp.ResultFlags)
+       if rgot != rwant {
+               t.Errorf("Results mismatch for %q: got:\n%swant:\n%s",
+                       dfn, rgot, rwant)
+       }
+       // Compare receiver + params.
+       pgot := propBitsToString[ParamPropBits](dfp.ParamFlags)
+       pwant := propBitsToString[ParamPropBits](efp.ParamFlags)
+       if pgot != pwant {
+               t.Errorf("Params mismatch for %q: got:\n%swant:\n%s",
+                       dfn, pgot, pwant)
+       }
+}
+
+type dumpReader struct {
+       s  *bufio.Scanner
+       t  *testing.T
+       p  string
+       ln int
+}
+
+// readDump reads in the contents of a dump file produced
+// by the "-d=dumpinlfuncprops=..." command line flag by the Go
+// compiler. It breaks the dump down into separate sections
+// by function, then deserializes each func section into a
+// fnInlHeur object and returns a slice of those objects.
+func readDump(t *testing.T, path string) ([]fnInlHeur, error) {
+       content, err := os.ReadFile(path)
+       if err != nil {
+               return nil, err
+       }
+       dr := &dumpReader{
+               s:  bufio.NewScanner(strings.NewReader(string(content))),
+               t:  t,
+               p:  path,
+               ln: 1,
+       }
+       // consume header comment until preamble delimiter.
+       found := false
+       for dr.scan() {
+               if dr.curLine() == preambleDelimiter {
+                       found = true
+                       break
+               }
+       }
+       if !found {
+               return nil, fmt.Errorf("malformed testcase file %s, missing preamble delimiter", path)
+       }
+       res := []fnInlHeur{}
+       for {
+               dentry, err := dr.readEntry()
+               if err != nil {
+                       t.Fatalf("reading func prop dump: %v", err)
+               }
+               if dentry.fname == "" {
+                       break
+               }
+               res = append(res, dentry)
+       }
+       return res, nil
+}
+
+func (dr *dumpReader) scan() bool {
+       v := dr.s.Scan()
+       if v {
+               dr.ln++
+       }
+       return v
+}
+
+func (dr *dumpReader) curLine() string {
+       res := strings.TrimSpace(dr.s.Text())
+       if !strings.HasPrefix(res, "// ") {
+               dr.t.Fatalf("malformed line %s:%d, no comment: %s", dr.p, dr.ln, res)
+       }
+       return res[3:]
+}
+
+// readObjBlob reads in a series of commented lines until
+// it hits a delimiter, then returns the contents of the comments.
+func (dr *dumpReader) readObjBlob(delim string) (string, error) {
+       var sb strings.Builder
+       foundDelim := false
+       for dr.scan() {
+               line := dr.curLine()
+               if delim == line {
+                       foundDelim = true
+                       break
+               }
+               sb.WriteString(line + "\n")
+       }
+       if err := dr.s.Err(); err != nil {
+               return "", err
+       }
+       if !foundDelim {
+               return "", fmt.Errorf("malformed input %s, missing delimiter %q",
+                       dr.p, delim)
+       }
+       return sb.String(), nil
+}
+
+// readEntry reads a single function's worth of material from
+// a file produced by the "-d=dumpinlfuncprops=..." command line
+// flag. It deserializes the json for the func properties and
+// returns the resulting properties and function name. EOF is
+// signaled by a nil FuncProps return (with no error
+func (dr *dumpReader) readEntry() (fnInlHeur, error) {
+       var fih fnInlHeur
+       if !dr.scan() {
+               return fih, nil
+       }
+       // first line contains info about function: file/name/line
+       info := dr.curLine()
+       chunks := strings.Fields(info)
+       fih.file = chunks[0]
+       fih.fname = chunks[1]
+       if _, err := fmt.Sscanf(chunks[2], "%d", &fih.line); err != nil {
+               return fih, err
+       }
+       // consume comments until and including delimiter
+       for {
+               if !dr.scan() {
+                       break
+               }
+               if dr.curLine() == comDelimiter {
+                       break
+               }
+       }
+
+       // Consume JSON for encoded props.
+       dr.scan()
+       line := dr.curLine()
+       fp := &FuncProps{}
+       if err := json.Unmarshal([]byte(line), fp); err != nil {
+               return fih, err
+       }
+       fih.props = fp
+
+       // Consume delimiter.
+       dr.scan()
+       line = dr.curLine()
+       if line != fnDelimiter {
+               return fih, fmt.Errorf("malformed testcase file %q, missing delimiter %q", dr.p, fnDelimiter)
+       }
+
+       return fih, nil
+}
+
+// gatherPropsDumpForFile builds the specified testcase 'testcase' from
+// testdata/props passing the "-d=dumpinlfuncprops=..." compiler option,
+// to produce a properties dump, then returns the path of the newly
+// created file. NB: we can't use "go tool compile" here, since
+// some of the test cases import stdlib packages (such as "os").
+// This means using "go build", which is problematic since the
+// Go command can potentially cache the results of the compile step,
+// causing the test to fail when being run interactively. E.g.
+//
+//     $ rm -f dump.txt
+//     $ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
+//     $ rm -f dump.txt foo.a
+//     $ go build -o foo.a -gcflags=-d=dumpinlfuncprops=dump.txt foo.go
+//     $ ls foo.a dump.txt > /dev/null
+//     ls : cannot access 'dump.txt': No such file or directory
+//     $
+//
+// For this reason, pick a unique filename for the dump, so as to
+// defeat the caching.
+func gatherPropsDumpForFile(t *testing.T, testcase string, td string) (string, error) {
+       t.Helper()
+       gopath := "testdata/props/" + testcase + ".go"
+       outpath := filepath.Join(td, testcase+".a")
+       salt := fmt.Sprintf(".p%dt%d", os.Getpid(), time.Now().UnixNano())
+       dumpfile := filepath.Join(td, testcase+salt+".dump.txt")
+       run := []string{testenv.GoToolPath(t), "build",
+               "-gcflags=-d=dumpinlfuncprops=" + dumpfile, "-o", outpath, gopath}
+       out, err := testenv.Command(t, run[0], run[1:]...).CombinedOutput()
+       if strings.TrimSpace(string(out)) != "" {
+               t.Logf("%s", out)
+       }
+       return dumpfile, err
+}
+
+// genExpected reads in a given Go testcase file, strips out all the
+// unindented (column 0) commands, writes them out to a new file, and
+// returns the path of that new file. By picking out just the comments
+// from the Go file we wind up with something that resembles the
+// output from a "-d=dumpinlfuncprops=..." compilation.
+func genExpected(td string, testcase string) (string, error) {
+       epath := filepath.Join(td, testcase+".expected")
+       outf, err := os.OpenFile(epath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+       if err != nil {
+               return "", err
+       }
+       gopath := "testdata/props/" + testcase + ".go"
+       content, err := os.ReadFile(gopath)
+       if err != nil {
+               return "", err
+       }
+       lines := strings.Split(string(content), "\n")
+       for _, line := range lines[3:] {
+               if !strings.HasPrefix(line, "// ") {
+                       continue
+               }
+               fmt.Fprintf(outf, "%s\n", line)
+       }
+       if err := outf.Close(); err != nil {
+               return "", err
+       }
+       return epath, nil
+}
+
+type upexState struct {
+       dentries   []fnInlHeur
+       newgolines []string
+       atline     map[uint]uint
+}
+
+func mkUpexState(dentries []fnInlHeur) *upexState {
+       atline := make(map[uint]uint)
+       for _, e := range dentries {
+               atline[e.line] = atline[e.line] + 1
+       }
+       return &upexState{
+               dentries: dentries,
+               atline:   atline,
+       }
+}
+
+// updateExpected takes a given Go testcase file X.go and writes out a
+// new/updated version of the file to X.go.new, where the column-0
+// "expected" comments have been updated using fresh data from
+// "dentries".
+//
+// Writing of expected results is complicated by closures and by
+// generics, where you can have multiple functions that all share the
+// same starting line. Currently we combine up all the dups and
+// closures into the single pre-func comment.
+func updateExpected(t *testing.T, testcase string, dentries []fnInlHeur) {
+       nd := len(dentries)
+
+       ues := mkUpexState(dentries)
+
+       gopath := "testdata/props/" + testcase + ".go"
+       newgopath := "testdata/props/" + testcase + ".go.new"
+
+       // Read the existing Go file.
+       content, err := os.ReadFile(gopath)
+       if err != nil {
+               t.Fatalf("opening %s: %v", gopath, err)
+       }
+       golines := strings.Split(string(content), "\n")
+
+       // Preserve copyright.
+       ues.newgolines = append(ues.newgolines, golines[:4]...)
+       if !strings.HasPrefix(golines[0], "// Copyright") {
+               t.Fatalf("missing copyright from existing testcase")
+       }
+       golines = golines[4:]
+
+       clore := regexp.MustCompile(`.+\.func\d+[\.\d]*$`)
+
+       emitFunc := func(e *fnInlHeur, instance, atl uint) {
+               var sb strings.Builder
+               dumpFnPreamble(&sb, e, instance, atl)
+               ues.newgolines = append(ues.newgolines,
+                       strings.Split(strings.TrimSpace(sb.String()), "\n")...)
+       }
+
+       // Write file preamble with "DO NOT EDIT" message and such.
+       var sb strings.Builder
+       dumpFilePreamble(&sb)
+       ues.newgolines = append(ues.newgolines,
+               strings.Split(strings.TrimSpace(sb.String()), "\n")...)
+
+       // Helper to add a clump of functions to the output file.
+       processClump := func(idx int, emit bool) int {
+               // Process func itself, plus anything else defined
+               // on the same line
+               atl := ues.atline[dentries[idx].line]
+               for k := uint(0); k < atl; k++ {
+                       if emit {
+                               emitFunc(&dentries[idx], k, atl)
+                       }
+                       idx++
+               }
+               // now process any closures it contains
+               ncl := 0
+               for idx < nd {
+                       nfn := dentries[idx].fname
+                       if !clore.MatchString(nfn) {
+                               break
+                       }
+                       ncl++
+                       if emit {
+                               emitFunc(&dentries[idx], 0, 1)
+                       }
+                       idx++
+               }
+               return idx
+       }
+
+       didx := 0
+       for _, line := range golines {
+               if strings.HasPrefix(line, "func ") {
+
+                       // We have a function definition.
+                       // Pick out the corresponding entry or entries in the dump
+                       // and emit if interesting (or skip if not).
+                       dentry := dentries[didx]
+                       emit := interestingToCompare(dentry.fname)
+                       didx = processClump(didx, emit)
+               }
+
+               // Consume all existing comments.
+               if strings.HasPrefix(line, "//") {
+                       continue
+               }
+               ues.newgolines = append(ues.newgolines, line)
+       }
+
+       if didx != nd {
+               t.Logf("didx=%d wanted %d", didx, nd)
+       }
+
+       // Open new Go file and write contents.
+       of, err := os.OpenFile(newgopath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
+       if err != nil {
+               t.Fatalf("opening %s: %v", newgopath, err)
+       }
+       fmt.Fprintf(of, "%s", strings.Join(ues.newgolines, "\n"))
+       if err := of.Close(); err != nil {
+               t.Fatalf("closing %s: %v", newgopath, err)
+       }
+
+       t.Logf("update-expected: emitted updated file %s", newgopath)
+       t.Logf("please compare the two files, then overwrite %s with %s\n",
+               gopath, newgopath)
+}
+
+// interestingToCompare returns TRUE if we want to compare results
+// for function 'fname'.
+func interestingToCompare(fname string) bool {
+       if strings.HasPrefix(fname, "init.") {
+               return true
+       }
+       if strings.HasPrefix(fname, "T_") {
+               return true
+       }
+       f := strings.Split(fname, ".")
+       if len(f) == 2 && strings.HasPrefix(f[1], "T_") {
+               return true
+       }
+       return false
+}
diff --git a/src/cmd/compile/internal/inline/inlheur/testdata/props/README.txt b/src/cmd/compile/internal/inline/inlheur/testdata/props/README.txt
new file mode 100644 (file)
index 0000000..815c892
--- /dev/null
@@ -0,0 +1,75 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+Notes on the format of the testcase files in
+cmd/compile/internal/inline/inlheur/testdata/props:
+
+- each (compilable) file contains input Go code and expected results
+  in the form of column-0 comments.
+
+- functions or methods that begin with "T_" are targeted for testing,
+  as well as "init" functions; all other functions are ignored.
+
+- function header comments begin with a line containing
+  the file name, function name, definition line, then index
+  and a count of the number of funcs that share that same
+  definition line (needed to support generics). Example:
+
+         // foo.go T_mumble 35 1 4
+
+  Here "T_mumble" is defined at line 35, and it is func 0
+  out of the 4 funcs that share that same line.
+
+- function property expected results appear as comments in immediately
+  prior to the function. For example, here we have first the function
+  name ("T_feeds_if_simple"), then human-readable dump of the function
+  properties, as well as the JSON for the properties object, each
+  section separated by a "<>" delimiter.
+
+         // funcflags.go T_feeds_if_simple 35 0 1
+         // RecvrParamFlags:
+         //   0: ParamFeedsIfOrSwitch
+         // <endpropsdump>
+         // {"Flags":0,"RecvrParamFlags":[8],"ReturnFlags":[]}
+         // <endfuncpreamble>
+         func T_feeds_if_simple(x int) {
+               if x < 100 {
+                       os.Exit(1)
+               }
+               println(x)
+         }
+
+- when the test runs, it will compile the Go source file with an
+  option to dump out function properties, then compare the new dump
+  for each function with the JSON appearing in the header comment for
+  the function (in the example above, the JSON appears between
+  "<endpropsdump>" and "<endfuncpreamble>". The material prior to the
+  dump is simply there for human consumption, so that a developer can
+  easily see that "RecvrParamFlags":[8] means that the first parameter
+  has flag ParamFeedsIfOrSwitch.
+
+- when making changes to the compiler (which can alter the expected
+  results) or edits/additions to the go code in the testcase files,
+  you can remaster the results by running
+
+    go test -v -count=1 .
+
+  In the trace output of this run, you'll see messages of the form
+
+      === RUN   TestFuncProperties
+       funcprops_test.go:NNN: update-expected: emitted updated file
+                              testdata/props/XYZ.go.new
+       funcprops_test.go:MMM: please compare the two files, then overwrite
+                              testdata/props/XYZ.go with testdata/props/XYZ.go.new
+
+  at which point you can compare the old and new files by hand, then
+  overwrite the *.go file with the *.go.new file if you are happy with
+  the diffs.
+
+- note that the remastering process will strip out any existing
+  column-0 (unindented) comments; if you write comments that you
+  want to see preserved, use "/* */" or indent them.
+
+
+
diff --git a/src/cmd/compile/internal/inline/inlheur/testdata/props/stub.go b/src/cmd/compile/internal/inline/inlheur/testdata/props/stub.go
new file mode 100644 (file)
index 0000000..2e43edd
--- /dev/null
@@ -0,0 +1,107 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+// DO NOT EDIT (use 'go test -v -update-expected' instead.)
+// See cmd/compile/internal/inline/inlheur/testdata/props/README.txt
+// for more information on the format of this file.
+// <endfilepreamble>
+
+package stub
+
+// stub.go T_stub 16 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func T_stub() {
+}
+
+func ThisFunctionShouldBeIgnored(x int) {
+       println(x)
+}
+
+// stub.go init.0 27 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func init() {
+       ThisFunctionShouldBeIgnored(1)
+}
+
+// stub.go T_contains_closures 43 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+// stub.go T_contains_closures.func1 44 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+// stub.go T_contains_closures.func2 46 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func T_contains_closures(q int) func() {
+       f := func() { M["a"] = 9 }
+       f()
+       f2 := func() { M["a"] = 4 }
+       if M["b"] != 9 {
+               return f
+       }
+       return f2
+}
+
+// stub.go T_Unique[go.shape.int] 69 0 4
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+// stub.go T_Unique[go.shape.string] 69 1 4
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+// stub.go T_Unique[int] 69 2 4
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+// stub.go T_Unique[string] 69 3 4
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func T_Unique[T comparable](set []T) []T {
+       nset := make([]T, 0, 8)
+loop:
+       for _, s := range set {
+               for _, e := range nset {
+                       if s == e {
+                               continue loop
+                       }
+               }
+               nset = append(nset, s)
+       }
+
+       return nset
+}
+
+// stub.go T_uniq_int_count 88 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func T_uniq_int_count(s []int) int {
+       return len(T_Unique[int](s))
+}
+
+// stub.go T_uniq_string_count 96 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func T_uniq_string_count(s []string) int {
+       return len(T_Unique[string](s))
+}
+
+// stub.go T_epilog 104 0 1
+// <endpropsdump>
+// {"Flags":0,"ParamFlags":null,"ResultFlags":null}
+// <endfuncpreamble>
+func T_epilog() {
+}
+
+var M = map[string]int{}
diff --git a/src/cmd/compile/internal/inline/inlheur/trace_off.go b/src/cmd/compile/internal/inline/inlheur/trace_off.go
new file mode 100644 (file)
index 0000000..1e03770
--- /dev/null
@@ -0,0 +1,15 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build !debugtrace
+
+package inlheur
+
+const debugTrace = 0
+
+func enableDebugTrace(x int) {
+}
+
+func disableDebugTrace() {
+}
diff --git a/src/cmd/compile/internal/inline/inlheur/trace_on.go b/src/cmd/compile/internal/inline/inlheur/trace_on.go
new file mode 100644 (file)
index 0000000..7164b60
--- /dev/null
@@ -0,0 +1,17 @@
+// Copyright 2023 The Go Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style
+// license that can be found in the LICENSE file.
+
+//go:build debugtrace
+
+package inlheur
+
+var debugTrace = 0
+
+func enableDebugTrace(x int) {
+       debugTrace = x
+}
+
+func disableDebugTrace() {
+       debugTrace = 0
+}