Source file src/cmd/compile/internal/ir/dump.go

     1  // Copyright 2018 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  // This file implements textual dumping of arbitrary data structures
     6  // for debugging purposes. The code is customized for Node graphs
     7  // and may be used for an alternative view of the node structure.
     8  
     9  package ir
    10  
    11  import (
    12  	"crypto/sha256"
    13  	"encoding/hex"
    14  	"fmt"
    15  	"io"
    16  	"net/url"
    17  	"os"
    18  	"reflect"
    19  	"regexp"
    20  	"strings"
    21  	"sync"
    22  
    23  	"cmd/compile/internal/base"
    24  	"cmd/compile/internal/types"
    25  	"cmd/internal/src"
    26  )
    27  
    28  // DumpAny is like FDumpAny but prints to stderr.
    29  func DumpAny(root any, filter string, depth int) {
    30  	FDumpAny(os.Stderr, root, filter, depth)
    31  }
    32  
    33  // FDumpAny prints the structure of a rooted data structure
    34  // to w by depth-first traversal of the data structure.
    35  //
    36  // The filter parameter is a regular expression. If it is
    37  // non-empty, only struct fields whose names match filter
    38  // are printed.
    39  //
    40  // The depth parameter controls how deep traversal recurses
    41  // before it returns (higher value means greater depth).
    42  // If an empty field filter is given, a good depth default value
    43  // is 4. A negative depth means no depth limit, which may be fine
    44  // for small data structures or if there is a non-empty filter.
    45  //
    46  // In the output, Node structs are identified by their Op name
    47  // rather than their type; struct fields with zero values or
    48  // non-matching field names are omitted, and "…" means recursion
    49  // depth has been reached or struct fields have been omitted.
    50  func FDumpAny(w io.Writer, root any, filter string, depth int) {
    51  	if root == nil {
    52  		fmt.Fprintln(w, "nil")
    53  		return
    54  	}
    55  
    56  	if filter == "" {
    57  		filter = ".*" // default
    58  	}
    59  
    60  	p := dumper{
    61  		output:  w,
    62  		fieldrx: regexp.MustCompile(filter),
    63  		ptrmap:  make(map[uintptr]int),
    64  		last:    '\n', // force printing of line number on first line
    65  	}
    66  
    67  	p.dump(reflect.ValueOf(root), depth)
    68  	p.printf("\n")
    69  }
    70  
    71  // MatchAstDump returns true if the fn matches the value
    72  // of the astdump debug flag.  Fn matches in the following
    73  // cases:
    74  //
    75  //   - astdump == name(fn)
    76  //   - astdump == pkgname(fn).name(fn)
    77  //   - astdump == afterslash(pkgname(fn)).name(fn)
    78  //   - astdump begins with a "~" and what follows "~" is a
    79  //     regular expression matching pkgname(fn).name(fn)
    80  //
    81  // If MatchAstDump returns true, it also prints to os.Stderr
    82  //
    83  //	\nir.Match(<fn>, <astdump>) for <where>\n
    84  func MatchAstDump(fn *Func, where string) bool {
    85  	if len(base.Debug.AstDump) == 0 {
    86  		return false
    87  	}
    88  	return matchForDump(fn, base.Ctxt.Pkgpath, where)
    89  }
    90  
    91  // matchForDump is marked noinline to ensure that the exported
    92  // function MatchAstDump IS inlineable and is also small, because
    93  // common case is AstDump is not set.
    94  //
    95  //go:noinline
    96  func matchForDump(fn *Func, pkgPath, where string) bool {
    97  	return MatchPkgFn(pkgPath, FuncName(fn), base.Debug.AstDump)
    98  }
    99  
   100  // MatchPkgFn returns true if pkg and fnName "match" toMatch.
   101  // "~REGEXP" matches REGEXP against pkgName + "." + fnName
   102  // "aFunc" matches "aFunc" (in any package)
   103  // "aPkg.aFunc" matches "aPkg.aFunc"
   104  // "aPkg/subPkg.aFunc" matches "subPkg.aFunc"
   105  func MatchPkgFn(pkgName, fnName, toMatch string) bool {
   106  	if toMatch[0] == '~' {
   107  		dbgRE := regexp.MustCompile(toMatch[1:])
   108  		return dbgRE.MatchString(pkgName + "." + fnName)
   109  	}
   110  	if fnName == toMatch {
   111  		return true
   112  	}
   113  	matchPkgDotName := func(pkg string) bool {
   114  		// Allocation-free equality check for toMatch == base.Ctxt.Pkgpath + "." + fnName
   115  		return len(toMatch) == len(pkg)+1+len(fnName) &&
   116  			strings.HasPrefix(toMatch, pkg) && toMatch[len(pkg)] == '.' && strings.HasSuffix(toMatch, fnName)
   117  	}
   118  	if matchPkgDotName(pkgName) {
   119  		return true
   120  	}
   121  	if l := strings.LastIndexByte(pkgName, '/'); l > 0 && matchPkgDotName(pkgName[l+1:]) {
   122  		return true
   123  	}
   124  
   125  	return false
   126  }
   127  
   128  // AstDump appends the ast dump for fn to the ast dump file for fn.
   129  // The generated file name is
   130  //
   131  //	url.PathEscape(PkgFuncName(fn)) + ".ast"
   132  //
   133  // It also prints
   134  //
   135  //	Writing ast output to <astfilename>\n
   136  //
   137  // to os.Stderr.
   138  func AstDump(fn *Func, why string) {
   139  	err := withLockAndFile(
   140  		fn,
   141  		func(w io.Writer) {
   142  			FDump(w, why, fn)
   143  		},
   144  	)
   145  	// strip text following comma, for phase names.
   146  	comma := strings.Index(why, ",")
   147  	if comma > 0 {
   148  		why = why[:comma]
   149  	}
   150  	DumpNodeHTML(fn, why, fn)
   151  	if err != nil {
   152  		fmt.Fprintf(os.Stderr, "Dump returned error %v\n", err)
   153  	}
   154  }
   155  
   156  var mu sync.Mutex
   157  var astDumpFiles = make(map[string]bool)
   158  
   159  func escapedFileName(fn *Func, suffix string) string {
   160  	return EscapedFileName(PkgFuncName(fn), suffix)
   161  }
   162  
   163  // EscapedFileName constructs a file name from fn and suffix,
   164  // url-path-escaping the function part of the name and replacing it
   165  // with a hash if it is too long.  The suffix is neither escaped
   166  // nor including in the length calculation, so an excessively
   167  // creative suffix will result in problems.
   168  func EscapedFileName(fn, suffix string) string {
   169  	name := url.PathEscape(fn)
   170  	if len(name) > 125 { // arbitrary limit on file names, as if anyone types these in by hand
   171  		hash := sha256.Sum256([]byte(name))
   172  		name = hex.EncodeToString(hash[:8])
   173  	}
   174  	return name + suffix
   175  }
   176  
   177  // withLockAndFile manages ast dump files for various function names
   178  // and invokes a dumping function to write output, under a lock.
   179  func withLockAndFile(fn *Func, dump func(io.Writer)) (err error) {
   180  	name := escapedFileName(fn, ".ast")
   181  
   182  	// Ensure that debugging output is not scrambled and is written promptly
   183  	mu.Lock()
   184  	defer mu.Unlock()
   185  	mode := os.O_APPEND | os.O_RDWR
   186  	if !astDumpFiles[name] {
   187  		astDumpFiles[name] = true
   188  		mode = os.O_CREATE | os.O_TRUNC | os.O_RDWR
   189  		fmt.Fprintf(os.Stderr, "Writing text ast output for %s to %s\n", PkgFuncName(fn), name)
   190  	}
   191  
   192  	fi, err := os.OpenFile(name, mode, 0777)
   193  	if err != nil {
   194  		return err
   195  	}
   196  	defer func() { err = fi.Close() }()
   197  	dump(fi)
   198  	return
   199  }
   200  
   201  var htmlWriters = make(map[*Func]*HTMLWriter)
   202  var orderedFuncs = []*Func{}
   203  
   204  // DumpNodeHTML dumps the node n to the HTML writer for fn.
   205  // It uses the same phase name as the text dump.
   206  func DumpNodeHTML(fn *Func, why string, n Node) {
   207  	mu.Lock()
   208  	defer mu.Unlock()
   209  	w, ok := htmlWriters[fn]
   210  	if !ok {
   211  		name := escapedFileName(fn, ".html")
   212  		w = NewHTMLWriter(name, fn, "")
   213  		htmlWriters[fn] = w
   214  		orderedFuncs = append(orderedFuncs, fn)
   215  	}
   216  	w.WritePhase(why, why)
   217  }
   218  
   219  // CloseHTMLWriters closes the HTML writer for fn, if one exists.
   220  func CloseHTMLWriters() {
   221  	mu.Lock()
   222  	defer mu.Unlock()
   223  	for _, fn := range orderedFuncs {
   224  		if w, ok := htmlWriters[fn]; ok {
   225  			w.Close("Writing html ast output for %s to %s\n", PkgFuncName(w.Func), w.path)
   226  			delete(htmlWriters, fn)
   227  		}
   228  	}
   229  	orderedFuncs = nil
   230  }
   231  
   232  type dumper struct {
   233  	output  io.Writer
   234  	fieldrx *regexp.Regexp  // field name filter
   235  	ptrmap  map[uintptr]int // ptr -> dump line number
   236  	lastadr string          // last address string printed (for shortening)
   237  
   238  	// output
   239  	indent int  // current indentation level
   240  	last   byte // last byte processed by Write
   241  	line   int  // current line number
   242  }
   243  
   244  var indentBytes = []byte(".  ")
   245  
   246  func (p *dumper) Write(data []byte) (n int, err error) {
   247  	var m int
   248  	for i, b := range data {
   249  		// invariant: data[0:n] has been written
   250  		if b == '\n' {
   251  			m, err = p.output.Write(data[n : i+1])
   252  			n += m
   253  			if err != nil {
   254  				return
   255  			}
   256  		} else if p.last == '\n' {
   257  			p.line++
   258  			_, err = fmt.Fprintf(p.output, "%6d  ", p.line)
   259  			if err != nil {
   260  				return
   261  			}
   262  			for j := p.indent; j > 0; j-- {
   263  				_, err = p.output.Write(indentBytes)
   264  				if err != nil {
   265  					return
   266  				}
   267  			}
   268  		}
   269  		p.last = b
   270  	}
   271  	if len(data) > n {
   272  		m, err = p.output.Write(data[n:])
   273  		n += m
   274  	}
   275  	return
   276  }
   277  
   278  // printf is a convenience wrapper.
   279  func (p *dumper) printf(format string, args ...any) {
   280  	if _, err := fmt.Fprintf(p, format, args...); err != nil {
   281  		panic(err)
   282  	}
   283  }
   284  
   285  // addr returns the (hexadecimal) address string of the object
   286  // represented by x (or "?" if x is not addressable), with the
   287  // common prefix between this and the prior address replaced by
   288  // "0x…" to make it easier to visually match addresses.
   289  func (p *dumper) addr(x reflect.Value) string {
   290  	if !x.CanAddr() {
   291  		return "?"
   292  	}
   293  	adr := fmt.Sprintf("%p", x.Addr().Interface())
   294  	s := adr
   295  	if i := commonPrefixLen(p.lastadr, adr); i > 0 {
   296  		s = "0x…" + adr[i:]
   297  	}
   298  	p.lastadr = adr
   299  	return s
   300  }
   301  
   302  // dump prints the contents of x.
   303  func (p *dumper) dump(x reflect.Value, depth int) {
   304  	if depth == 0 {
   305  		p.printf("…")
   306  		return
   307  	}
   308  
   309  	if pos, ok := x.Interface().(src.XPos); ok {
   310  		p.printf("%s", base.FmtPos(pos))
   311  		return
   312  	}
   313  
   314  	switch x.Kind() {
   315  	case reflect.String:
   316  		p.printf("%q", x.Interface()) // print strings in quotes
   317  
   318  	case reflect.Interface:
   319  		if x.IsNil() {
   320  			p.printf("nil")
   321  			return
   322  		}
   323  		p.dump(x.Elem(), depth-1)
   324  
   325  	case reflect.Ptr:
   326  		if x.IsNil() {
   327  			p.printf("nil")
   328  			return
   329  		}
   330  
   331  		p.printf("*")
   332  		ptr := x.Pointer()
   333  		if line, exists := p.ptrmap[ptr]; exists {
   334  			p.printf("(@%d)", line)
   335  			return
   336  		}
   337  		p.ptrmap[ptr] = p.line
   338  		p.dump(x.Elem(), depth) // don't count pointer indirection towards depth
   339  
   340  	case reflect.Slice:
   341  		if x.IsNil() {
   342  			p.printf("nil")
   343  			return
   344  		}
   345  		p.printf("%s (%d entries) {", x.Type(), x.Len())
   346  		if x.Len() > 0 {
   347  			p.indent++
   348  			p.printf("\n")
   349  			for i, n := 0, x.Len(); i < n; i++ {
   350  				p.printf("%d: ", i)
   351  				p.dump(x.Index(i), depth-1)
   352  				p.printf("\n")
   353  			}
   354  			p.indent--
   355  		}
   356  		p.printf("}")
   357  
   358  	case reflect.Struct:
   359  		typ := x.Type()
   360  
   361  		isNode := false
   362  		if n, ok := x.Interface().(Node); ok {
   363  			isNode = true
   364  			p.printf("%s %s {", n.Op().String(), p.addr(x))
   365  		} else {
   366  			p.printf("%s {", typ)
   367  		}
   368  		p.indent++
   369  
   370  		first := true
   371  		omitted := false
   372  		for i, n := 0, typ.NumField(); i < n; i++ {
   373  			// Exclude non-exported fields because their
   374  			// values cannot be accessed via reflection.
   375  			if name := typ.Field(i).Name; types.IsExported(name) {
   376  				if !p.fieldrx.MatchString(name) {
   377  					omitted = true
   378  					continue // field name not selected by filter
   379  				}
   380  
   381  				// special cases
   382  				if isNode && name == "Op" {
   383  					omitted = true
   384  					continue // Op field already printed for Nodes
   385  				}
   386  				x := x.Field(i)
   387  				if x.IsZero() {
   388  					omitted = true
   389  					continue // exclude zero-valued fields
   390  				}
   391  				if n, ok := x.Interface().(Nodes); ok && len(n) == 0 {
   392  					omitted = true
   393  					continue // exclude empty Nodes slices
   394  				}
   395  
   396  				if first {
   397  					p.printf("\n")
   398  					first = false
   399  				}
   400  				p.printf("%s: ", name)
   401  				p.dump(x, depth-1)
   402  				p.printf("\n")
   403  			}
   404  		}
   405  		if omitted {
   406  			p.printf("…\n")
   407  		}
   408  
   409  		p.indent--
   410  		p.printf("}")
   411  
   412  	default:
   413  		p.printf("%v", x.Interface())
   414  	}
   415  }
   416  
   417  func commonPrefixLen(a, b string) (i int) {
   418  	for i < len(a) && i < len(b) && a[i] == b[i] {
   419  		i++
   420  	}
   421  	return
   422  }
   423  

View as plain text