github.com/bir3/gocompiler@v0.3.205/src/cmd/compile/internal/base/hashdebug.go (about)

     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  	"github.com/bir3/gocompiler/src/cmd/internal/notsha256"
    10  	"github.com/bir3/gocompiler/src/cmd/internal/obj"
    11  	"github.com/bir3/gocompiler/src/cmd/internal/src"
    12  	"fmt"
    13  	"io"
    14  	"os"
    15  	"strconv"
    16  	"strings"
    17  	"sync"
    18  )
    19  
    20  type writeSyncer interface {
    21  	io.Writer
    22  	Sync() error
    23  }
    24  
    25  type hashAndMask struct {
    26  	// a hash h matches if (h^hash)&mask == 0
    27  	hash uint64
    28  	mask uint64
    29  	name string // base name, or base name + "0", "1", etc.
    30  }
    31  
    32  type HashDebug struct {
    33  	mu   sync.Mutex // for logfile, posTmp, bytesTmp
    34  	name string     // base name of the flag/variable.
    35  	// what file (if any) receives the yes/no logging?
    36  	// default is os.Stdout
    37  	logfile  writeSyncer
    38  	posTmp   []src.Pos
    39  	bytesTmp bytes.Buffer
    40  	matches  []hashAndMask // A hash matches if one of these matches.
    41  	yes, no  bool
    42  }
    43  
    44  // The default compiler-debugging HashDebug, for "-d=gossahash=..."
    45  var hashDebug *HashDebug
    46  var FmaHash *HashDebug
    47  
    48  // DebugHashMatch reports whether debug variable Gossahash
    49  //
    50  //  1. is empty (returns true; this is a special more-quickly implemented case of 4 below)
    51  //
    52  //  2. is "y" or "Y" (returns true)
    53  //
    54  //  3. is "n" or "N" (returns false)
    55  //
    56  //  4. is a suffix of the sha1 hash of pkgAndName (returns true)
    57  //
    58  //  5. OR
    59  //     if the value is in the regular language "[01]+(;[01]+)+"
    60  //     test the [01]+ substrings after in order returning true
    61  //     for the first one that suffix-matches. The substrings AFTER
    62  //     the first semicolon are numbered 0,1, etc and are named
    63  //     fmt.Sprintf("%s%d", varname, number)
    64  //     Clause 5 is not really intended for human use and only
    65  //     matters for failures that require multiple triggers.
    66  //
    67  // Otherwise it returns false.
    68  //
    69  // Unless Flags.Gossahash is empty, when DebugHashMatch returns true the message
    70  //
    71  //	"%s triggered %s\n", varname, pkgAndName
    72  //
    73  // is printed on the file named in environment variable GSHS_LOGFILE,
    74  // or standard out if that is empty.  "Varname" is either the name of
    75  // the variable or the name of the substring, depending on which matched.
    76  //
    77  // Typical use:
    78  //
    79  //  1. you make a change to the compiler, say, adding a new phase
    80  //
    81  //  2. it is broken in some mystifying way, for example, make.bash builds a broken
    82  //     compiler that almost works, but crashes compiling a test in run.bash.
    83  //
    84  //  3. add this guard to the code, which by default leaves it broken, but does not
    85  //     run the broken new code if Flags.Gossahash is non-empty and non-matching:
    86  //
    87  //     if !base.DebugHashMatch(ir.PkgFuncName(fn)) {
    88  //     return nil // early exit, do nothing
    89  //     }
    90  //
    91  //  4. rebuild w/o the bad code,
    92  //     GOCOMPILEDEBUG=gossahash=n ./all.bash
    93  //     to verify that you put the guard in the right place with the right sense of the test.
    94  //
    95  //  5. use github.com/dr2chase/gossahash to search for the error:
    96  //
    97  //     go install github.com/dr2chase/gossahash@latest
    98  //
    99  //     gossahash -- <the thing that fails>
   100  //
   101  //     for example: GOMAXPROCS=1 gossahash -- ./all.bash
   102  //
   103  //  6. gossahash should return a single function whose miscompilation
   104  //     causes the problem, and you can focus on that.
   105  func DebugHashMatch(pkgAndName string) bool {
   106  	return hashDebug.DebugHashMatch(pkgAndName)
   107  }
   108  
   109  // HasDebugHash returns true if Flags.Gossahash is non-empty, which
   110  // results in hashDebug being not-nil.  I.e., if !HasDebugHash(),
   111  // there is no need to create the string for hashing and testing.
   112  func HasDebugHash() bool {
   113  	return hashDebug != nil
   114  }
   115  
   116  func toHashAndMask(s, varname string) hashAndMask {
   117  	l := len(s)
   118  	if l > 64 {
   119  		s = s[l-64:]
   120  		l = 64
   121  	}
   122  	m := ^(^uint64(0) << l)
   123  	h, err := strconv.ParseUint(s, 2, 64)
   124  	if err != nil {
   125  		Fatalf("Could not parse %s (=%s) as a binary number", varname, s)
   126  	}
   127  
   128  	return hashAndMask{name: varname, hash: h, mask: m}
   129  }
   130  
   131  // NewHashDebug returns a new hash-debug tester for the
   132  // environment variable ev.  If ev is not set, it returns
   133  // nil, allowing a lightweight check for normal-case behavior.
   134  func NewHashDebug(ev, s string, file writeSyncer) *HashDebug {
   135  	if s == "" {
   136  		return nil
   137  	}
   138  
   139  	hd := &HashDebug{name: ev, logfile: file}
   140  	switch s[0] {
   141  	case 'y', 'Y':
   142  		hd.yes = true
   143  		return hd
   144  	case 'n', 'N':
   145  		hd.no = true
   146  		return hd
   147  	}
   148  	ss := strings.Split(s, "/")
   149  	hd.matches = append(hd.matches, toHashAndMask(ss[0], ev))
   150  	// hash searches may use additional EVs with 0, 1, 2, ... suffixes.
   151  	for i := 1; i < len(ss); i++ {
   152  		evi := fmt.Sprintf("%s%d", ev, i-1) // convention is extras begin indexing at zero
   153  		hd.matches = append(hd.matches, toHashAndMask(ss[i], evi))
   154  	}
   155  	return hd
   156  
   157  }
   158  
   159  func hashOf(pkgAndName string, param uint64) uint64 {
   160  	return hashOfBytes([]byte(pkgAndName), param)
   161  }
   162  
   163  func hashOfBytes(sbytes []byte, param uint64) uint64 {
   164  	hbytes := notsha256.Sum256(sbytes)
   165  	hash := uint64(hbytes[7])<<56 + uint64(hbytes[6])<<48 +
   166  		uint64(hbytes[5])<<40 + uint64(hbytes[4])<<32 +
   167  		uint64(hbytes[3])<<24 + uint64(hbytes[2])<<16 +
   168  		uint64(hbytes[1])<<8 + uint64(hbytes[0])
   169  
   170  	if param != 0 {
   171  		// Because param is probably a line number, probably near zero,
   172  		// hash it up a little bit, but even so only the lower-order bits
   173  		// likely matter because search focuses on those.
   174  		p0 := param + uint64(hbytes[9]) + uint64(hbytes[10])<<8 +
   175  			uint64(hbytes[11])<<16 + uint64(hbytes[12])<<24
   176  
   177  		p1 := param + uint64(hbytes[13]) + uint64(hbytes[14])<<8 +
   178  			uint64(hbytes[15])<<16 + uint64(hbytes[16])<<24
   179  
   180  		param += p0 * p1
   181  		param ^= param>>17 ^ param<<47
   182  	}
   183  
   184  	return hash ^ param
   185  }
   186  
   187  // DebugHashMatch returns true if either the variable used to create d is
   188  // unset, or if its value is y, or if it is a suffix of the base-two
   189  // representation of the hash of pkgAndName.  If the variable is not nil,
   190  // then a true result is accompanied by stylized output to d.logfile, which
   191  // is used for automated bug search.
   192  func (d *HashDebug) DebugHashMatch(pkgAndName string) bool {
   193  	return d.DebugHashMatchParam(pkgAndName, 0)
   194  }
   195  
   196  // DebugHashMatchParam returns true if either the variable used to create d is
   197  // unset, or if its value is y, or if it is a suffix of the base-two
   198  // representation of the hash of pkgAndName and param. If the variable is not
   199  // nil, then a true result is accompanied by stylized output to d.logfile,
   200  // which is used for automated bug search.
   201  func (d *HashDebug) DebugHashMatchParam(pkgAndName string, param uint64) bool {
   202  	if d == nil {
   203  		return true
   204  	}
   205  	if d.no {
   206  		return false
   207  	}
   208  
   209  	if d.yes {
   210  		d.logDebugHashMatch(d.name, pkgAndName, "y", param)
   211  		return true
   212  	}
   213  
   214  	hash := hashOf(pkgAndName, param)
   215  
   216  	for _, m := range d.matches {
   217  		if (m.hash^hash)&m.mask == 0 {
   218  			hstr := ""
   219  			if hash == 0 {
   220  				hstr = "0"
   221  			} else {
   222  				for ; hash != 0; hash = hash >> 1 {
   223  					hstr = string('0'+byte(hash&1)) + hstr
   224  				}
   225  			}
   226  			d.logDebugHashMatch(m.name, pkgAndName, hstr, param)
   227  			return true
   228  		}
   229  	}
   230  	return false
   231  }
   232  
   233  // DebugHashMatchPos is similar to DebugHashMatchParam, but for hash computation
   234  // it uses the source position including all inlining information instead of
   235  // package name and path. The output trigger string is prefixed with "POS=" so
   236  // that tools processing the output can reliably tell the difference. The mutex
   237  // locking is also more frequent and more granular.
   238  func (d *HashDebug) DebugHashMatchPos(ctxt *obj.Link, pos src.XPos) bool {
   239  	if d == nil {
   240  		return true
   241  	}
   242  	if d.no {
   243  		return false
   244  	}
   245  	d.mu.Lock()
   246  	defer d.mu.Unlock()
   247  
   248  	b := d.bytesForPos(ctxt, pos)
   249  
   250  	if d.yes {
   251  		d.logDebugHashMatchLocked(d.name, string(b), "y", 0)
   252  		return true
   253  	}
   254  
   255  	hash := hashOfBytes(b, 0)
   256  
   257  	for _, m := range d.matches {
   258  		if (m.hash^hash)&m.mask == 0 {
   259  			hstr := ""
   260  			if hash == 0 {
   261  				hstr = "0"
   262  			} else {
   263  				for ; hash != 0; hash = hash >> 1 {
   264  					hstr = string('0'+byte(hash&1)) + hstr
   265  				}
   266  			}
   267  			d.logDebugHashMatchLocked(m.name, "POS="+string(b), hstr, 0)
   268  			return true
   269  		}
   270  	}
   271  	return false
   272  }
   273  
   274  // bytesForPos renders a position, including inlining, into d.bytesTmp
   275  // and returns the byte array.  d.mu must be locked.
   276  func (d *HashDebug) bytesForPos(ctxt *obj.Link, pos src.XPos) []byte {
   277  	d.posTmp = ctxt.AllPos(pos, d.posTmp)
   278  	// Reverse posTmp to put outermost first.
   279  	b := &d.bytesTmp
   280  	b.Reset()
   281  	for i := len(d.posTmp) - 1; i >= 0; i-- {
   282  		p := &d.posTmp[i]
   283  		fmt.Fprintf(b, "%s:%d:%d", p.Filename(), p.Line(), p.Col())
   284  		if i != 0 {
   285  			b.WriteByte(';')
   286  		}
   287  	}
   288  	return b.Bytes()
   289  }
   290  
   291  func (d *HashDebug) logDebugHashMatch(varname, name, hstr string, param uint64) {
   292  	d.mu.Lock()
   293  	defer d.mu.Unlock()
   294  	d.logDebugHashMatchLocked(varname, name, hstr, param)
   295  }
   296  
   297  func (d *HashDebug) logDebugHashMatchLocked(varname, name, hstr string, param uint64) {
   298  	file := d.logfile
   299  	if file == nil {
   300  		if tmpfile := os.Getenv("GSHS_LOGFILE"); tmpfile != "" {
   301  			var err error
   302  			file, err = os.OpenFile(tmpfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
   303  			if err != nil {
   304  				Fatalf("could not open hash-testing logfile %s", tmpfile)
   305  				return
   306  			}
   307  		}
   308  		if file == nil {
   309  			file = os.Stdout
   310  		}
   311  		d.logfile = file
   312  	}
   313  	if len(hstr) > 24 {
   314  		hstr = hstr[len(hstr)-24:]
   315  	}
   316  	// External tools depend on this string
   317  	if param == 0 {
   318  		fmt.Fprintf(file, "%s triggered %s %s\n", varname, name, hstr)
   319  	} else {
   320  		fmt.Fprintf(file, "%s triggered %s:%d %s\n", varname, name, param, hstr)
   321  	}
   322  	file.Sync()
   323  }