experiment: add simple way to toggle experimental features
diff --git a/experiment/experiment.go b/experiment/experiment.go
new file mode 100644
index 0000000..1e57f8d
--- /dev/null
+++ b/experiment/experiment.go
@@ -0,0 +1,108 @@
+// Package experiment provides support for experimental features.
+package experiment
+
+import (
+ "fmt"
+ "io"
+ "strings"
+ "sync"
+)
+
+// Experiment represents an experimental feature.
+// Experiments are global.
+type Experiment struct {
+ Name string // The name of the experiment used in -x flag
+ Description string // A short description of what the experiment does
+ Enabled bool // Whether the experiment is enabled
+}
+
+var (
+ mu sync.Mutex
+ experiments = []Experiment{
+ {
+ Name: "list",
+ Description: "List all available experiments and exit",
+ },
+ {
+ Name: "all",
+ Description: "Enable all experiments",
+ },
+ }
+ byName = map[string]*Experiment{}
+)
+
+func Enabled(name string) bool {
+ mu.Lock()
+ defer mu.Unlock()
+ return byName[name].Enabled
+}
+
+func init() {
+ for _, e := range experiments {
+ byName[e.Name] = &e
+ }
+}
+
+func (e Experiment) String() string {
+ return fmt.Sprintf("\t%-15s %s\n", e.Name, e.Description)
+}
+
+// Fprint writes a list of all available experiments to w.
+func Fprint(w io.Writer) {
+ mu.Lock()
+ defer mu.Unlock()
+
+ fmt.Fprintln(w, "Available experiments:")
+ for _, e := range experiments {
+ fmt.Fprintln(w, e)
+ }
+}
+
+// Flag is a custom flag type that allows for comma-separated
+// values and can be used multiple times.
+type Flag struct {
+ Value string
+}
+
+// String returns the string representation of the flag value.
+func (f *Flag) String() string {
+ return f.Value
+}
+
+// Set adds a value to the flag.
+func (f *Flag) Set(value string) error {
+ f.Value = f.Value + "," + value // quadratic, doesn't matter, tiny N
+ return nil
+}
+
+// Get returns the flag values.
+func (f *Flag) Get() any {
+ return f.Value
+}
+
+// Process handles all flag values, enabling the appropriate experiments.
+func (f *Flag) Process() error {
+ mu.Lock()
+ defer mu.Unlock()
+
+ for name := range strings.SplitSeq(f.Value, ",") {
+ name = strings.TrimSpace(name)
+ if name == "" {
+ continue
+ }
+ e, ok := byName[name]
+ if !ok {
+ return fmt.Errorf("unknown experiment: %q", name)
+ }
+ e.Enabled = true
+ }
+ if byName["all"].Enabled {
+ for _, e := range experiments {
+ if e.Name == "list" {
+ continue
+ }
+ e.Enabled = true
+ }
+ }
+ return nil
+}