// 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 manipulates symbolic constraints on vector shapes. // // # Shapes // // A vector shape consists of a base type B (e.g., int, float), element width N // (8, 16, 32, or 64), and a vector width W (128, 256, 512, or scalable). It // also has a lane count L, which is the vector width / element width. These are // written like "Int32x4" or "Int32w128" or, for a scalable vector, "Int32s" // Masks are represented as vectors with a base type of "Mask", e.g., // "Mask32x4". // // Scalar shapes consist only of a base type and an element width, e.g., // "uint32". // // # Expressions // // Constraints are written as boolean expressions over shapes and a few basic // types. // // The primary expressions are: // // - Variable ([a-zA-Z]+), such as x or xL // // - Integers ([0-9]+) // // - Shapes, written in the form given above, but where each component can be // written as a bracketed expression, such as "Int32x{z/2}". // // These can be combined with operators * or /, or comparison operators =, >, <, // >=, <=. Arithmetic operators bind more tightly than comparison operators. // // # Example // // Consider a DotProductPairs function that takes two vectors x and y that have // the same shape and produces a vector z that has the same base type as x and // y, but has half as many elements, each of double the width. This can be // expressed as: // // y=x // z={xB}{xN*2}x{xL/2} // // # Width rounding // // The minimum vector width is 128 bits. Sometimes, operations would naturally // produce a width smaller than this, so hardware simply pads the vector out to // 128 bits. Shapes implement this behavior. For example, consider a "convert to // float32 operation" with constraints // // z=Float32x{xL} // // If x is Float64x2, then z would naturally be Float32x2, but since this is // only 64 bits, the shape is "rounded" up to Float32x4. // // # Limitations // // The solver is intentionally simple. See [Solver] for a description of its // limitations. If you run up against its limitations, you're probably being too // clever. package specexpr import ( "cmp" "container/heap" "fmt" "io" "iter" "log" "maps" "slices" "strings" ) // A Solver solves a set of constraints. // // This is a simple monotonic solver. It looks for a single order in which it // can resolve all constraints, assertions of the form "var=expr" are treated as // candidates for resolving the value of "var", and anything else is treated as // a boolean check. Variables that appear only on the right hand side of // assignments are "independent" and it will enumerate all possible values of // these variables. It never tries to invert any formulas and refuses to solve a // system with cycles. This is intentional to keep this solver fast: if your // formulas are cyclic, you're doing something too complicated. type Solver struct { vars map[Variable][]any asserts []Expr tracer *tracer } // SetTrace enables emitting a solver trace to w. func (s *Solver) SetTrace(w io.Writer) { if w == nil { s.tracer = nil } else { s.tracer = &tracer{w: w} } } // Declare declares a variable and its domain. // // Any "int" values in domain will be converted to [Int]. func (s *Solver) Declare(v Variable, domain []any) { if s.vars == nil { s.vars = make(map[Variable][]any) } if _, ok := s.vars[v]; ok { panic(v + " redeclared") } for i, d := range domain { if d, ok := d.(int); ok { domain[i] = Int(d) } } s.vars[v] = domain } // Assign is a convenience for asserting that v=val. func (s *Solver) Assign(v Variable, val Expr) Variable { s.Assert(&BinExpr{Op: OpEqual, X: v, Y: val}) return v } // Assert asserts a boolean condition must be true. func (s *Solver) Assert(cond Expr) { s.asserts = append(s.asserts, cond) } func (s *Solver) Fprint(w io.Writer) { for _, v := range slices.Sorted(maps.Keys(s.vars)) { fmt.Fprintf(w, "%s in %v\n", v, s.vars[v]) } for _, expr := range s.asserts { fmt.Fprintf(w, "%s\n", expr) } } // Bindings is a set of variable values. type Bindings struct { varNames map[Variable]int // Shared between all solutions vals []any } // Get returns the value of v if resolved, or nil. func (b *Bindings) Get(v Variable) any { vid, ok := b.varNames[v] if !ok || vid >= len(b.vals) { return nil } return b.vals[vid] } // All yields all variable bindings. func (b *Bindings) All() iter.Seq2[Variable, any] { return func(yield func(Variable, any) bool) { for _, varName := range slices.Sorted(maps.Keys(b.varNames)) { vid := b.varNames[varName] if vid < len(b.vals) && !yield(varName, b.vals[vid]) { return } } } } func (b *Bindings) String() string { var buf strings.Builder buf.WriteByte('{') for v, val := range b.All() { if buf.Len() > 1 { buf.WriteByte(' ') } fmt.Fprintf(&buf, "%s=%v", v, val) } buf.WriteByte('}') return buf.String() } // Solve yields all satisfying assignments of the variables in s. func (s *Solver) Solve() iter.Seq2[*Bindings, error] { steps, err := s.topoSort() if err != nil { return func(yield func(*Bindings, error) bool) { yield(nil, err) } } // Assign variable indexes varIDs := make(map[Variable]int) for _, step := range steps { if step.kind == solverStepCheck { continue } if _, ok := varIDs[step.bind]; ok { panic(fmt.Sprintf("variable %s resolved multiple times by solver sequence", step.bind)) } varIDs[step.bind] = len(varIDs) } // If there are no solutions, then we report any evaluation errors. As soon // as we yield anything, we set this to nil to indicate that. errors := make(map[string]bool) addErr := func(err error) { if errors != nil { errors[err.Error()] = true } } // Walk solver steps b := Bindings{ varNames: varIDs, vals: make([]any, 0, len(varIDs)), } pop := func() { b.vals = b.vals[:len(b.vals)-1] } var visit func(steps []*solverStep, yield func(*Bindings, error) bool) bool visit = func(steps []*solverStep, yield func(*Bindings, error) bool) bool { if len(steps) == 0 { errors = nil // Discard any errors // Snapshot Bindings. s.tracer.sat() return yield(&Bindings{varNames: b.varNames, vals: slices.Clone(b.vals)}, nil) } step := steps[0] steps = steps[1:] switch step.kind { case solverStepAssign: val, err := step.expr.(*BinExpr).Y.eval(&b) if err == nil { if domain, ok := s.vars[step.bind]; ok && !slices.Contains(domain, val) { err = fmt.Errorf("cannot assign %s=%v: not in domain", step.bind, val) } } s.tracer.assign(step.expr, step.bind, val, err) if err != nil { addErr(err) return true } b.vals = append(b.vals, val) defer pop() return visit(steps, yield) case solverStepCheck: val, err := step.expr.eval(&b) s.tracer.check(step.expr, val, err) if err != nil { addErr(err) return true } vBool, ok := val.(bool) if !ok { panic(fmt.Errorf("%s has type %T, expected bool", step.expr, val)) } if vBool { return visit(steps, yield) } return true case solverStepIndep: i := len(b.vals) b.vals = append(b.vals, nil) defer pop() for _, val := range s.vars[step.bind] { b.vals[i] = val s.tracer.enter(step.bind, val) if !visit(steps, yield) { return false } s.tracer.exit() } return true } panic("bad step kind") } return func(yield func(*Bindings, error) bool) { if visit(steps, yield) { if len(errors) > 0 { err := fmt.Errorf("%s", strings.Join(slices.Sorted(maps.Keys(errors)), "\n")) yield(nil, err) } } } } type solverStep struct { kind solverStepKind id int expr Expr bind Variable // Variable to bind for solverStepAssign or solverStepIndep hid int // heap index } type solverStepKind int const ( // solverStepIndep is a solverStep that simultaneously binds // [solverStep.bind] to every possible value in bind's domain. solverStepIndep solverStepKind = iota // solverStepAssign is a solverStep where expr is an [OpEqual] [BinExpr] // where the LHS is a Variable. It evaluates the RHS and assigns it to the // variable. Variable must not have been bound by an earlier step (any // subsequent OpEqual expressions for this variable should instead be a // solverStepCheck). solverStepAssign // solverStepCheck is a solverStep that checks that expr is true and // otherwise terminates the current solver branch. solverStepCheck ) func (s *solverStep) Compare(t *solverStep) int { // Put assertions before independent variables because they may cut off paths // before we have to enumerate values. if s.kind != t.kind { return cmp.Compare(s.kind, t.kind) } switch s.kind { case solverStepCheck, solverStepAssign: return cmp.Compare(s.id, t.id) case solverStepIndep: return cmp.Compare(s.bind, t.bind) } panic("bad solverStep kind") } type solverHeap []*solverStep func (sh solverHeap) Len() int { return len(sh) } func (sh solverHeap) Less(i, j int) bool { return sh[i].Compare(sh[j]) < 0 } func (sh solverHeap) Swap(i, j int) { sh[i], sh[j] = sh[j], sh[i] sh[i].hid, sh[j].hid = i, j } func (sh *solverHeap) Push(x any) { item := x.(*solverStep) item.hid = len(*sh) *sh = append(*sh, item) } func (sh *solverHeap) Pop() any { old := *sh n := len(old) item := old[n-1] old[n-1] = nil item.hid = -1 *sh = old[0 : n-1] return item } func (s *Solver) topoSort() (order []*solverStep, err error) { // This is a topo-sort with a few tricks: an assertion can be evaluated once // all of its input variables are available, BUT a variable value can be // resolved by potentially more than one assertion. Hence, we have a mix of // "AND" and "OR" dependencies. For example, if we have: // // x=Basic{xB, xN} // x=y // // Then x depends on "Basic{xB, xN}" OR "y" because we can assign x's value // as soon as we resolve either of these. But resolving the first of these // two expressions depends on xB AND xN. // // To handle this mix of "AND" and "OR" dependencies, we use a // wavefront-style topo sort where we track the number of unresolved input // variables to each assertion and whenever we resolve one of these inputs // for the first time, we decrement that count. Once it reaches zero, that // assertion is let out of the gate. // // The second trick is that in the set of possible next steps, we bias // toward taking steps that are more likely to cut off a path and less // likely to cause more fan-out. var queue solverHeap defs := make(map[Variable][]*solverStep) uses := make(map[Variable]map[*solverStep]bool) remaining := make(map[*solverStep]int) isVarAssign := func(e Expr) (Variable, Expr, bool) { switch e := e.(type) { case *BinExpr: if e.Op == OpEqual { switch x := e.X.(type) { case Variable: return x, e.Y, true } } } return "", nil, false } for i, assert := range s.asserts { var rhs Expr // Wrap the assertion in a step, assigning an ID. var step *solverStep if def, val, ok := isVarAssign(assert); ok { if def == val { // x=x assertion. It's always true and will muck up the sorting, // so throw it out. continue } // Variable assignment step = &solverStep{kind: solverStepAssign, id: i, expr: assert, hid: -1, bind: def} // Record variable definition. defs[def] = append(defs[def], step) rhs = val } else { // Boolean check step = &solverStep{kind: solverStepCheck, id: i, expr: assert, hid: -1} rhs = assert } // Record variables this step depends on. deps := 0 for use := range exprVars(rhs) { if uses[use] == nil { uses[use] = make(map[*solverStep]bool) } if !uses[use][step] { uses[use][step] = true deps++ } } if deps == 0 { // Already solvable, enqueue it. step.hid = len(queue) queue = append(queue, step) } else { remaining[step] = deps } } // Find the independent variables and also seed the frontier with them. for v := range uses { if defs[v] == nil { if _, ok := s.vars[v]; !ok { return nil, fmt.Errorf("no domain for independent variable %q", v) } queue = append(queue, &solverStep{kind: solverStepIndep, bind: v, hid: -1}) } } // Any variables that are declared but not referenced are also independent. for v := range s.vars { if uses[v] == nil && defs[v] == nil { queue = append(queue, &solverStep{kind: solverStepIndep, bind: v, hid: -1}) } } // Drive the frontier. queue is the frontier of variables whose dependencies // are all resolved, maintained in a heuristic order. heap.Init(&queue) for len(queue) > 0 { step := queue[0] heap.Pop(&queue) order = append(order, step) if step.kind == solverStepCheck { continue } // This step resolved variable v. v := step.bind // Demote any other assignments of this variable to checks. for _, def := range defs[v] { if def != step { def.kind = solverStepCheck // Adjust heap if def.hid != -1 { heap.Fix(&queue, def.hid) } } } delete(defs, v) // Check steps that depend on v. for use := range uses[v] { remaining[use]-- if remaining[use] == 0 { // All variables used by this step are now resolved. Add it to the // queue. heap.Push(&queue, use) delete(remaining, use) } } } if len(remaining) > 0 { return nil, reportCycle(defs, remaining) } return order, nil } func reportCycle(defs map[Variable][]*solverStep, remaining map[*solverStep]int) error { // There was a cycle. There could be more than one cycle, but report one. // Start with the "minimum" remaining step for stability. var step *solverStep for rem := range remaining { if rem.kind == solverStepAssign && (step == nil || rem.Compare(step) < 0) { step = rem } } // Walk forward through the graph, filtered to unresolved nodes. var cycle []Expr have := make(map[*solverStep]int) var visit func(step *solverStep) error visit = func(step *solverStep) error { if step.kind != solverStepAssign || remaining[step] == 0 { return nil } if i, ok := have[step]; ok { // Found the cycle return fmt.Errorf("cyclic requirements: %v", cycle[i:]) } have[step] = len(cycle) cycle = append(cycle, step.expr) for v := range exprVars(step.expr.(*BinExpr).Y) { for _, def := range defs[v] { if err := visit(def); err != nil { return err } } } delete(have, step) cycle = cycle[:len(cycle)-1] return nil } err := visit(step) if err == nil { log.Printf("remaining:") for rem, count := range remaining { log.Printf(" %v (%d)", rem, count) } log.Fatal("unresolved assertions, but failed to find a cycle") } return err }