summaryrefslogtreecommitdiffstats
path: root/src/cmd/compile/internal/logopt
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 13:16:40 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-28 13:16:40 +0000
commit47ab3d4a42e9ab51c465c4322d2ec233f6324e6b (patch)
treea61a0ffd83f4a3def4b36e5c8e99630c559aa723 /src/cmd/compile/internal/logopt
parentInitial commit. (diff)
downloadgolang-1.18-47ab3d4a42e9ab51c465c4322d2ec233f6324e6b.tar.xz
golang-1.18-47ab3d4a42e9ab51c465c4322d2ec233f6324e6b.zip
Adding upstream version 1.18.10.upstream/1.18.10upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'src/cmd/compile/internal/logopt')
-rw-r--r--src/cmd/compile/internal/logopt/escape.go14
-rw-r--r--src/cmd/compile/internal/logopt/escape_bootstrap.go13
-rw-r--r--src/cmd/compile/internal/logopt/log_opts.go523
-rw-r--r--src/cmd/compile/internal/logopt/logopt_test.go258
4 files changed, 808 insertions, 0 deletions
diff --git a/src/cmd/compile/internal/logopt/escape.go b/src/cmd/compile/internal/logopt/escape.go
new file mode 100644
index 0000000..9660e93
--- /dev/null
+++ b/src/cmd/compile/internal/logopt/escape.go
@@ -0,0 +1,14 @@
+// Copyright 2019 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 go1.8
+// +build go1.8
+
+package logopt
+
+import "net/url"
+
+func pathEscape(s string) string {
+ return url.PathEscape(s)
+}
diff --git a/src/cmd/compile/internal/logopt/escape_bootstrap.go b/src/cmd/compile/internal/logopt/escape_bootstrap.go
new file mode 100644
index 0000000..cc04eaa
--- /dev/null
+++ b/src/cmd/compile/internal/logopt/escape_bootstrap.go
@@ -0,0 +1,13 @@
+// Copyright 2019 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 !go1.8
+// +build !go1.8
+
+package logopt
+
+// For bootstrapping with an early version of Go
+func pathEscape(s string) string {
+ panic("This should never be called; the compiler is not fully bootstrapped if it is.")
+}
diff --git a/src/cmd/compile/internal/logopt/log_opts.go b/src/cmd/compile/internal/logopt/log_opts.go
new file mode 100644
index 0000000..97ebf56
--- /dev/null
+++ b/src/cmd/compile/internal/logopt/log_opts.go
@@ -0,0 +1,523 @@
+// Copyright 2019 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 logopt
+
+import (
+ "cmd/internal/obj"
+ "cmd/internal/src"
+ "encoding/json"
+ "fmt"
+ "internal/buildcfg"
+ "io"
+ "log"
+ "net/url"
+ "os"
+ "path/filepath"
+ "sort"
+ "strconv"
+ "strings"
+ "sync"
+ "unicode"
+)
+
+// This implements (non)optimization logging for -json option to the Go compiler
+// The option is -json 0,<destination>.
+//
+// 0 is the version number; to avoid the need for synchronized updates, if
+// new versions of the logging appear, the compiler will support both, for a while,
+// and clients will specify what they need.
+//
+// <destination> is a directory.
+// Directories are specified with a leading / or os.PathSeparator,
+// or more explicitly with file://directory. The second form is intended to
+// deal with corner cases on Windows, and to allow specification of a relative
+// directory path (which is normally a bad idea, because the local directory
+// varies a lot in a build, especially with modules and/or vendoring, and may
+// not be writeable).
+//
+// For each package pkg compiled, a url.PathEscape(pkg)-named subdirectory
+// is created. For each source file.go in that package that generates
+// diagnostics (no diagnostics means no file),
+// a url.PathEscape(file)+".json"-named file is created and contains the
+// logged diagnostics.
+//
+// For example, "cmd%2Finternal%2Fdwarf/%3Cautogenerated%3E.json"
+// for "cmd/internal/dwarf" and <autogenerated> (which is not really a file, but the compiler sees it)
+//
+// If the package string is empty, it is replaced internally with string(0) which encodes to %00.
+//
+// Each log file begins with a JSON record identifying version,
+// platform, and other context, followed by optimization-relevant
+// LSP Diagnostic records, one per line (LSP version 3.15, no difference from 3.14 on the subset used here
+// see https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/ )
+//
+// The fields of a Diagnostic are used in the following way:
+// Range: the outermost source position, for now begin and end are equal.
+// Severity: (always) SeverityInformation (3)
+// Source: (always) "go compiler"
+// Code: a string describing the missed optimization, e.g., "nilcheck", "cannotInline", "isInBounds", "escape"
+// Message: depending on code, additional information, e.g., the reason a function cannot be inlined.
+// RelatedInformation: if the missed optimization actually occurred at a function inlined at Range,
+// then the sequence of inlined locations appears here, from (second) outermost to innermost,
+// each with message="inlineLoc".
+//
+// In the case of escape analysis explanations, after any outer inlining locations,
+// the lines of the explanation appear, each potentially followed with its own inlining
+// location if the escape flow occurred within an inlined function.
+//
+// For example <destination>/cmd%2Fcompile%2Finternal%2Fssa/prove.json
+// might begin with the following line (wrapped for legibility):
+//
+// {"version":0,"package":"cmd/compile/internal/ssa","goos":"darwin","goarch":"amd64",
+// "gc_version":"devel +e1b9a57852 Fri Nov 1 15:07:00 2019 -0400",
+// "file":"/Users/drchase/work/go/src/cmd/compile/internal/ssa/prove.go"}
+//
+// and later contain (also wrapped for legibility):
+//
+// {"range":{"start":{"line":191,"character":24},"end":{"line":191,"character":24}},
+// "severity":3,"code":"nilcheck","source":"go compiler","message":"",
+// "relatedInformation":[
+// {"location":{"uri":"file:///Users/drchase/work/go/src/cmd/compile/internal/ssa/func.go",
+// "range":{"start":{"line":153,"character":16},"end":{"line":153,"character":16}}},
+// "message":"inlineLoc"}]}
+//
+// That is, at prove.go (implicit from context, provided in both filename and header line),
+// line 191, column 24, a nilcheck occurred in the generated code.
+// The relatedInformation indicates that this code actually came from
+// an inlined call to func.go, line 153, character 16.
+//
+// prove.go:191:
+// ft.orderS = f.newPoset()
+// func.go:152 and 153:
+// func (f *Func) newPoset() *poset {
+// if len(f.Cache.scrPoset) > 0 {
+//
+// In the case that the package is empty, the string(0) package name is also used in the header record, for example
+//
+// go tool compile -json=0,file://logopt x.go # no -p option to set the package
+// head -1 logopt/%00/x.json
+// {"version":0,"package":"\u0000","goos":"darwin","goarch":"amd64","gc_version":"devel +86487adf6a Thu Nov 7 19:34:56 2019 -0500","file":"x.go"}
+
+type VersionHeader struct {
+ Version int `json:"version"`
+ Package string `json:"package"`
+ Goos string `json:"goos"`
+ Goarch string `json:"goarch"`
+ GcVersion string `json:"gc_version"`
+ File string `json:"file,omitempty"` // LSP requires an enclosing resource, i.e., a file
+}
+
+// DocumentURI, Position, Range, Location, Diagnostic, DiagnosticRelatedInformation all reuse json definitions from gopls.
+// See https://github.com/golang/tools/blob/22afafe3322a860fcd3d88448768f9db36f8bc5f/internal/lsp/protocol/tsprotocol.go
+
+type DocumentURI string
+
+type Position struct {
+ Line uint `json:"line"` // gopls uses float64, but json output is the same for integers
+ Character uint `json:"character"` // gopls uses float64, but json output is the same for integers
+}
+
+// A Range in a text document expressed as (zero-based) start and end positions.
+// A range is comparable to a selection in an editor. Therefore the end position is exclusive.
+// If you want to specify a range that contains a line including the line ending character(s)
+// then use an end position denoting the start of the next line.
+type Range struct {
+ /*Start defined:
+ * The range's start position
+ */
+ Start Position `json:"start"`
+
+ /*End defined:
+ * The range's end position
+ */
+ End Position `json:"end"` // exclusive
+}
+
+// A Location represents a location inside a resource, such as a line inside a text file.
+type Location struct {
+ // URI is
+ URI DocumentURI `json:"uri"`
+
+ // Range is
+ Range Range `json:"range"`
+}
+
+/* DiagnosticRelatedInformation defined:
+ * Represents a related message and source code location for a diagnostic. This should be
+ * used to point to code locations that cause or related to a diagnostics, e.g when duplicating
+ * a symbol in a scope.
+ */
+type DiagnosticRelatedInformation struct {
+
+ /*Location defined:
+ * The location of this related diagnostic information.
+ */
+ Location Location `json:"location"`
+
+ /*Message defined:
+ * The message of this related diagnostic information.
+ */
+ Message string `json:"message"`
+}
+
+// DiagnosticSeverity defines constants
+type DiagnosticSeverity uint
+
+const (
+ /*SeverityInformation defined:
+ * Reports an information.
+ */
+ SeverityInformation DiagnosticSeverity = 3
+)
+
+// DiagnosticTag defines constants
+type DiagnosticTag uint
+
+/*Diagnostic defined:
+ * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
+ * are only valid in the scope of a resource.
+ */
+type Diagnostic struct {
+
+ /*Range defined:
+ * The range at which the message applies
+ */
+ Range Range `json:"range"`
+
+ /*Severity defined:
+ * The diagnostic's severity. Can be omitted. If omitted it is up to the
+ * client to interpret diagnostics as error, warning, info or hint.
+ */
+ Severity DiagnosticSeverity `json:"severity,omitempty"` // always SeverityInformation for optimizer logging.
+
+ /*Code defined:
+ * The diagnostic's code, which usually appear in the user interface.
+ */
+ Code string `json:"code,omitempty"` // LSP uses 'number | string' = gopls interface{}, but only string here, e.g. "boundsCheck", "nilcheck", etc.
+
+ /*Source defined:
+ * A human-readable string describing the source of this
+ * diagnostic, e.g. 'typescript' or 'super lint'. It usually
+ * appears in the user interface.
+ */
+ Source string `json:"source,omitempty"` // "go compiler"
+
+ /*Message defined:
+ * The diagnostic's message. It usually appears in the user interface
+ */
+ Message string `json:"message"` // sometimes used, provides additional information.
+
+ /*Tags defined:
+ * Additional metadata about the diagnostic.
+ */
+ Tags []DiagnosticTag `json:"tags,omitempty"` // always empty for logging optimizations.
+
+ /*RelatedInformation defined:
+ * An array of related diagnostic information, e.g. when symbol-names within
+ * a scope collide all definitions can be marked via this property.
+ */
+ RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
+}
+
+// A LoggedOpt is what the compiler produces and accumulates,
+// to be converted to JSON for human or IDE consumption.
+type LoggedOpt struct {
+ pos src.XPos // Source code position at which the event occurred. If it is inlined, outer and all inlined locations will appear in JSON.
+ compilerPass string // Compiler pass. For human/adhoc consumption; does not appear in JSON (yet)
+ functionName string // Function name. For human/adhoc consumption; does not appear in JSON (yet)
+ what string // The (non) optimization; "nilcheck", "boundsCheck", "inline", "noInline"
+ target []interface{} // Optional target(s) or parameter(s) of "what" -- what was inlined, why it was not, size of copy, etc. 1st is most important/relevant.
+}
+
+type logFormat uint8
+
+const (
+ None logFormat = iota
+ Json0 // version 0 for LSP 3.14, 3.15; future versions of LSP may change the format and the compiler may need to support both as clients are updated.
+)
+
+var Format = None
+var dest string
+
+// LogJsonOption parses and validates the version,directory value attached to the -json compiler flag.
+func LogJsonOption(flagValue string) {
+ version, directory := parseLogFlag("json", flagValue)
+ if version != 0 {
+ log.Fatal("-json version must be 0")
+ }
+ dest = checkLogPath(directory)
+ Format = Json0
+}
+
+// parseLogFlag checks the flag passed to -json
+// for version,destination format and returns the two parts.
+func parseLogFlag(flag, value string) (version int, directory string) {
+ if Format != None {
+ log.Fatal("Cannot repeat -json flag")
+ }
+ commaAt := strings.Index(value, ",")
+ if commaAt <= 0 {
+ log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag)
+ }
+ v, err := strconv.Atoi(value[:commaAt])
+ if err != nil {
+ log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err)
+ }
+ version = v
+ directory = value[commaAt+1:]
+ return
+}
+
+// isWindowsDriveURI returns true if the file URI is of the format used by
+// Windows URIs. The url.Parse package does not specially handle Windows paths
+// (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
+// (copied from tools/internal/span/uri.go)
+// this is less comprehensive that the processing in filepath.IsAbs on Windows.
+func isWindowsDriveURIPath(uri string) bool {
+ if len(uri) < 4 {
+ return false
+ }
+ return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
+}
+
+func parseLogPath(destination string) (string, string) {
+ if filepath.IsAbs(destination) {
+ return filepath.Clean(destination), ""
+ }
+ if strings.HasPrefix(destination, "file://") { // IKWIAD, or Windows C:\foo\bar\baz
+ uri, err := url.Parse(destination)
+ if err != nil {
+ return "", fmt.Sprintf("optimizer logging destination looked like file:// URI but failed to parse: err=%v", err)
+ }
+ destination = uri.Host + uri.Path
+ if isWindowsDriveURIPath(destination) {
+ // strip leading / from /C:
+ // unlike tools/internal/span/uri.go, do not uppercase the drive letter -- let filepath.Clean do what it does.
+ destination = destination[1:]
+ }
+ return filepath.Clean(destination), ""
+ }
+ return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file://-prefixed file URI", destination, string(filepath.Separator))
+}
+
+// checkLogPath does superficial early checking of the string specifying
+// the directory to which optimizer logging is directed, and if
+// it passes the test, stores the string in LO_dir
+func checkLogPath(destination string) string {
+ path, complaint := parseLogPath(destination)
+ if complaint != "" {
+ log.Fatalf(complaint)
+ }
+ err := os.MkdirAll(path, 0755)
+ if err != nil {
+ log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err)
+ }
+ return path
+}
+
+var loggedOpts []*LoggedOpt
+var mu = sync.Mutex{} // mu protects loggedOpts.
+
+// NewLoggedOpt allocates a new LoggedOpt, to later be passed to either NewLoggedOpt or LogOpt as "args".
+// Pos is the source position (including inlining), what is the message, pass is which pass created the message,
+// funcName is the name of the function
+// A typical use for this to accumulate an explanation for a missed optimization, for example, why did something escape?
+func NewLoggedOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt {
+ pass = strings.Replace(pass, " ", "_", -1)
+ return &LoggedOpt{pos, pass, funcName, what, args}
+}
+
+// Logopt logs information about a (usually missed) optimization performed by the compiler.
+// Pos is the source position (including inlining), what is the message, pass is which pass created the message,
+// funcName is the name of the function
+func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) {
+ if Format == None {
+ return
+ }
+ lo := NewLoggedOpt(pos, what, pass, funcName, args...)
+ mu.Lock()
+ defer mu.Unlock()
+ // Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
+ loggedOpts = append(loggedOpts, lo)
+}
+
+// Enabled returns whether optimization logging is enabled.
+func Enabled() bool {
+ switch Format {
+ case None:
+ return false
+ case Json0:
+ return true
+ }
+ panic("Unexpected optimizer-logging level")
+}
+
+// byPos sorts diagnostics by source position.
+type byPos struct {
+ ctxt *obj.Link
+ a []*LoggedOpt
+}
+
+func (x byPos) Len() int { return len(x.a) }
+func (x byPos) Less(i, j int) bool {
+ return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos))
+}
+func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] }
+
+func writerForLSP(subdirpath, file string) io.WriteCloser {
+ basename := file
+ lastslash := strings.LastIndexAny(basename, "\\/")
+ if lastslash != -1 {
+ basename = basename[lastslash+1:]
+ }
+ lastdot := strings.LastIndex(basename, ".go")
+ if lastdot != -1 {
+ basename = basename[:lastdot]
+ }
+ basename = pathEscape(basename)
+
+ // Assume a directory, make a file
+ p := filepath.Join(subdirpath, basename+".json")
+ w, err := os.Create(p)
+ if err != nil {
+ log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err)
+ }
+ return w
+}
+
+func fixSlash(f string) string {
+ if os.PathSeparator == '/' {
+ return f
+ }
+ return strings.Replace(f, string(os.PathSeparator), "/", -1)
+}
+
+func uriIfy(f string) DocumentURI {
+ url := url.URL{
+ Scheme: "file",
+ Path: fixSlash(f),
+ }
+ return DocumentURI(url.String())
+}
+
+// Return filename, replacing a first occurrence of $GOROOT with the
+// actual value of the GOROOT (because LSP does not speak "$GOROOT").
+func uprootedPath(filename string) string {
+ if !strings.HasPrefix(filename, "$GOROOT/") {
+ return filename
+ }
+ return buildcfg.GOROOT + filename[len("$GOROOT"):]
+}
+
+// FlushLoggedOpts flushes all the accumulated optimization log entries.
+func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
+ if Format == None {
+ return
+ }
+
+ sort.Stable(byPos{ctxt, loggedOpts}) // Stable is necessary to preserve the per-function order, which is repeatable.
+ switch Format {
+
+ case Json0: // LSP 3.15
+ var posTmp []src.Pos
+ var encoder *json.Encoder
+ var w io.WriteCloser
+
+ if slashPkgPath == "" {
+ slashPkgPath = "\000"
+ }
+ subdirpath := filepath.Join(dest, pathEscape(slashPkgPath))
+ err := os.MkdirAll(subdirpath, 0755)
+ if err != nil {
+ log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err)
+ }
+ diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation}
+
+ // For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory.
+ currentFile := ""
+ for _, x := range loggedOpts {
+ posTmp, p0 := x.parsePos(ctxt, posTmp)
+ p0f := uprootedPath(p0.Filename())
+
+ if currentFile != p0f {
+ if w != nil {
+ w.Close()
+ }
+ currentFile = p0f
+ w = writerForLSP(subdirpath, currentFile)
+ encoder = json.NewEncoder(w)
+ encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile})
+ }
+
+ // The first "target" is the most important one.
+ var target string
+ if len(x.target) > 0 {
+ target = fmt.Sprint(x.target[0])
+ }
+
+ diagnostic.Code = x.what
+ diagnostic.Message = target
+ diagnostic.Range = newPointRange(p0)
+ diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]
+
+ appendInlinedPos(posTmp, &diagnostic)
+
+ // Diagnostic explanation is stored in RelatedInformation after inlining info
+ if len(x.target) > 1 {
+ switch y := x.target[1].(type) {
+ case []*LoggedOpt:
+ for _, z := range y {
+ posTmp, p0 := z.parsePos(ctxt, posTmp)
+ loc := newLocation(p0)
+ msg := z.what
+ if len(z.target) > 0 {
+ msg = msg + ": " + fmt.Sprint(z.target[0])
+ }
+
+ diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
+ appendInlinedPos(posTmp, &diagnostic)
+ }
+ }
+ }
+
+ encoder.Encode(diagnostic)
+ }
+ if w != nil {
+ w.Close()
+ }
+ }
+}
+
+// newPointRange returns a single-position Range for the compiler source location p.
+func newPointRange(p src.Pos) Range {
+ return Range{Start: Position{p.Line(), p.Col()},
+ End: Position{p.Line(), p.Col()}}
+}
+
+// newLocation returns the Location for the compiler source location p
+func newLocation(p src.Pos) Location {
+ loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)}
+ return loc
+}
+
+// appendInlinedPos extracts inlining information from posTmp and append it to diagnostic
+func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) {
+ for i := 1; i < len(posTmp); i++ {
+ p := posTmp[i]
+ loc := newLocation(p)
+ diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
+ }
+}
+
+func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) {
+ posTmp = ctxt.AllPos(x.pos, posTmp)
+ // Reverse posTmp to put outermost first.
+ l := len(posTmp)
+ for i := 0; i < l/2; i++ {
+ posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
+ }
+ p0 := posTmp[0]
+ return posTmp, p0
+}
diff --git a/src/cmd/compile/internal/logopt/logopt_test.go b/src/cmd/compile/internal/logopt/logopt_test.go
new file mode 100644
index 0000000..902cbc8
--- /dev/null
+++ b/src/cmd/compile/internal/logopt/logopt_test.go
@@ -0,0 +1,258 @@
+// Copyright 2019 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 logopt
+
+import (
+ "internal/testenv"
+ "io/ioutil"
+ "os"
+ "os/exec"
+ "path/filepath"
+ "runtime"
+ "strings"
+ "testing"
+)
+
+const srcCode = `package x
+type pair struct {a,b int}
+func bar(y *pair) *int {
+ return &y.b
+}
+var a []int
+func foo(w, z *pair) *int {
+ if *bar(w) > 0 {
+ return bar(z)
+ }
+ if a[1] > 0 {
+ a = a[:2]
+ }
+ return &a[0]
+}
+
+// address taking prevents closure inlining
+func n() int {
+ foo := func() int { return 1 }
+ bar := &foo
+ x := (*bar)() + foo()
+ return x
+}
+`
+
+func want(t *testing.T, out string, desired string) {
+ // On Windows, Unicode escapes in the JSON output end up "normalized" elsewhere to /u....,
+ // so "normalize" what we're looking for to match that.
+ s := strings.ReplaceAll(desired, string(os.PathSeparator), "/")
+ if !strings.Contains(out, s) {
+ t.Errorf("did not see phrase %s in \n%s", s, out)
+ }
+}
+
+func wantN(t *testing.T, out string, desired string, n int) {
+ if strings.Count(out, desired) != n {
+ t.Errorf("expected exactly %d occurrences of %s in \n%s", n, desired, out)
+ }
+}
+
+func TestPathStuff(t *testing.T) {
+ sep := string(filepath.Separator)
+ if path, whine := parseLogPath("file:///c:foo"); path != "c:foo" || whine != "" { // good path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ if path, whine := parseLogPath("file:///foo"); path != sep+"foo" || whine != "" { // good path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ if path, whine := parseLogPath("foo"); path != "" || whine == "" { // BAD path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ if sep == "\\" { // On WINDOWS ONLY
+ if path, whine := parseLogPath("C:/foo"); path != "C:\\foo" || whine != "" { // good path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ if path, whine := parseLogPath("c:foo"); path != "" || whine == "" { // BAD path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ if path, whine := parseLogPath("/foo"); path != "" || whine == "" { // BAD path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ } else { // ON UNIX ONLY
+ if path, whine := parseLogPath("/foo"); path != sep+"foo" || whine != "" { // good path
+ t.Errorf("path='%s', whine='%s'", path, whine)
+ }
+ }
+}
+
+func TestLogOpt(t *testing.T) {
+ t.Parallel()
+
+ testenv.MustHaveGoBuild(t)
+
+ dir, err := ioutil.TempDir("", "TestLogOpt")
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer os.RemoveAll(dir)
+
+ dir = fixSlash(dir) // Normalize the directory name as much as possible, for Windows testing
+ src := filepath.Join(dir, "file.go")
+ if err := ioutil.WriteFile(src, []byte(srcCode), 0644); err != nil {
+ t.Fatal(err)
+ }
+
+ outfile := filepath.Join(dir, "file.o")
+
+ t.Run("JSON_fails", func(t *testing.T) {
+ // Test malformed flag
+ out, err := testLogOpt(t, "-json=foo", src, outfile)
+ if err == nil {
+ t.Error("-json=foo succeeded unexpectedly")
+ }
+ want(t, out, "option should be")
+ want(t, out, "number")
+
+ // Test a version number that is currently unsupported (and should remain unsupported for a while)
+ out, err = testLogOpt(t, "-json=9,foo", src, outfile)
+ if err == nil {
+ t.Error("-json=0,foo succeeded unexpectedly")
+ }
+ want(t, out, "version must be")
+
+ })
+
+ // replace d (dir) with t ("tmpdir") and convert path separators to '/'
+ normalize := func(out []byte, d, t string) string {
+ s := string(out)
+ s = strings.ReplaceAll(s, d, t)
+ s = strings.ReplaceAll(s, string(os.PathSeparator), "/")
+ return s
+ }
+
+ // Ensure that <128 byte copies are not reported and that 128-byte copies are.
+ // Check at both 1 and 8-byte alignments.
+ t.Run("Copy", func(t *testing.T) {
+ const copyCode = `package x
+func s128a1(x *[128]int8) [128]int8 {
+ return *x
+}
+func s127a1(x *[127]int8) [127]int8 {
+ return *x
+}
+func s16a8(x *[16]int64) [16]int64 {
+ return *x
+}
+func s15a8(x *[15]int64) [15]int64 {
+ return *x
+}
+`
+ copy := filepath.Join(dir, "copy.go")
+ if err := ioutil.WriteFile(copy, []byte(copyCode), 0644); err != nil {
+ t.Fatal(err)
+ }
+ outcopy := filepath.Join(dir, "copy.o")
+
+ // On not-amd64, test the host architecture and os
+ arches := []string{runtime.GOARCH}
+ goos0 := runtime.GOOS
+ if runtime.GOARCH == "amd64" { // Test many things with "linux" (wasm will get "js")
+ arches = []string{"arm", "arm64", "386", "amd64", "mips", "mips64", "ppc64le", "riscv64", "s390x", "wasm"}
+ goos0 = "linux"
+ }
+
+ for _, arch := range arches {
+ t.Run(arch, func(t *testing.T) {
+ goos := goos0
+ if arch == "wasm" {
+ goos = "js"
+ }
+ _, err := testCopy(t, dir, arch, goos, copy, outcopy)
+ if err != nil {
+ t.Error("-json=0,file://log/opt should have succeeded")
+ }
+ logged, err := ioutil.ReadFile(filepath.Join(dir, "log", "opt", "x", "copy.json"))
+ if err != nil {
+ t.Error("-json=0,file://log/opt missing expected log file")
+ }
+ slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
+ t.Logf("%s", slogged)
+ want(t, slogged, `{"range":{"start":{"line":3,"character":2},"end":{"line":3,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
+ want(t, slogged, `{"range":{"start":{"line":9,"character":2},"end":{"line":9,"character":2}},"severity":3,"code":"copy","source":"go compiler","message":"128 bytes"}`)
+ wantN(t, slogged, `"code":"copy"`, 2)
+ })
+ }
+ })
+
+ // Some architectures don't fault on nil dereference, so nilchecks are eliminated differently.
+ // The N-way copy test also doesn't need to run N-ways N times.
+ if runtime.GOARCH != "amd64" {
+ return
+ }
+
+ t.Run("Success", func(t *testing.T) {
+ // This test is supposed to succeed
+
+ // Note 'file://' is the I-Know-What-I-Am-Doing way of specifying a file, also to deal with corner cases for Windows.
+ _, err := testLogOptDir(t, dir, "-json=0,file://log/opt", src, outfile)
+ if err != nil {
+ t.Error("-json=0,file://log/opt should have succeeded")
+ }
+ logged, err := ioutil.ReadFile(filepath.Join(dir, "log", "opt", "x", "file.json"))
+ if err != nil {
+ t.Error("-json=0,file://log/opt missing expected log file")
+ }
+ // All this delicacy with uriIfy and filepath.Join is to get this test to work right on Windows.
+ slogged := normalize(logged, string(uriIfy(dir)), string(uriIfy("tmpdir")))
+ t.Logf("%s", slogged)
+ // below shows proper nilcheck
+ want(t, slogged, `{"range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}},"severity":3,"code":"nilcheck","source":"go compiler","message":"",`+
+ `"relatedInformation":[{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"}]}`)
+ want(t, slogged, `{"range":{"start":{"line":11,"character":6},"end":{"line":11,"character":6}},"severity":3,"code":"isInBounds","source":"go compiler","message":""}`)
+ want(t, slogged, `{"range":{"start":{"line":7,"character":6},"end":{"line":7,"character":6}},"severity":3,"code":"canInlineFunction","source":"go compiler","message":"cost: 35"}`)
+ // escape analysis explanation
+ want(t, slogged, `{"range":{"start":{"line":7,"character":13},"end":{"line":7,"character":13}},"severity":3,"code":"leak","source":"go compiler","message":"parameter z leaks to ~r0 with derefs=0",`+
+ `"relatedInformation":[`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: y = z:"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y := z (assign-pair)"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: flow: ~R0 = y:"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from y.b (dot of pointer)"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":11},"end":{"line":4,"character":11}}},"message":"inlineLoc"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from \u0026y.b (address-of)"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":4,"character":9},"end":{"line":4,"character":9}}},"message":"inlineLoc"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":13},"end":{"line":9,"character":13}}},"message":"escflow: from ~R0 = \u0026y.b (assign-pair)"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: flow: ~r0 = ~R0:"},`+
+ `{"location":{"uri":"file://tmpdir/file.go","range":{"start":{"line":9,"character":3},"end":{"line":9,"character":3}}},"message":"escflow: from return ~R0 (return)"}]}`)
+ })
+}
+
+func testLogOpt(t *testing.T, flag, src, outfile string) (string, error) {
+ run := []string{testenv.GoToolPath(t), "tool", "compile", flag, "-o", outfile, src}
+ t.Log(run)
+ cmd := exec.Command(run[0], run[1:]...)
+ out, err := cmd.CombinedOutput()
+ t.Logf("%s", out)
+ return string(out), err
+}
+
+func testLogOptDir(t *testing.T, dir, flag, src, outfile string) (string, error) {
+ // Notice the specified import path "x"
+ run := []string{testenv.GoToolPath(t), "tool", "compile", "-p", "x", flag, "-o", outfile, src}
+ t.Log(run)
+ cmd := exec.Command(run[0], run[1:]...)
+ cmd.Dir = dir
+ out, err := cmd.CombinedOutput()
+ t.Logf("%s", out)
+ return string(out), err
+}
+
+func testCopy(t *testing.T, dir, goarch, goos, src, outfile string) (string, error) {
+ // Notice the specified import path "x"
+ run := []string{testenv.GoToolPath(t), "tool", "compile", "-p", "x", "-json=0,file://log/opt", "-o", outfile, src}
+ t.Log(run)
+ cmd := exec.Command(run[0], run[1:]...)
+ cmd.Dir = dir
+ cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+goos)
+ out, err := cmd.CombinedOutput()
+ t.Logf("%s", out)
+ return string(out), err
+}