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