// Copyright 2026 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 unify import ( "fmt" "math/rand/v2" "slices" "strings" "testing" ) // TestFastPaths checks that unification optimizations don't change the results // of unification by comparing unification of random inputs with and without // them. Some optimizations change the order of the results, so this ignores // the order. func TestFastPaths(t *testing.T) { n := 500 if testing.Short() { n = 100 } for seed := range uint64(n) { r := rand.New(rand.NewPCG(seed, 0)) var inputs []string for range 2 + r.IntN(2) { inputs = append(inputs, randYAML(r, 0)) } fast := unifyAllString(inputs) debugNoFastPaths = true slow := unifyAllString(inputs) debugNoFastPaths = false if fast != slow { t.Errorf("seed %d: results differ\ninputs:\n%s\nwith fast paths:\n%s\nwithout fast paths:\n%s", seed, strings.Join(inputs, "\n"), fast, slow) } } } // unifyAllString unifies inputs and returns a string containing all values of // the result, sorted, or the error or panic message. func unifyAllString(inputs []string) (res string) { defer func() { if p := recover(); p != nil { res = fmt.Sprintf("panic: %v", p) } }() var cs []Closure for i, in := range inputs { c, err := Read(strings.NewReader(in), fmt.Sprintf("input%d", i), ReadOpts{}) if err != nil { return fmt.Sprintf("error: %v", err) } cs = append(cs, c) } c, err := Unify(cs...) if err != nil { return fmt.Sprintf("error: %v", err) } const limit = 100 var vals []string for v := range c.All() { if len(vals) == limit { return fmt.Sprintf("more than %d values", limit) } vals = append(vals, fmt.Sprintf("- %s", v)) } slices.Sort(vals) return strings.Join(vals, "") } // randYAML returns a random YAML def. The values are biased toward sums, // variables, and small alphabets so that inputs are likely to partly unify. // In practice, almost all inputs unify to at least one value, and most to // several. // They follow a fixed schema so that unification rarely fails with a kind // mismatch: field f is a string, g is a def, and h is a tuple. // // Unification can create infinite values (and overflow the stack) when the // same variable appears in some alternatives of a sum and elsewhere, so this // never generates variables inside sums. func randYAML(r *rand.Rand, depth int) string { return randDef(r, depth, true) } // randSum returns either a value generated by gen or a sum of 2 or 3 values // generated by gen. It passes whether variables are allowed to gen. func randSum(r *rand.Rand, vars bool, gen func(vars bool) string) string { if r.IntN(2) == 0 { return gen(vars) } var elts []string for range 2 + r.IntN(2) { elts = append(elts, gen(false)) } return "!sum [" + strings.Join(elts, ", ") + "]" } func randDef(r *rand.Rand, depth int, vars bool) string { if vars && depth == 1 && r.IntN(8) == 0 { // Use $d at only one depth. Otherwise, it may be unified with a // value that contains it. return "$d" } return randSum(r, vars, func(vars bool) string { var fields []string if r.IntN(4) != 0 { fields = append(fields, "f: "+randString(r, vars)) } if depth < 2 && r.IntN(2) == 0 { fields = append(fields, "g: "+randDef(r, depth+1, vars)) } if depth < 2 && r.IntN(3) == 0 { fields = append(fields, "h: "+randTuple(r, vars)) } return "{" + strings.Join(fields, ", ") + "}" }) } func randTuple(r *rand.Rand, vars bool) string { return randSum(r, vars, func(vars bool) string { if r.IntN(3) == 0 { // Named variables in !repeat elements don't work. return "!repeat [" + randString(r, false) + "]" } var elts []string for range 1 + r.IntN(2) { elts = append(elts, randString(r, vars)) } return "[" + strings.Join(elts, ", ") + "]" }) } func randString(r *rand.Rand, vars bool) string { if vars && r.IntN(3) == 0 { return "$" + []string{"x", "y"}[r.IntN(2)] } return randSum(r, vars, func(bool) string { switch r.IntN(6) { case 0: return "_" case 1: return `"a|b"` } return []string{"a", "b", "c"}[r.IntN(3)] }) }