cmd/bisect: add -compile and -godebug shorthands

This makes it easier for people to invoke bisect for its usual use cases:

	bisect -compile=loopvar go test

Change-Id: I60ae74f145a2aef7b8d028accc89264e80019d1e
Reviewed-on: https://go-review.googlesource.com/c/tools/+/493856
Run-TryBot: Russ Cox <rsc@golang.org>
Reviewed-by: David Chase <drchase@google.com>
TryBot-Result: Gopher Robot <gobot@golang.org>
gopls-CI: kokoro <noreply+kokoro@google.com>
diff --git a/cmd/bisect/main.go b/cmd/bisect/main.go
index 1fc2b02..baae1fa 100644
--- a/cmd/bisect/main.go
+++ b/cmd/bisect/main.go
@@ -16,7 +16,7 @@
 // code zero). With all the changes enabled, the target is known to fail
 // (exit any other way). Bisect repeats the target with different sets of
 // changes enabled, using binary search to find (non-overlapping) minimal
-// change sets that preserve the failure.
+// change sets that provoke the failure.
 //
 // The target must cooperate with bisect by accepting a change pattern
 // and then enabling and reporting the changes that match that pattern.
@@ -29,25 +29,36 @@
 // targets implement this protocol. We plan to publish that package
 // in a non-internal location after finalizing its API.
 //
+// Bisect starts by running the target with no changes enabled and then
+// with all changes enabled. It expects the former to succeed and the latter to fail,
+// and then it will search for the minimal set of changes that must be enabled
+// to provoke the failure. If the situation is reversed – the target fails with no
+// changes enabled and succeeds with all changes enabled – then bisect
+// automatically runs in reverse as well, searching for the minimal set of changes
+// that must be disabled to provoke the failure.
+//
+// Bisect prints tracing logs to standard error and the minimal change sets
+// to standard output.
+//
 // # Command Line Flags
 //
 // Bisect supports the following command-line flags:
 //
-//	-max M
+//	-max=M
 //
 // Stop after finding M minimal change sets. The default is no maximum, meaning to run until
 // all changes that provoke a failure have been identified.
 //
-//	-maxset S
+//	-maxset=S
 //
 // Disallow change sets larger than S elements. The default is no maximum.
 //
-//	-timeout D
+//	-timeout=D
 //
 // If the target runs for longer than duration D, stop the target and interpret that as a failure.
 // The default is no timeout.
 //
-//	-count N
+//	-count=N
 //
 // Run each trial N times (default 2), checking for consistency.
 //
@@ -55,19 +66,47 @@
 //
 // Print verbose output, showing each run and its match lines.
 //
+// In addition to these general flags,
+// bisect supports a few “shortcut” flags that make it more convenient
+// to use with specific targets.
+//
+//	-compile=<rewrite>
+//
+// This flag is equivalent to adding an environment variable
+// “GOCOMPILEDEBUG=<rewrite>hash=PATTERN”,
+// which, as discussed in more detail in the example below,
+// allows bisect to identify the specific source locations where the
+// compiler rewrite causes the target to fail.
+//
+//	-godebug=<name>=<value>
+//
+// This flag is equivalent to adding an environment variable
+// “GODEBUG=<name>=<value>#PATTERN”,
+// which allows bisect to identify the specific call stacks where
+// the changed [GODEBUG setting] value causes the target to fail.
+//
 // # Example
 //
-// For example, the Go compiler can be used as a bisect target to
-// determine the source locations that cause a test failure when compiled with
-// a new optimization:
+// The Go compiler provides support for enabling or disabling certain rewrites
+// and optimizations to allow bisect to identify specific source locations where
+// the rewrite causes the program to fail. For example, to bisect a failure caused
+// by the new loop variable semantics:
 //
 //	bisect go test -gcflags=all=-d=loopvarhash=PATTERN
 //
 // The -gcflags=all= instructs the go command to pass the -d=... to the Go compiler
-// when compiling all packages. Bisect replaces the literal text “PATTERN” with a specific pattern
-// on each invocation, varying the patterns to determine the minimal set of changes
+// when compiling all packages. Bisect varies PATTERN to determine the minimal set of changes
 // needed to reproduce the failure.
 //
+// The go command also checks the GOCOMPILEDEBUG environment variable for flags
+// to pass to the compiler, so the above command is equivalent to:
+//
+//	bisect GOCOMPILEDEBUG=loopvarhash=PATTERN go test
+//
+// Finally, as mentioned earlier, the -compile flag allows shortening this command further:
+//
+//	bisect -compile=loopvar go test
+//
 // # Defeating Build Caches
 //
 // Build systems cache build results, to avoid repeating the same compilations
@@ -87,6 +126,8 @@
 // previous example using Bazel, the invocation is:
 //
 //	bazel test --define=gc_goopts=-d=loopvarhash=PATTERN,unused=RANDOM //path/to:test
+//
+// [GODEBUG setting]: https://tip.golang.org/doc/godebug
 package main
 
 import (
@@ -127,6 +168,25 @@
 	flag.IntVar(&b.Count, "count", 2, "run target `n` times for each trial")
 	flag.BoolVar(&b.Verbose, "v", false, "enable verbose output")
 
+	env := ""
+	envFlag := ""
+	flag.Func("compile", "bisect source locations affected by Go compiler `rewrite` (fma, loopvar, ...)", func(value string) error {
+		if envFlag != "" {
+			return fmt.Errorf("cannot use -%s and -compile", envFlag)
+		}
+		envFlag = "compile"
+		env = "GOCOMPILEDEBUG=" + value + "hash=PATTERN"
+		return nil
+	})
+	flag.Func("godebug", "bisect call stacks affected by GODEBUG setting `name=value`", func(value string) error {
+		if envFlag != "" {
+			return fmt.Errorf("cannot use -%s and -godebug", envFlag)
+		}
+		envFlag = "godebug"
+		env = "GODEBUG=" + value + "#PATTERN"
+		return nil
+	})
+
 	flag.Usage = usage
 	flag.Parse()
 	args := flag.Args()
@@ -140,6 +200,26 @@
 		usage()
 	}
 	b.Env, b.Cmd, b.Args = args[:i], args[i], args[i+1:]
+	if env != "" {
+		b.Env = append([]string{env}, b.Env...)
+	}
+
+	// Check that PATTERN is available for us to vary.
+	found := false
+	for _, e := range b.Env {
+		if _, v, _ := strings.Cut(e, "="); strings.Contains(v, "PATTERN") {
+			found = true
+		}
+	}
+	for _, a := range b.Args {
+		if strings.Contains(a, "PATTERN") {
+			found = true
+		}
+	}
+	if !found {
+		log.Fatalf("no PATTERN in target environment or args")
+	}
+
 	if !b.Search() {
 		os.Exit(1)
 	}