mirror of https://github.com/golang/go.git
418 lines
13 KiB
Go
418 lines
13 KiB
Go
// Copyright 2022 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 base
|
|
|
|
import (
|
|
"bytes"
|
|
"cmd/internal/obj"
|
|
"cmd/internal/src"
|
|
"fmt"
|
|
"internal/bisect"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
)
|
|
|
|
type hashAndMask struct {
|
|
// a hash h matches if (h^hash)&mask == 0
|
|
hash uint64
|
|
mask uint64
|
|
name string // base name, or base name + "0", "1", etc.
|
|
}
|
|
|
|
type HashDebug struct {
|
|
mu sync.Mutex // for logfile, posTmp, bytesTmp
|
|
name string // base name of the flag/variable.
|
|
// what file (if any) receives the yes/no logging?
|
|
// default is os.Stdout
|
|
logfile io.Writer
|
|
posTmp []src.Pos
|
|
bytesTmp bytes.Buffer
|
|
matches []hashAndMask // A hash matches if one of these matches.
|
|
excludes []hashAndMask // explicitly excluded hash suffixes
|
|
bisect *bisect.Matcher
|
|
fileSuffixOnly bool // for Pos hashes, remove the directory prefix.
|
|
inlineSuffixOnly bool // for Pos hashes, remove all but the most inline position.
|
|
}
|
|
|
|
// SetInlineSuffixOnly controls whether hashing and reporting use the entire
|
|
// inline position, or just the most-inline suffix. Compiler debugging tends
|
|
// to want the whole inlining, debugging user problems (loopvarhash, e.g.)
|
|
// typically does not need to see the entire inline tree, there is just one
|
|
// copy of the source code.
|
|
func (d *HashDebug) SetInlineSuffixOnly(b bool) *HashDebug {
|
|
d.inlineSuffixOnly = b
|
|
return d
|
|
}
|
|
|
|
// The default compiler-debugging HashDebug, for "-d=gossahash=..."
|
|
var hashDebug *HashDebug
|
|
|
|
var FmaHash *HashDebug // for debugging fused-multiply-add floating point changes
|
|
var LoopVarHash *HashDebug // for debugging shared/private loop variable changes
|
|
var PGOHash *HashDebug // for debugging PGO optimization decisions
|
|
|
|
// DebugHashMatchPkgFunc reports whether debug variable Gossahash
|
|
//
|
|
// 1. is empty (returns true; this is a special more-quickly implemented case of 4 below)
|
|
//
|
|
// 2. is "y" or "Y" (returns true)
|
|
//
|
|
// 3. is "n" or "N" (returns false)
|
|
//
|
|
// 4. does not explicitly exclude the sha1 hash of pkgAndName (see step 6)
|
|
//
|
|
// 5. is a suffix of the sha1 hash of pkgAndName (returns true)
|
|
//
|
|
// 6. OR
|
|
// if the (non-empty) value is in the regular language
|
|
// "(-[01]+/)+?([01]+(/[01]+)+?"
|
|
// (exclude..)(....include...)
|
|
// test the [01]+ exclude substrings, if any suffix-match, return false (4 above)
|
|
// test the [01]+ include substrings, if any suffix-match, return true
|
|
// The include substrings AFTER the first slash are numbered 0,1, etc and
|
|
// are named fmt.Sprintf("%s%d", varname, number)
|
|
// As an extra-special case for multiple failure search,
|
|
// an excludes-only string ending in a slash (terminated, not separated)
|
|
// implicitly specifies the include string "0/1", that is, match everything.
|
|
// (Exclude strings are used for automated search for multiple failures.)
|
|
// Clause 6 is not really intended for human use and only
|
|
// matters for failures that require multiple triggers.
|
|
//
|
|
// Otherwise it returns false.
|
|
//
|
|
// Unless Flags.Gossahash is empty, when DebugHashMatchPkgFunc returns true the message
|
|
//
|
|
// "%s triggered %s\n", varname, pkgAndName
|
|
//
|
|
// is printed on the file named in environment variable GSHS_LOGFILE,
|
|
// or standard out if that is empty. "Varname" is either the name of
|
|
// the variable or the name of the substring, depending on which matched.
|
|
//
|
|
// Typical use:
|
|
//
|
|
// 1. you make a change to the compiler, say, adding a new phase
|
|
//
|
|
// 2. it is broken in some mystifying way, for example, make.bash builds a broken
|
|
// compiler that almost works, but crashes compiling a test in run.bash.
|
|
//
|
|
// 3. add this guard to the code, which by default leaves it broken, but does not
|
|
// run the broken new code if Flags.Gossahash is non-empty and non-matching:
|
|
//
|
|
// if !base.DebugHashMatch(ir.PkgFuncName(fn)) {
|
|
// return nil // early exit, do nothing
|
|
// }
|
|
//
|
|
// 4. rebuild w/o the bad code,
|
|
// GOCOMPILEDEBUG=gossahash=n ./all.bash
|
|
// to verify that you put the guard in the right place with the right sense of the test.
|
|
//
|
|
// 5. use github.com/dr2chase/gossahash to search for the error:
|
|
//
|
|
// go install github.com/dr2chase/gossahash@latest
|
|
//
|
|
// gossahash -- <the thing that fails>
|
|
//
|
|
// for example: GOMAXPROCS=1 gossahash -- ./all.bash
|
|
//
|
|
// 6. gossahash should return a single function whose miscompilation
|
|
// causes the problem, and you can focus on that.
|
|
func DebugHashMatchPkgFunc(pkg, fn string) bool {
|
|
return hashDebug.MatchPkgFunc(pkg, fn, nil)
|
|
}
|
|
|
|
func DebugHashMatchPos(pos src.XPos) bool {
|
|
return hashDebug.MatchPos(pos, nil)
|
|
}
|
|
|
|
// HasDebugHash returns true if Flags.Gossahash is non-empty, which
|
|
// results in hashDebug being not-nil. I.e., if !HasDebugHash(),
|
|
// there is no need to create the string for hashing and testing.
|
|
func HasDebugHash() bool {
|
|
return hashDebug != nil
|
|
}
|
|
|
|
// TODO: Delete when we switch to bisect-only.
|
|
func toHashAndMask(s, varname string) hashAndMask {
|
|
l := len(s)
|
|
if l > 64 {
|
|
s = s[l-64:]
|
|
l = 64
|
|
}
|
|
m := ^(^uint64(0) << l)
|
|
h, err := strconv.ParseUint(s, 2, 64)
|
|
if err != nil {
|
|
Fatalf("Could not parse %s (=%s) as a binary number", varname, s)
|
|
}
|
|
|
|
return hashAndMask{name: varname, hash: h, mask: m}
|
|
}
|
|
|
|
// NewHashDebug returns a new hash-debug tester for the
|
|
// environment variable ev. If ev is not set, it returns
|
|
// nil, allowing a lightweight check for normal-case behavior.
|
|
func NewHashDebug(ev, s string, file io.Writer) *HashDebug {
|
|
if s == "" {
|
|
return nil
|
|
}
|
|
|
|
hd := &HashDebug{name: ev, logfile: file}
|
|
if !strings.Contains(s, "/") {
|
|
m, err := bisect.New(s)
|
|
if err != nil {
|
|
Fatalf("%s: %v", ev, err)
|
|
}
|
|
hd.bisect = m
|
|
return hd
|
|
}
|
|
|
|
// TODO: Delete remainder of function when we switch to bisect-only.
|
|
ss := strings.Split(s, "/")
|
|
// first remove any leading exclusions; these are preceded with "-"
|
|
i := 0
|
|
for len(ss) > 0 {
|
|
s := ss[0]
|
|
if len(s) == 0 || len(s) > 0 && s[0] != '-' {
|
|
break
|
|
}
|
|
ss = ss[1:]
|
|
hd.excludes = append(hd.excludes, toHashAndMask(s[1:], fmt.Sprintf("%s%d", "HASH_EXCLUDE", i)))
|
|
i++
|
|
}
|
|
// hash searches may use additional EVs with 0, 1, 2, ... suffixes.
|
|
i = 0
|
|
for _, s := range ss {
|
|
if s == "" {
|
|
if i != 0 || len(ss) > 1 && ss[1] != "" || len(ss) > 2 {
|
|
Fatalf("Empty hash match string for %s should be first (and only) one", ev)
|
|
}
|
|
// Special case of should match everything.
|
|
hd.matches = append(hd.matches, toHashAndMask("0", fmt.Sprintf("%s0", ev)))
|
|
hd.matches = append(hd.matches, toHashAndMask("1", fmt.Sprintf("%s1", ev)))
|
|
break
|
|
}
|
|
if i == 0 {
|
|
hd.matches = append(hd.matches, toHashAndMask(s, ev))
|
|
} else {
|
|
hd.matches = append(hd.matches, toHashAndMask(s, fmt.Sprintf("%s%d", ev, i-1)))
|
|
}
|
|
i++
|
|
}
|
|
return hd
|
|
}
|
|
|
|
// TODO: Delete when we switch to bisect-only.
|
|
func (d *HashDebug) excluded(hash uint64) bool {
|
|
for _, m := range d.excludes {
|
|
if (m.hash^hash)&m.mask == 0 {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// TODO: Delete when we switch to bisect-only.
|
|
func hashString(hash uint64) string {
|
|
hstr := ""
|
|
if hash == 0 {
|
|
hstr = "0"
|
|
} else {
|
|
for ; hash != 0; hash = hash >> 1 {
|
|
hstr = string('0'+byte(hash&1)) + hstr
|
|
}
|
|
}
|
|
if len(hstr) > 24 {
|
|
hstr = hstr[len(hstr)-24:]
|
|
}
|
|
return hstr
|
|
}
|
|
|
|
// TODO: Delete when we switch to bisect-only.
|
|
func (d *HashDebug) match(hash uint64) *hashAndMask {
|
|
for i, m := range d.matches {
|
|
if (m.hash^hash)&m.mask == 0 {
|
|
return &d.matches[i]
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// MatchPkgFunc returns true if either the variable used to create d is
|
|
// unset, or if its value is y, or if it is a suffix of the base-two
|
|
// representation of the hash of pkg and fn. If the variable is not nil,
|
|
// then a true result is accompanied by stylized output to d.logfile, which
|
|
// is used for automated bug search.
|
|
func (d *HashDebug) MatchPkgFunc(pkg, fn string, note func() string) bool {
|
|
if d == nil {
|
|
return true
|
|
}
|
|
// Written this way to make inlining likely.
|
|
return d.matchPkgFunc(pkg, fn, note)
|
|
}
|
|
|
|
func (d *HashDebug) matchPkgFunc(pkg, fn string, note func() string) bool {
|
|
hash := bisect.Hash(pkg, fn)
|
|
return d.matchAndLog(hash, func() string { return pkg + "." + fn }, note)
|
|
}
|
|
|
|
// MatchPos is similar to MatchPkgFunc, but for hash computation
|
|
// it uses the source position including all inlining information instead of
|
|
// package name and path.
|
|
// Note that the default answer for no environment variable (d == nil)
|
|
// is "yes", do the thing.
|
|
func (d *HashDebug) MatchPos(pos src.XPos, desc func() string) bool {
|
|
if d == nil {
|
|
return true
|
|
}
|
|
// Written this way to make inlining likely.
|
|
return d.matchPos(Ctxt, pos, desc)
|
|
}
|
|
|
|
func (d *HashDebug) matchPos(ctxt *obj.Link, pos src.XPos, note func() string) bool {
|
|
return d.matchPosWithInfo(ctxt, pos, nil, note)
|
|
}
|
|
|
|
func (d *HashDebug) matchPosWithInfo(ctxt *obj.Link, pos src.XPos, info any, note func() string) bool {
|
|
hash := d.hashPos(ctxt, pos)
|
|
if info != nil {
|
|
hash = bisect.Hash(hash, info)
|
|
}
|
|
return d.matchAndLog(hash,
|
|
func() string {
|
|
r := d.fmtPos(ctxt, pos)
|
|
if info != nil {
|
|
r += fmt.Sprintf(" (%v)", info)
|
|
}
|
|
return r
|
|
},
|
|
note)
|
|
}
|
|
|
|
// MatchPosWithInfo is similar to MatchPos, but with additional information
|
|
// that is included for hash computation, so it can distinguish multiple
|
|
// matches on the same source location.
|
|
// Note that the default answer for no environment variable (d == nil)
|
|
// is "yes", do the thing.
|
|
func (d *HashDebug) MatchPosWithInfo(pos src.XPos, info any, desc func() string) bool {
|
|
if d == nil {
|
|
return true
|
|
}
|
|
// Written this way to make inlining likely.
|
|
return d.matchPosWithInfo(Ctxt, pos, info, desc)
|
|
}
|
|
|
|
// matchAndLog is the core matcher. It reports whether the hash matches the pattern.
|
|
// If a report needs to be printed, match prints that report to the log file.
|
|
// The text func must be non-nil and should return a user-readable
|
|
// representation of what was hashed. The note func may be nil; if non-nil,
|
|
// it should return additional information to display to the user when this
|
|
// change is selected.
|
|
func (d *HashDebug) matchAndLog(hash uint64, text, note func() string) bool {
|
|
if d.bisect != nil {
|
|
enabled := d.bisect.ShouldEnable(hash)
|
|
if d.bisect.ShouldPrint(hash) {
|
|
disabled := ""
|
|
if !enabled {
|
|
disabled = " [DISABLED]"
|
|
}
|
|
var t string
|
|
if !d.bisect.MarkerOnly() {
|
|
t = text()
|
|
if note != nil {
|
|
if n := note(); n != "" {
|
|
t += ": " + n + disabled
|
|
disabled = ""
|
|
}
|
|
}
|
|
}
|
|
d.log(d.name, hash, strings.TrimSpace(t+disabled))
|
|
}
|
|
return enabled
|
|
}
|
|
|
|
// TODO: Delete rest of function body when we switch to bisect-only.
|
|
if d.excluded(hash) {
|
|
return false
|
|
}
|
|
if m := d.match(hash); m != nil {
|
|
d.log(m.name, hash, text())
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
// short returns the form of file name to use for d.
|
|
// The default is the full path, but fileSuffixOnly selects
|
|
// just the final path element.
|
|
func (d *HashDebug) short(name string) string {
|
|
if d.fileSuffixOnly {
|
|
return filepath.Base(name)
|
|
}
|
|
return name
|
|
}
|
|
|
|
// hashPos returns a hash of the position pos, including its entire inline stack.
|
|
// If d.inlineSuffixOnly is true, hashPos only considers the innermost (leaf) position on the inline stack.
|
|
func (d *HashDebug) hashPos(ctxt *obj.Link, pos src.XPos) uint64 {
|
|
if d.inlineSuffixOnly {
|
|
p := ctxt.InnermostPos(pos)
|
|
return bisect.Hash(d.short(p.Filename()), p.Line(), p.Col())
|
|
}
|
|
h := bisect.Hash()
|
|
ctxt.AllPos(pos, func(p src.Pos) {
|
|
h = bisect.Hash(h, d.short(p.Filename()), p.Line(), p.Col())
|
|
})
|
|
return h
|
|
}
|
|
|
|
// fmtPos returns a textual formatting of the position pos, including its entire inline stack.
|
|
// If d.inlineSuffixOnly is true, fmtPos only considers the innermost (leaf) position on the inline stack.
|
|
func (d *HashDebug) fmtPos(ctxt *obj.Link, pos src.XPos) string {
|
|
format := func(p src.Pos) string {
|
|
return fmt.Sprintf("%s:%d:%d", d.short(p.Filename()), p.Line(), p.Col())
|
|
}
|
|
if d.inlineSuffixOnly {
|
|
return format(ctxt.InnermostPos(pos))
|
|
}
|
|
var stk []string
|
|
ctxt.AllPos(pos, func(p src.Pos) {
|
|
stk = append(stk, format(p))
|
|
})
|
|
return strings.Join(stk, "; ")
|
|
}
|
|
|
|
// log prints a match with the given hash and textual formatting.
|
|
// TODO: Delete varname parameter when we switch to bisect-only.
|
|
func (d *HashDebug) log(varname string, hash uint64, text string) {
|
|
d.mu.Lock()
|
|
defer d.mu.Unlock()
|
|
|
|
file := d.logfile
|
|
if file == nil {
|
|
if tmpfile := os.Getenv("GSHS_LOGFILE"); tmpfile != "" {
|
|
var err error
|
|
file, err = os.OpenFile(tmpfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
|
|
if err != nil {
|
|
Fatalf("could not open hash-testing logfile %s", tmpfile)
|
|
return
|
|
}
|
|
}
|
|
if file == nil {
|
|
file = os.Stdout
|
|
}
|
|
d.logfile = file
|
|
}
|
|
|
|
// Bisect output.
|
|
fmt.Fprintf(file, "%s %s\n", text, bisect.Marker(hash))
|
|
|
|
// Gossahash output.
|
|
// TODO: Delete rest of function when we switch to bisect-only.
|
|
fmt.Fprintf(file, "%s triggered %s %s\n", varname, text, hashString(hash))
|
|
}
|