| // 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 specexpr | |
| import ( | |
| "fmt" | |
| "maps" | |
| "slices" | |
| "strings" | |
| "testing" | |
| ) | |
| func newSolver(t *testing.T) *Solver { | |
| var s Solver | |
| s.SetTrace(t.Output()) | |
| return &s | |
| } | |
| func TestSolver(t *testing.T) { | |
| t.Run("constant assignment", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| s.Assign(v1, Int(10)) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{v1: Int(10)}) | |
| }) | |
| t.Run("simple dependency", func(t *testing.T) { | |
| testBinaryOp(t, 10, &BinExpr{ | |
| Op: OpTimes, | |
| X: Variable("v1"), | |
| Y: Int(2), | |
| }, Int(20)) | |
| }) | |
| t.Run("division", func(t *testing.T) { | |
| testBinaryOp(t, 16, &BinExpr{ | |
| Op: OpDiv, | |
| X: Variable("v1"), | |
| Y: Int(4), | |
| }, Int(4)) | |
| }) | |
| t.Run("cycle detection", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, Variable(v2)) | |
| s.Assign(v2, Variable(v1)) | |
| err := solverError(t, s) | |
| if !strings.Contains(err.Error(), "cyclic requirements") { | |
| t.Fatalf("expected cycle error, got %v", err) | |
| } | |
| }) | |
| t.Run("multiple assignment conflicting values", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| s.Assign(v1, Int(10)) | |
| s.Assign(v1, Int(20)) | |
| err := solverError(t, s) | |
| if !strings.Contains(err.Error(), "no solutions") { | |
| t.Fatalf("expected no solutions error, got %v", err) | |
| } | |
| }) | |
| t.Run("multiple assignment same value", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| s.Assign(v1, Int(10)) | |
| s.Assign(v1, Int(10)) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{v1: Int(10)}) | |
| }) | |
| t.Run("swidth times int", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, mkWidth(1, 2)) | |
| s.Assign(v2, &BinExpr{ | |
| Op: OpTimes, | |
| X: v1, | |
| Y: Int(4), | |
| }) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| v1: mkWidth(1, 2), | |
| v2: mkWidth(2, 1), | |
| }) | |
| }) | |
| t.Run("int times swidth", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, mkWidth(1, 2)) | |
| s.Assign(v2, &BinExpr{ | |
| Op: OpTimes, | |
| X: Int(4), | |
| Y: v1, | |
| }) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| v1: mkWidth(1, 2), | |
| v2: mkWidth(2, 1), | |
| }) | |
| }) | |
| t.Run("swidth div int", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, mkWidth(1, 2)) | |
| s.Assign(v2, &BinExpr{ | |
| Op: OpDiv, | |
| X: v1, | |
| Y: Int(2), | |
| }) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| v1: mkWidth(1, 2), | |
| v2: mkWidth(1, 4), | |
| }) | |
| }) | |
| } | |
| func testBinaryOp(t *testing.T, v1Val Int, v2Expr Expr, expectedV2Val any) { | |
| t.Helper() | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, v1Val) | |
| s.Assign(v2, v2Expr) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{v1: v1Val, v2: expectedV2Val}) | |
| } | |
| func TestComparisons(t *testing.T) { | |
| t.Run("greater than", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, Int(20)) | |
| s.Assign(v2, Int(10)) | |
| // Add comparison v1 > v2 and assert it must be true | |
| s.Assert(&BinExpr{Op: OpGreaterThan, X: v1, Y: v2}) | |
| uniqueSolution(t, s) | |
| }) | |
| t.Run("less than", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Assign(v1, Int(10)) | |
| s.Assign(v2, Int(20)) | |
| s.Assert(&BinExpr{Op: OpLessThan, X: v1, Y: v2}) | |
| uniqueSolution(t, s) | |
| }) | |
| } | |
| func TestSolveShape(t *testing.T) { | |
| t.Run("scalar Int32", func(t *testing.T) { | |
| s := newSolver(t) | |
| s.Assign("x", mustParseExpr(t, "Int32")) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "x": Basic{"int", 32}, | |
| }) | |
| }) | |
| t.Run("vector Int32x4", func(t *testing.T) { | |
| s := newSolver(t) | |
| s.Assign("x", mustParseExpr(t, "Int32x4")) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "x": Vector{Basic{"int", 32}, Int(128)}, | |
| }) | |
| }) | |
| t.Run("scalar symbolic {xB}{xN}", func(t *testing.T) { | |
| s := newSolver(t) | |
| s.Assign("x", mustParseExpr(t, "{xB}{xN}")) | |
| s.Assign("xB", &Literal{"int"}) | |
| s.Assign("xN", Int(32)) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "x": Basic{"int", 32}, | |
| "xB": "int", | |
| "xN": Int(32), | |
| }) | |
| }) | |
| vectorElem := MakeField[Vector]("Elem") | |
| basicBase := MakeField[Basic]("Base") | |
| basicBits := MakeField[Basic]("Bits") | |
| vectorWidth := MakeField[Vector]("Width") | |
| assignVector := func(s *Solver, v Variable, e Expr) { | |
| x := s.Assign(v, e) | |
| s.Assign(v+"B", basicBase.Apply(vectorElem.Apply(x))) | |
| xN := s.Assign(v+"N", basicBits.Apply(vectorElem.Apply(x))) | |
| xW := s.Assign(v+"W", vectorWidth.Apply(x)) | |
| s.Assign(v+"L", &BinExpr{Op: OpDiv, X: xW, Y: xN}) | |
| } | |
| t.Run("derived scalable vector with lane count", func(t *testing.T) { | |
| s := newSolver(t) | |
| assignVector(s, "x", mustParseExpr(t, "Int32s")) | |
| s.Assign("y", mustParseExpr(t, "{xB}{xN*2}x{xL/2}")) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "y": Vector{Basic{"int", 64}, mkWidth(1, 1)}, | |
| "xW": mkWidth(1, 1), | |
| "xL": mkWidth(1, 32), | |
| }) | |
| }) | |
| t.Run("derived scalable vector with width", func(t *testing.T) { | |
| s := newSolver(t) | |
| assignVector(s, "x", mustParseExpr(t, "Int32s")) | |
| s.Assign("y", mustParseExpr(t, "{xB}{xN*2}w{xW}")) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "y": Vector{Basic{"int", 64}, mkWidth(1, 1)}, | |
| "xW": mkWidth(1, 1), | |
| }) | |
| }) | |
| t.Run("width rounding", func(t *testing.T) { | |
| s := newSolver(t) | |
| assignVector(s, "x", mustParseExpr(t, "Int64x2")) | |
| s.Assign("y", mustParseExpr(t, "{xB}{xN/2}x{xL}")) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "y": Vector{Basic{"int", 32}, Int(128)}, | |
| }) | |
| }) | |
| t.Run("domain limits", func(t *testing.T) { | |
| s := newSolver(t) | |
| s.Declare("x", []any{1, 2}) | |
| s.Declare("y", []any{1, 2}) | |
| s.Assign("y", mustParseExpr(t, "x*2")) | |
| sol := uniqueSolution(t, s) | |
| bCheck(t, sol, map[Variable]any{ | |
| "x": Int(1), "y": Int(2), | |
| }) | |
| }) | |
| t.Run("non-scalable width", func(t *testing.T) { | |
| s := newSolver(t) | |
| assignVector(s, "x", mustParseExpr(t, "Int32s")) | |
| s.Assign("y", mustParseExpr(t, "Int16x{xL}")) | |
| err := solverError(t, s) | |
| if !strings.Contains(err.Error(), "invalid width") { | |
| t.Fatalf("expected invalid width error, got: %v", err) | |
| } | |
| }) | |
| } | |
| func TestEnumerator(t *testing.T) { | |
| t.Run("simple enumeration", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Declare(v1, []any{1, 2, 3, 4}) | |
| // Assert v2 = v1 * 2 | |
| s.Assign(v2, &BinExpr{Op: OpTimes, X: v1, Y: Int(2)}) | |
| sols := allSolutions(s) | |
| if len(sols) != 4 { | |
| t.Errorf("expected 4 solutions, got %d", len(sols)) | |
| } | |
| // Verify that each solution maps v2 to v1*2 | |
| for _, sol := range sols { | |
| m := bmap(sol) | |
| v1Val := m[v1].(Int) | |
| v2Val := m[v2].(Int) | |
| if v2Val != v1Val*2 { | |
| t.Errorf("solution %v violates v2 = v1*2", m) | |
| } | |
| } | |
| }) | |
| t.Run("comparison constraints enumeration", func(t *testing.T) { | |
| s := newSolver(t) | |
| v1 := Variable("v1") | |
| v2 := Variable("v2") | |
| s.Declare(v1, []any{1, 2, 3, 4, 5}) | |
| // v2 = v1 * 2 | |
| s.Assign(v2, &BinExpr{Op: OpTimes, X: v1, Y: Int(2)}) | |
| // v1 > 2 | |
| s.Assert(&BinExpr{Op: OpGreaterThan, X: v1, Y: Int(2)}) | |
| // v2 < 10 | |
| s.Assert(&BinExpr{Op: OpLessThan, X: v2, Y: Int(10)}) | |
| sols := allSolutions(s) | |
| // Solutions should be v1=3, v1=4. So 2 solutions | |
| if len(sols) != 2 { | |
| t.Errorf("expected 2 solutions, got %d", len(sols)) | |
| } | |
| }) | |
| } | |
| func mustParseExpr(t *testing.T, x string) Expr { | |
| t.Helper() | |
| e, err := ParseExpr(x) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| return e | |
| } | |
| func allSolutions(s *Solver) []*Bindings { | |
| var sols []*Bindings | |
| for b, err := range s.Solve() { | |
| if err != nil { | |
| panic(err) // tests expect valid execution when enumerating | |
| } | |
| sols = append(sols, b) | |
| } | |
| return sols | |
| } | |
| func uniqueSolution(t *testing.T, s *Solver) *Bindings { | |
| t.Helper() | |
| var sols []*Bindings | |
| for b, err := range s.Solve() { | |
| if err != nil { | |
| t.Fatalf("solve failed: %v", err) | |
| } | |
| sols = append(sols, b) | |
| if len(sols) >= 20 { | |
| // Stop before we go too deep | |
| t.Fatalf("expected exactly one solution, got >= 20") | |
| } | |
| } | |
| if len(sols) != 1 { | |
| t.Errorf("expected exactly one solution, got %d", len(sols)) | |
| for _, sol := range sols { | |
| t.Errorf(" %s", sol) | |
| } | |
| t.FailNow() | |
| } | |
| return sols[0] | |
| } | |
| func solverError(t *testing.T, s *Solver) error { | |
| t.Helper() | |
| for soln, err := range s.Solve() { | |
| if err != nil { | |
| return err | |
| } | |
| t.Fatalf("expected solver error, but got solution:\n%s", soln) | |
| } | |
| return fmt.Errorf("no solutions") | |
| } | |
| // bmap converts a Bindings to a map. | |
| func bmap(b *Bindings) map[Variable]any { | |
| return maps.Collect(b.All()) | |
| } | |
| // bCheck fails t if got[v] != want[v] for any keys in want. | |
| func bCheck(t *testing.T, got *Bindings, want map[Variable]any) { | |
| t.Helper() | |
| var keys []Variable | |
| for k := range want { | |
| keys = append(keys, k) | |
| } | |
| slices.Sort(keys) | |
| var mismatches []string | |
| for _, k := range keys { | |
| wantVal := want[k] | |
| gotVal := got.Get(k) | |
| if gotVal != wantVal { | |
| mismatches = append(mismatches, fmt.Sprintf(" %s: got %v (%T), want %v (%T)", k, gotVal, gotVal, wantVal, wantVal)) | |
| } | |
| } | |
| if len(mismatches) > 0 { | |
| t.Fatalf("solution mismatch:\n%s", strings.Join(mismatches, "\n")) | |
| } | |
| } |