]> Cypherpunks.ru repositories - gostls13.git/blob - src/cmd/compile/internal/logopt/log_opts.go
internal/buildcfg: move build configuration out of cmd/internal/objabi
[gostls13.git] / src / cmd / compile / internal / logopt / log_opts.go
1 // Copyright 2019 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 logopt
6
7 import (
8         "cmd/internal/obj"
9         "cmd/internal/src"
10         "encoding/json"
11         "fmt"
12         "internal/buildcfg"
13         "io"
14         "log"
15         "net/url"
16         "os"
17         "path/filepath"
18         "sort"
19         "strconv"
20         "strings"
21         "sync"
22         "unicode"
23 )
24
25 // This implements (non)optimization logging for -json option to the Go compiler
26 // The option is -json 0,<destination>.
27 //
28 // 0 is the version number; to avoid the need for synchronized updates, if
29 // new versions of the logging appear, the compiler will support both, for a while,
30 // and clients will specify what they need.
31 //
32 // <destination> is a directory.
33 // Directories are specified with a leading / or os.PathSeparator,
34 // or more explicitly with file://directory.  The second form is intended to
35 // deal with corner cases on Windows, and to allow specification of a relative
36 // directory path (which is normally a bad idea, because the local directory
37 // varies a lot in a build, especially with modules and/or vendoring, and may
38 // not be writeable).
39 //
40 // For each package pkg compiled, a url.PathEscape(pkg)-named subdirectory
41 // is created.  For each source file.go in that package that generates
42 // diagnostics (no diagnostics means no file),
43 // a url.PathEscape(file)+".json"-named file is created and contains the
44 // logged diagnostics.
45 //
46 // For example, "cmd%2Finternal%2Fdwarf/%3Cautogenerated%3E.json"
47 // for "cmd/internal/dwarf" and <autogenerated> (which is not really a file, but the compiler sees it)
48 //
49 // If the package string is empty, it is replaced internally with string(0) which encodes to %00.
50 //
51 // Each log file begins with a JSON record identifying version,
52 // platform, and other context, followed by optimization-relevant
53 // LSP Diagnostic records, one per line (LSP version 3.15, no difference from 3.14 on the subset used here
54 // see https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/ )
55 //
56 // The fields of a Diagnostic are used in the following way:
57 // Range: the outermost source position, for now begin and end are equal.
58 // Severity: (always) SeverityInformation (3)
59 // Source: (always) "go compiler"
60 // Code: a string describing the missed optimization, e.g., "nilcheck", "cannotInline", "isInBounds", "escape"
61 // Message: depending on code, additional information, e.g., the reason a function cannot be inlined.
62 // RelatedInformation: if the missed optimization actually occurred at a function inlined at Range,
63 //    then the sequence of inlined locations appears here, from (second) outermost to innermost,
64 //    each with message="inlineLoc".
65 //
66 //    In the case of escape analysis explanations, after any outer inlining locations,
67 //    the lines of the explanation appear, each potentially followed with its own inlining
68 //    location if the escape flow occurred within an inlined function.
69 //
70 // For example <destination>/cmd%2Fcompile%2Finternal%2Fssa/prove.json
71 // might begin with the following line (wrapped for legibility):
72 //
73 // {"version":0,"package":"cmd/compile/internal/ssa","goos":"darwin","goarch":"amd64",
74 //  "gc_version":"devel +e1b9a57852 Fri Nov 1 15:07:00 2019 -0400",
75 //  "file":"/Users/drchase/work/go/src/cmd/compile/internal/ssa/prove.go"}
76 //
77 // and later contain (also wrapped for legibility):
78 //
79 // {"range":{"start":{"line":191,"character":24},"end":{"line":191,"character":24}},
80 //  "severity":3,"code":"nilcheck","source":"go compiler","message":"",
81 //  "relatedInformation":[
82 //    {"location":{"uri":"file:///Users/drchase/work/go/src/cmd/compile/internal/ssa/func.go",
83 //                 "range":{"start":{"line":153,"character":16},"end":{"line":153,"character":16}}},
84 //     "message":"inlineLoc"}]}
85 //
86 // That is, at prove.go (implicit from context, provided in both filename and header line),
87 // line 191, column 24, a nilcheck occurred in the generated code.
88 // The relatedInformation indicates that this code actually came from
89 // an inlined call to func.go, line 153, character 16.
90 //
91 // prove.go:191:
92 //      ft.orderS = f.newPoset()
93 // func.go:152 and 153:
94 //  func (f *Func) newPoset() *poset {
95 //          if len(f.Cache.scrPoset) > 0 {
96 //
97 // In the case that the package is empty, the string(0) package name is also used in the header record, for example
98 //
99 //  go tool compile -json=0,file://logopt x.go       # no -p option to set the package
100 //  head -1 logopt/%00/x.json
101 //  {"version":0,"package":"\u0000","goos":"darwin","goarch":"amd64","gc_version":"devel +86487adf6a Thu Nov 7 19:34:56 2019 -0500","file":"x.go"}
102
103 type VersionHeader struct {
104         Version   int    `json:"version"`
105         Package   string `json:"package"`
106         Goos      string `json:"goos"`
107         Goarch    string `json:"goarch"`
108         GcVersion string `json:"gc_version"`
109         File      string `json:"file,omitempty"` // LSP requires an enclosing resource, i.e., a file
110 }
111
112 // DocumentURI, Position, Range, Location, Diagnostic, DiagnosticRelatedInformation all reuse json definitions from gopls.
113 // See https://github.com/golang/tools/blob/22afafe3322a860fcd3d88448768f9db36f8bc5f/internal/lsp/protocol/tsprotocol.go
114
115 type DocumentURI string
116
117 type Position struct {
118         Line      uint `json:"line"`      // gopls uses float64, but json output is the same for integers
119         Character uint `json:"character"` // gopls uses float64, but json output is the same for integers
120 }
121
122 // A Range in a text document expressed as (zero-based) start and end positions.
123 // A range is comparable to a selection in an editor. Therefore the end position is exclusive.
124 // If you want to specify a range that contains a line including the line ending character(s)
125 // then use an end position denoting the start of the next line.
126 type Range struct {
127         /*Start defined:
128          * The range's start position
129          */
130         Start Position `json:"start"`
131
132         /*End defined:
133          * The range's end position
134          */
135         End Position `json:"end"` // exclusive
136 }
137
138 // A Location represents a location inside a resource, such as a line inside a text file.
139 type Location struct {
140         // URI is
141         URI DocumentURI `json:"uri"`
142
143         // Range is
144         Range Range `json:"range"`
145 }
146
147 /* DiagnosticRelatedInformation defined:
148  * Represents a related message and source code location for a diagnostic. This should be
149  * used to point to code locations that cause or related to a diagnostics, e.g when duplicating
150  * a symbol in a scope.
151  */
152 type DiagnosticRelatedInformation struct {
153
154         /*Location defined:
155          * The location of this related diagnostic information.
156          */
157         Location Location `json:"location"`
158
159         /*Message defined:
160          * The message of this related diagnostic information.
161          */
162         Message string `json:"message"`
163 }
164
165 // DiagnosticSeverity defines constants
166 type DiagnosticSeverity uint
167
168 const (
169         /*SeverityInformation defined:
170          * Reports an information.
171          */
172         SeverityInformation DiagnosticSeverity = 3
173 )
174
175 // DiagnosticTag defines constants
176 type DiagnosticTag uint
177
178 /*Diagnostic defined:
179  * Represents a diagnostic, such as a compiler error or warning. Diagnostic objects
180  * are only valid in the scope of a resource.
181  */
182 type Diagnostic struct {
183
184         /*Range defined:
185          * The range at which the message applies
186          */
187         Range Range `json:"range"`
188
189         /*Severity defined:
190          * The diagnostic's severity. Can be omitted. If omitted it is up to the
191          * client to interpret diagnostics as error, warning, info or hint.
192          */
193         Severity DiagnosticSeverity `json:"severity,omitempty"` // always SeverityInformation for optimizer logging.
194
195         /*Code defined:
196          * The diagnostic's code, which usually appear in the user interface.
197          */
198         Code string `json:"code,omitempty"` // LSP uses 'number | string' = gopls interface{}, but only string here, e.g. "boundsCheck", "nilcheck", etc.
199
200         /*Source defined:
201          * A human-readable string describing the source of this
202          * diagnostic, e.g. 'typescript' or 'super lint'. It usually
203          * appears in the user interface.
204          */
205         Source string `json:"source,omitempty"` // "go compiler"
206
207         /*Message defined:
208          * The diagnostic's message. It usually appears in the user interface
209          */
210         Message string `json:"message"` // sometimes used, provides additional information.
211
212         /*Tags defined:
213          * Additional metadata about the diagnostic.
214          */
215         Tags []DiagnosticTag `json:"tags,omitempty"` // always empty for logging optimizations.
216
217         /*RelatedInformation defined:
218          * An array of related diagnostic information, e.g. when symbol-names within
219          * a scope collide all definitions can be marked via this property.
220          */
221         RelatedInformation []DiagnosticRelatedInformation `json:"relatedInformation,omitempty"`
222 }
223
224 // A LoggedOpt is what the compiler produces and accumulates,
225 // to be converted to JSON for human or IDE consumption.
226 type LoggedOpt struct {
227         pos          src.XPos      // Source code position at which the event occurred. If it is inlined, outer and all inlined locations will appear in JSON.
228         compilerPass string        // Compiler pass.  For human/adhoc consumption; does not appear in JSON (yet)
229         functionName string        // Function name.  For human/adhoc consumption; does not appear in JSON (yet)
230         what         string        // The (non) optimization; "nilcheck", "boundsCheck", "inline", "noInline"
231         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.
232 }
233
234 type logFormat uint8
235
236 const (
237         None  logFormat = iota
238         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.
239 )
240
241 var Format = None
242 var dest string
243
244 // LogJsonOption parses and validates the version,directory value attached to the -json compiler flag.
245 func LogJsonOption(flagValue string) {
246         version, directory := parseLogFlag("json", flagValue)
247         if version != 0 {
248                 log.Fatal("-json version must be 0")
249         }
250         dest = checkLogPath(directory)
251         Format = Json0
252 }
253
254 // parseLogFlag checks the flag passed to -json
255 // for version,destination format and returns the two parts.
256 func parseLogFlag(flag, value string) (version int, directory string) {
257         if Format != None {
258                 log.Fatal("Cannot repeat -json flag")
259         }
260         commaAt := strings.Index(value, ",")
261         if commaAt <= 0 {
262                 log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number", flag)
263         }
264         v, err := strconv.Atoi(value[:commaAt])
265         if err != nil {
266                 log.Fatalf("-%s option should be '<version>,<destination>' where <version> is a number: err=%v", flag, err)
267         }
268         version = v
269         directory = value[commaAt+1:]
270         return
271 }
272
273 // isWindowsDriveURI returns true if the file URI is of the format used by
274 // Windows URIs. The url.Parse package does not specially handle Windows paths
275 // (see golang/go#6027), so we check if the URI path has a drive prefix (e.g. "/C:").
276 // (copied from tools/internal/span/uri.go)
277 // this is less comprehensive that the processing in filepath.IsAbs on Windows.
278 func isWindowsDriveURIPath(uri string) bool {
279         if len(uri) < 4 {
280                 return false
281         }
282         return uri[0] == '/' && unicode.IsLetter(rune(uri[1])) && uri[2] == ':'
283 }
284
285 func parseLogPath(destination string) (string, string) {
286         if filepath.IsAbs(destination) {
287                 return filepath.Clean(destination), ""
288         }
289         if strings.HasPrefix(destination, "file://") { // IKWIAD, or Windows C:\foo\bar\baz
290                 uri, err := url.Parse(destination)
291                 if err != nil {
292                         return "", fmt.Sprintf("optimizer logging destination looked like file:// URI but failed to parse: err=%v", err)
293                 }
294                 destination = uri.Host + uri.Path
295                 if isWindowsDriveURIPath(destination) {
296                         // strip leading / from /C:
297                         // unlike tools/internal/span/uri.go, do not uppercase the drive letter -- let filepath.Clean do what it does.
298                         destination = destination[1:]
299                 }
300                 return filepath.Clean(destination), ""
301         }
302         return "", fmt.Sprintf("optimizer logging destination %s was neither %s-prefixed directory nor file://-prefixed file URI", destination, string(filepath.Separator))
303 }
304
305 // checkLogPath does superficial early checking of the string specifying
306 // the directory to which optimizer logging is directed, and if
307 // it passes the test, stores the string in LO_dir
308 func checkLogPath(destination string) string {
309         path, complaint := parseLogPath(destination)
310         if complaint != "" {
311                 log.Fatalf(complaint)
312         }
313         err := os.MkdirAll(path, 0755)
314         if err != nil {
315                 log.Fatalf("optimizer logging destination '<version>,<directory>' but could not create <directory>: err=%v", err)
316         }
317         return path
318 }
319
320 var loggedOpts []*LoggedOpt
321 var mu = sync.Mutex{} // mu protects loggedOpts.
322
323 // NewLoggedOpt allocates a new LoggedOpt, to later be passed to either NewLoggedOpt or LogOpt as "args".
324 // Pos is the source position (including inlining), what is the message, pass is which pass created the message,
325 // funcName is the name of the function
326 // A typical use for this to accumulate an explanation for a missed optimization, for example, why did something escape?
327 func NewLoggedOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) *LoggedOpt {
328         pass = strings.Replace(pass, " ", "_", -1)
329         return &LoggedOpt{pos, pass, funcName, what, args}
330 }
331
332 // Logopt logs information about a (usually missed) optimization performed by the compiler.
333 // Pos is the source position (including inlining), what is the message, pass is which pass created the message,
334 // funcName is the name of the function
335 func LogOpt(pos src.XPos, what, pass, funcName string, args ...interface{}) {
336         if Format == None {
337                 return
338         }
339         lo := NewLoggedOpt(pos, what, pass, funcName, args...)
340         mu.Lock()
341         defer mu.Unlock()
342         // Because of concurrent calls from back end, no telling what the order will be, but is stable-sorted by outer Pos before use.
343         loggedOpts = append(loggedOpts, lo)
344 }
345
346 // Enabled returns whether optimization logging is enabled.
347 func Enabled() bool {
348         switch Format {
349         case None:
350                 return false
351         case Json0:
352                 return true
353         }
354         panic("Unexpected optimizer-logging level")
355 }
356
357 // byPos sorts diagnostics by source position.
358 type byPos struct {
359         ctxt *obj.Link
360         a    []*LoggedOpt
361 }
362
363 func (x byPos) Len() int { return len(x.a) }
364 func (x byPos) Less(i, j int) bool {
365         return x.ctxt.OutermostPos(x.a[i].pos).Before(x.ctxt.OutermostPos(x.a[j].pos))
366 }
367 func (x byPos) Swap(i, j int) { x.a[i], x.a[j] = x.a[j], x.a[i] }
368
369 func writerForLSP(subdirpath, file string) io.WriteCloser {
370         basename := file
371         lastslash := strings.LastIndexAny(basename, "\\/")
372         if lastslash != -1 {
373                 basename = basename[lastslash+1:]
374         }
375         lastdot := strings.LastIndex(basename, ".go")
376         if lastdot != -1 {
377                 basename = basename[:lastdot]
378         }
379         basename = pathEscape(basename)
380
381         // Assume a directory, make a file
382         p := filepath.Join(subdirpath, basename+".json")
383         w, err := os.Create(p)
384         if err != nil {
385                 log.Fatalf("Could not create file %s for logging optimizer actions, %v", p, err)
386         }
387         return w
388 }
389
390 func fixSlash(f string) string {
391         if os.PathSeparator == '/' {
392                 return f
393         }
394         return strings.Replace(f, string(os.PathSeparator), "/", -1)
395 }
396
397 func uriIfy(f string) DocumentURI {
398         url := url.URL{
399                 Scheme: "file",
400                 Path:   fixSlash(f),
401         }
402         return DocumentURI(url.String())
403 }
404
405 // Return filename, replacing a first occurrence of $GOROOT with the
406 // actual value of the GOROOT (because LSP does not speak "$GOROOT").
407 func uprootedPath(filename string) string {
408         if !strings.HasPrefix(filename, "$GOROOT/") {
409                 return filename
410         }
411         return buildcfg.GOROOT + filename[len("$GOROOT"):]
412 }
413
414 // FlushLoggedOpts flushes all the accumulated optimization log entries.
415 func FlushLoggedOpts(ctxt *obj.Link, slashPkgPath string) {
416         if Format == None {
417                 return
418         }
419
420         sort.Stable(byPos{ctxt, loggedOpts}) // Stable is necessary to preserve the per-function order, which is repeatable.
421         switch Format {
422
423         case Json0: // LSP 3.15
424                 var posTmp []src.Pos
425                 var encoder *json.Encoder
426                 var w io.WriteCloser
427
428                 if slashPkgPath == "" {
429                         slashPkgPath = "\000"
430                 }
431                 subdirpath := filepath.Join(dest, pathEscape(slashPkgPath))
432                 err := os.MkdirAll(subdirpath, 0755)
433                 if err != nil {
434                         log.Fatalf("Could not create directory %s for logging optimizer actions, %v", subdirpath, err)
435                 }
436                 diagnostic := Diagnostic{Source: "go compiler", Severity: SeverityInformation}
437
438                 // For LSP, make a subdirectory for the package, and for each file foo.go, create foo.json in that subdirectory.
439                 currentFile := ""
440                 for _, x := range loggedOpts {
441                         posTmp, p0 := x.parsePos(ctxt, posTmp)
442                         p0f := uprootedPath(p0.Filename())
443
444                         if currentFile != p0f {
445                                 if w != nil {
446                                         w.Close()
447                                 }
448                                 currentFile = p0f
449                                 w = writerForLSP(subdirpath, currentFile)
450                                 encoder = json.NewEncoder(w)
451                                 encoder.Encode(VersionHeader{Version: 0, Package: slashPkgPath, Goos: buildcfg.GOOS, Goarch: buildcfg.GOARCH, GcVersion: buildcfg.Version, File: currentFile})
452                         }
453
454                         // The first "target" is the most important one.
455                         var target string
456                         if len(x.target) > 0 {
457                                 target = fmt.Sprint(x.target[0])
458                         }
459
460                         diagnostic.Code = x.what
461                         diagnostic.Message = target
462                         diagnostic.Range = newPointRange(p0)
463                         diagnostic.RelatedInformation = diagnostic.RelatedInformation[:0]
464
465                         appendInlinedPos(posTmp, &diagnostic)
466
467                         // Diagnostic explanation is stored in RelatedInformation after inlining info
468                         if len(x.target) > 1 {
469                                 switch y := x.target[1].(type) {
470                                 case []*LoggedOpt:
471                                         for _, z := range y {
472                                                 posTmp, p0 := z.parsePos(ctxt, posTmp)
473                                                 loc := newLocation(p0)
474                                                 msg := z.what
475                                                 if len(z.target) > 0 {
476                                                         msg = msg + ": " + fmt.Sprint(z.target[0])
477                                                 }
478
479                                                 diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: msg})
480                                                 appendInlinedPos(posTmp, &diagnostic)
481                                         }
482                                 }
483                         }
484
485                         encoder.Encode(diagnostic)
486                 }
487                 if w != nil {
488                         w.Close()
489                 }
490         }
491 }
492
493 // newPointRange returns a single-position Range for the compiler source location p.
494 func newPointRange(p src.Pos) Range {
495         return Range{Start: Position{p.Line(), p.Col()},
496                 End: Position{p.Line(), p.Col()}}
497 }
498
499 // newLocation returns the Location for the compiler source location p
500 func newLocation(p src.Pos) Location {
501         loc := Location{URI: uriIfy(uprootedPath(p.Filename())), Range: newPointRange(p)}
502         return loc
503 }
504
505 // appendInlinedPos extracts inlining information from posTmp and append it to diagnostic
506 func appendInlinedPos(posTmp []src.Pos, diagnostic *Diagnostic) {
507         for i := 1; i < len(posTmp); i++ {
508                 p := posTmp[i]
509                 loc := newLocation(p)
510                 diagnostic.RelatedInformation = append(diagnostic.RelatedInformation, DiagnosticRelatedInformation{Location: loc, Message: "inlineLoc"})
511         }
512 }
513
514 func (x *LoggedOpt) parsePos(ctxt *obj.Link, posTmp []src.Pos) ([]src.Pos, src.Pos) {
515         posTmp = ctxt.AllPos(x.pos, posTmp)
516         // Reverse posTmp to put outermost first.
517         l := len(posTmp)
518         for i := 0; i < l/2; i++ {
519                 posTmp[i], posTmp[l-i-1] = posTmp[l-i-1], posTmp[i]
520         }
521         p0 := posTmp[0]
522         return posTmp, p0
523 }