Source file src/cmd/compile/internal/base/hashdebug.go

     1  // Copyright 2022 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 base
     6  
     7  import (
     8  	"bytes"
     9  	"cmd/internal/obj"
    10  	"cmd/internal/src"
    11  	"fmt"
    12  	"internal/bisect"
    13  	"io"
    14  	"os"
    15  	"path/filepath"
    16  	"strconv"
    17  	"strings"
    18  	"sync"
    19  )
    20  
    21  type hashAndMask struct {
    22  	// a hash h matches if (h^hash)&mask == 0
    23  	hash uint64
    24  	mask uint64
    25  	name string // base name, or base name + "0", "1", etc.
    26  }
    27  
    28  type HashDebug struct {
    29  	mu   sync.Mutex // for logfile, posTmp, bytesTmp
    30  	name string     // base name of the flag/variable.
    31  	// what file (if any) receives the yes/no logging?
    32  	// default is os.Stdout
    33  	logfile          io.Writer
    34  	posTmp           []src.Pos
    35  	bytesTmp         bytes.Buffer
    36  	matches          []hashAndMask // A hash matches if one of these matches.
    37  	excludes         []hashAndMask // explicitly excluded hash suffixes
    38  	bisect           *bisect.Matcher
    39  	fileSuffixOnly   bool // for Pos hashes, remove the directory prefix.
    40  	inlineSuffixOnly bool // for Pos hashes, remove all but the most inline position.
    41  }
    42  
    43  // SetInlineSuffixOnly controls whether hashing and reporting use the entire
    44  // inline position, or just the most-inline suffix.  Compiler debugging tends
    45  // to want the whole inlining, debugging user problems (loopvarhash, e.g.)
    46  // typically does not need to see the entire inline tree, there is just one
    47  // copy of the source code.
    48  func (d *HashDebug) SetInlineSuffixOnly(b bool) *HashDebug {
    49  	d.inlineSuffixOnly = b
    50  	return d
    51  }
    52  
    53  // The default compiler-debugging HashDebug, for "-d=gossahash=..."
    54  var hashDebug *HashDebug
    55  
    56  var ConvertHash *HashDebug      // for debugging float-to-[u]int conversion changes
    57  var FmaHash *HashDebug          // for debugging fused-multiply-add floating point changes
    58  var LoopVarHash *HashDebug      // for debugging shared/private loop variable changes
    59  var PGOHash *HashDebug          // for debugging PGO optimization decisions
    60  var LiteralAllocHash *HashDebug // for debugging literal allocation optimizations
    61  var MergeLocalsHash *HashDebug  // for debugging local stack slot merging changes
    62  var VariableMakeHash *HashDebug // for debugging variable-sized make optimizations
    63  
    64  // DebugHashMatchPkgFunc reports whether debug variable Gossahash
    65  //
    66  //  1. is empty (returns true; this is a special more-quickly implemented case of 4 below)
    67  //
    68  //  2. is "y" or "Y" (returns true)
    69  //
    70  //  3. is "n" or "N" (returns false)
    71  //
    72  //  4. does not explicitly exclude the sha1 hash of pkgAndName (see step 6)
    73  //
    74  //  5. is a suffix of the sha1 hash of pkgAndName (returns true)
    75  //
    76  //  6. OR
    77  //     if the (non-empty) value is in the regular language
    78  //     "(-[01]+/)+?([01]+(/[01]+)+?"
    79  //     (exclude..)(....include...)
    80  //     test the [01]+ exclude substrings, if any suffix-match, return false (4 above)
    81  //     test the [01]+ include substrings, if any suffix-match, return true
    82  //     The include substrings AFTER the first slash are numbered 0,1, etc and
    83  //     are named fmt.Sprintf("%s%d", varname, number)
    84  //     As an extra-special case for multiple failure search,
    85  //     an excludes-only string ending in a slash (terminated, not separated)
    86  //     implicitly specifies the include string "0/1", that is, match everything.
    87  //     (Exclude strings are used for automated search for multiple failures.)
    88  //     Clause 6 is not really intended for human use and only
    89  //     matters for failures that require multiple triggers.
    90  //
    91  // Otherwise it returns false.
    92  //
    93  // Unless Flags.Gossahash is empty, when DebugHashMatchPkgFunc returns true the message
    94  //
    95  //	"%s triggered %s\n", varname, pkgAndName
    96  //
    97  // is printed on the file named in environment variable GSHS_LOGFILE,
    98  // or standard out if that is empty.  "Varname" is either the name of
    99  // the variable or the name of the substring, depending on which matched.
   100  //
   101  // Typical use:
   102  //
   103  //  1. you make a change to the compiler, say, adding a new phase
   104  //
   105  //  2. it is broken in some mystifying way, for example, make.bash builds a broken
   106  //     compiler that almost works, but crashes compiling a test in run.bash.
   107  //
   108  //  3. add this guard to the code, which by default leaves it broken, but does not
   109  //     run the broken new code if Flags.Gossahash is non-empty and non-matching:
   110  //
   111  //     if !base.DebugHashMatch(ir.PkgFuncName(fn)) {
   112  //     return nil // early exit, do nothing
   113  //     }
   114  //
   115  //  4. rebuild w/o the bad code,
   116  //     GOCOMPILEDEBUG=gossahash=n ./all.bash
   117  //     to verify that you put the guard in the right place with the right sense of the test.
   118  //
   119  //  5. use github.com/dr2chase/gossahash to search for the error:
   120  //
   121  //     go install github.com/dr2chase/gossahash@latest
   122  //
   123  //     gossahash -- <the thing that fails>
   124  //
   125  //     for example: GOMAXPROCS=1 gossahash -- ./all.bash
   126  //
   127  //  6. gossahash should return a single function whose miscompilation
   128  //     causes the problem, and you can focus on that.
   129  func DebugHashMatchPkgFunc(pkg, fn string) bool {
   130  	return hashDebug.MatchPkgFunc(pkg, fn, nil)
   131  }
   132  
   133  func DebugHashMatchPos(pos src.XPos) bool {
   134  	return hashDebug.MatchPos(pos, nil)
   135  }
   136  
   137  // HasDebugHash returns true if Flags.Gossahash is non-empty, which
   138  // results in hashDebug being not-nil.  I.e., if !HasDebugHash(),
   139  // there is no need to create the string for hashing and testing.
   140  func HasDebugHash() bool {
   141  	return hashDebug != nil
   142  }
   143  
   144  // TODO: Delete when we switch to bisect-only.
   145  func toHashAndMask(s, varname string) hashAndMask {
   146  	l := len(s)
   147  	if l > 64 {
   148  		s = s[l-64:]
   149  		l = 64
   150  	}
   151  	m := ^(^uint64(0) << l)
   152  	h, err := strconv.ParseUint(s, 2, 64)
   153  	if err != nil {
   154  		Fatalf("Could not parse %s (=%s) as a binary number", varname, s)
   155  	}
   156  
   157  	return hashAndMask{name: varname, hash: h, mask: m}
   158  }
   159  
   160  // NewHashDebug returns a new hash-debug tester for the
   161  // environment variable ev.  If ev is not set, it returns
   162  // nil, allowing a lightweight check for normal-case behavior.
   163  func NewHashDebug(ev, s string, file io.Writer) *HashDebug {
   164  	if s == "" {
   165  		return nil
   166  	}
   167  
   168  	hd := &HashDebug{name: ev, logfile: file}
   169  	if !strings.Contains(s, "/") {
   170  		m, err := bisect.New(s)
   171  		if err != nil {
   172  			Fatalf("%s: %v", ev, err)
   173  		}
   174  		hd.bisect = m
   175  		return hd
   176  	}
   177  
   178  	// TODO: Delete remainder of function when we switch to bisect-only.
   179  	ss := strings.Split(s, "/")
   180  	// first remove any leading exclusions; these are preceded with "-"
   181  	i := 0
   182  	for len(ss) > 0 {
   183  		s := ss[0]
   184  		if len(s) == 0 || len(s) > 0 && s[0] != '-' {
   185  			break
   186  		}
   187  		ss = ss[1:]
   188  		hd.excludes = append(hd.excludes, toHashAndMask(s[1:], fmt.Sprintf("%s%d", "HASH_EXCLUDE", i)))
   189  		i++
   190  	}
   191  	// hash searches may use additional EVs with 0, 1, 2, ... suffixes.
   192  	i = 0
   193  	for _, s := range ss {
   194  		if s == "" {
   195  			if i != 0 || len(ss) > 1 && ss[1] != "" || len(ss) > 2 {
   196  				Fatalf("Empty hash match string for %s should be first (and only) one", ev)
   197  			}
   198  			// Special case of should match everything.
   199  			hd.matches = append(hd.matches, toHashAndMask("0", fmt.Sprintf("%s0", ev)))
   200  			hd.matches = append(hd.matches, toHashAndMask("1", fmt.Sprintf("%s1", ev)))
   201  			break
   202  		}
   203  		if i == 0 {
   204  			hd.matches = append(hd.matches, toHashAndMask(s, ev))
   205  		} else {
   206  			hd.matches = append(hd.matches, toHashAndMask(s, fmt.Sprintf("%s%d", ev, i-1)))
   207  		}
   208  		i++
   209  	}
   210  	return hd
   211  }
   212  
   213  // TODO: Delete when we switch to bisect-only.
   214  func (d *HashDebug) excluded(hash uint64) bool {
   215  	for _, m := range d.excludes {
   216  		if (m.hash^hash)&m.mask == 0 {
   217  			return true
   218  		}
   219  	}
   220  	return false
   221  }
   222  
   223  // TODO: Delete when we switch to bisect-only.
   224  func hashString(hash uint64) string {
   225  	hstr := ""
   226  	if hash == 0 {
   227  		hstr = "0"
   228  	} else {
   229  		for ; hash != 0; hash = hash >> 1 {
   230  			hstr = string('0'+byte(hash&1)) + hstr
   231  		}
   232  	}
   233  	if len(hstr) > 24 {
   234  		hstr = hstr[len(hstr)-24:]
   235  	}
   236  	return hstr
   237  }
   238  
   239  // TODO: Delete when we switch to bisect-only.
   240  func (d *HashDebug) match(hash uint64) *hashAndMask {
   241  	for i, m := range d.matches {
   242  		if (m.hash^hash)&m.mask == 0 {
   243  			return &d.matches[i]
   244  		}
   245  	}
   246  	return nil
   247  }
   248  
   249  // MatchPkgFunc returns true if either the variable used to create d is
   250  // unset, or if its value is y, or if it is a suffix of the base-two
   251  // representation of the hash of pkg and fn.  If the variable is not nil,
   252  // then a true result is accompanied by stylized output to d.logfile, which
   253  // is used for automated bug search.
   254  func (d *HashDebug) MatchPkgFunc(pkg, fn string, note func() string) bool {
   255  	if d == nil {
   256  		return true
   257  	}
   258  	// Written this way to make inlining likely.
   259  	return d.matchPkgFunc(pkg, fn, note)
   260  }
   261  
   262  func (d *HashDebug) matchPkgFunc(pkg, fn string, note func() string) bool {
   263  	hash := bisect.Hash(pkg, fn)
   264  	return d.matchAndLog(hash, func() string { return pkg + "." + fn }, note)
   265  }
   266  
   267  // MatchPos is similar to MatchPkgFunc, but for hash computation
   268  // it uses the source position including all inlining information instead of
   269  // package name and path.
   270  // Note that the default answer for no environment variable (d == nil)
   271  // is "yes", do the thing.
   272  func (d *HashDebug) MatchPos(pos src.XPos, desc func() string) bool {
   273  	if d == nil {
   274  		return true
   275  	}
   276  	// Written this way to make inlining likely.
   277  	return d.matchPos(Ctxt, pos, desc)
   278  }
   279  
   280  func (d *HashDebug) matchPos(ctxt *obj.Link, pos src.XPos, note func() string) bool {
   281  	return d.matchPosWithInfo(ctxt, pos, nil, note)
   282  }
   283  
   284  func (d *HashDebug) matchPosWithInfo(ctxt *obj.Link, pos src.XPos, info any, note func() string) bool {
   285  	hash := d.hashPos(ctxt, pos)
   286  	if info != nil {
   287  		hash = bisect.Hash(hash, info)
   288  	}
   289  	return d.matchAndLog(hash,
   290  		func() string {
   291  			r := d.fmtPos(ctxt, pos)
   292  			if info != nil {
   293  				r += fmt.Sprintf(" (%v)", info)
   294  			}
   295  			return r
   296  		},
   297  		note)
   298  }
   299  
   300  // MatchPosWithInfo is similar to MatchPos, but with additional information
   301  // that is included for hash computation, so it can distinguish multiple
   302  // matches on the same source location.
   303  // Note that the default answer for no environment variable (d == nil)
   304  // is "yes", do the thing.
   305  func (d *HashDebug) MatchPosWithInfo(pos src.XPos, info any, desc func() string) bool {
   306  	if d == nil {
   307  		return true
   308  	}
   309  	// Written this way to make inlining likely.
   310  	return d.matchPosWithInfo(Ctxt, pos, info, desc)
   311  }
   312  
   313  // matchAndLog is the core matcher. It reports whether the hash matches the pattern.
   314  // If a report needs to be printed, match prints that report to the log file.
   315  // The text func must be non-nil and should return a user-readable
   316  // representation of what was hashed. The note func may be nil; if non-nil,
   317  // it should return additional information to display to the user when this
   318  // change is selected.
   319  func (d *HashDebug) matchAndLog(hash uint64, text, note func() string) bool {
   320  	if d.bisect != nil {
   321  		enabled := d.bisect.ShouldEnable(hash)
   322  		if d.bisect.ShouldPrint(hash) {
   323  			disabled := ""
   324  			if !enabled {
   325  				disabled = " [DISABLED]"
   326  			}
   327  			var t string
   328  			if !d.bisect.MarkerOnly() {
   329  				t = text()
   330  				if note != nil {
   331  					if n := note(); n != "" {
   332  						t += ": " + n + disabled
   333  						disabled = ""
   334  					}
   335  				}
   336  			}
   337  			d.log(d.name, hash, strings.TrimSpace(t+disabled))
   338  		}
   339  		return enabled
   340  	}
   341  
   342  	// TODO: Delete rest of function body when we switch to bisect-only.
   343  	if d.excluded(hash) {
   344  		return false
   345  	}
   346  	if m := d.match(hash); m != nil {
   347  		d.log(m.name, hash, text())
   348  		return true
   349  	}
   350  	return false
   351  }
   352  
   353  // short returns the form of file name to use for d.
   354  // The default is the full path, but fileSuffixOnly selects
   355  // just the final path element.
   356  func (d *HashDebug) short(name string) string {
   357  	if d.fileSuffixOnly {
   358  		return filepath.Base(name)
   359  	}
   360  	return name
   361  }
   362  
   363  // hashPos returns a hash of the position pos, including its entire inline stack.
   364  // If d.inlineSuffixOnly is true, hashPos only considers the innermost (leaf) position on the inline stack.
   365  func (d *HashDebug) hashPos(ctxt *obj.Link, pos src.XPos) uint64 {
   366  	if d.inlineSuffixOnly {
   367  		p := ctxt.InnermostPos(pos)
   368  		return bisect.Hash(d.short(p.Filename()), p.Line(), p.Col())
   369  	}
   370  	h := bisect.Hash()
   371  	ctxt.AllPos(pos, func(p src.Pos) {
   372  		h = bisect.Hash(h, d.short(p.Filename()), p.Line(), p.Col())
   373  	})
   374  	return h
   375  }
   376  
   377  // fmtPos returns a textual formatting of the position pos, including its entire inline stack.
   378  // If d.inlineSuffixOnly is true, fmtPos only considers the innermost (leaf) position on the inline stack.
   379  func (d *HashDebug) fmtPos(ctxt *obj.Link, pos src.XPos) string {
   380  	format := func(p src.Pos) string {
   381  		return fmt.Sprintf("%s:%d:%d", d.short(p.Filename()), p.Line(), p.Col())
   382  	}
   383  	if d.inlineSuffixOnly {
   384  		return format(ctxt.InnermostPos(pos))
   385  	}
   386  	var stk []string
   387  	ctxt.AllPos(pos, func(p src.Pos) {
   388  		stk = append(stk, format(p))
   389  	})
   390  	return strings.Join(stk, "; ")
   391  }
   392  
   393  // log prints a match with the given hash and textual formatting.
   394  // TODO: Delete varname parameter when we switch to bisect-only.
   395  func (d *HashDebug) log(varname string, hash uint64, text string) {
   396  	d.mu.Lock()
   397  	defer d.mu.Unlock()
   398  
   399  	file := d.logfile
   400  	if file == nil {
   401  		if tmpfile := os.Getenv("GSHS_LOGFILE"); tmpfile != "" {
   402  			var err error
   403  			file, err = os.OpenFile(tmpfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
   404  			if err != nil {
   405  				Fatalf("could not open hash-testing logfile %s", tmpfile)
   406  				return
   407  			}
   408  		}
   409  		if file == nil {
   410  			file = os.Stdout
   411  		}
   412  		d.logfile = file
   413  	}
   414  
   415  	// Bisect output.
   416  	fmt.Fprintf(file, "%s %s\n", text, bisect.Marker(hash))
   417  
   418  	// Gossahash output.
   419  	// TODO: Delete rest of function when we switch to bisect-only.
   420  	fmt.Fprintf(file, "%s triggered %s %s\n", varname, text, hashString(hash))
   421  }
   422  

View as plain text