Austin Clements would like David Chase and Junyang Shao to review this change.
simd/internal/spec: initial source-oriented SIMD spec
This is the beginning of an executable spec for the SIMD APIs. The
intent is that this will act as the input to all other SIMD
generators, forming the single source of truth for the meaning of all
exported APIs.
This is meant to replace simdgen's categories.yaml files. Those were
originally intended as the single source of truth for all APIs, but
fell short in several ways:
- categories.yaml doesn't describe type signatures or actual operation
behavior. The spec package addresses this by providing a full Go
implementation of operations.
- Nothing forces categories.yaml to include all of the information
it's "supposed to" or prevents platform-specified details from
leaking into it. It's just an input to the unifier and the unifier
doesn't know anything about anything. On the other hand, the spec
package is directly tailored to this use.
- Not all generation is done by simdgen (and thus the unifier), and
other generators can't make use of categories.yaml. The spec package
is designed to be self-contained and easy to use from any generator.
diff --git a/src/go/build/deps_test.go b/src/go/build/deps_test.go
index 4959a42..5710265 100644
--- a/src/go/build/deps_test.go
+++ b/src/go/build/deps_test.go
@@ -179,6 +179,9 @@
MATH
< runtime/metrics;
+ MATH
+ < simd/internal/spec;
+
MATH, unicode/utf8
< strconv;
diff --git a/src/simd/internal/spec/TASKS.md b/src/simd/internal/spec/TASKS.md
new file mode 100644
index 0000000..37c3fb7
--- /dev/null
+++ b/src/simd/internal/spec/TASKS.md
@@ -0,0 +1,31 @@
+This spec is quite incomplete! The plan is to migrate everything from simdgen's
+categories.yaml files into the spec and replace categories.yaml. As we're doing
+so, we should think carefully about:
+
+1. The doc comment for each operation. Let's start moving toward a more formal
+ style for mathematically specified operations.
+
+2. How every operation generalizes across types, sizes, and especially to
+ scalable vectors. The spec is meant to be *maximalist*, circumscribing what
+ every architecture can do.
+
+3. In what cases operations are architecture-dependent.
+
+Tasks
+- [ ] Hook specgen into simdgen, probably unioning the categories.yaml input and
+ the specgen input while we transition
+- [ ] Migrate categories.yaml to spec
+- [ ] Figure out how to represent compiler-only (non-API) simdgen operations
+- [ ] Delete categories.yaml
+- [ ] Incorporate specgen into other generators, so the spec becomes the sole
+ source of truth for all exported APIs.
+ - [ ] tmplgen
+ - [ ] wasmgen
+- [ ] Build a tool to ensure hand-written APIs (emulations, etc) match the spec.
+ Maybe the hand-written code still lives directly in archsimd, but as
+ unexported function, and the generator writes the trivial exported API glue
+ for these.
+- [ ] Generate a full reference implementation for testing that provides the
+ SIMD API but just wraps the spec package.
+- [ ] Generate conformance tests of the archsimd API against the spec testing
+ layer.
diff --git a/src/simd/internal/spec/basic_test.go b/src/simd/internal/spec/basic_test.go
new file mode 100644
index 0000000..cf620c2
--- /dev/null
+++ b/src/simd/internal/spec/basic_test.go
@@ -0,0 +1,70 @@
+// Copyright 2025 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 spec
+
+import (
+ "fmt"
+ "slices"
+ "testing"
+)
+
+func vecOf[E EltOrMask, W Width](xs ...E) Vec[E, W] {
+ l := lanes[E, W]()
+ if len(xs) != l {
+ panic(fmt.Sprintf("got %d elements, want %d", len(xs), l))
+ }
+ return xs
+}
+
+func TestPreserveTNxL(t *testing.T) {
+ x := vecOf[int32, Width128](1, 2, 3, 4)
+ y := vecOf[int32, Width128](2, 3, 4, 5)
+ want := vecOf[int32, Width128](3, 5, 7, 9)
+ z := Add(x, y)
+ if !slices.Equal(z, want) {
+ t.Fatalf("got %v, want %v", z, want)
+ }
+}
+
+func TestPreserveL(t *testing.T) {
+ // This operation changes T and N
+ x := vecOf[int64, Width256](1, 2, 3, 4)
+ want := vecOf[float32, Width128](1, 2, 3, 4)
+ z := ConvertToZ[int64, Width256, float32, Width128](x)
+ if !slices.Equal(z, want) {
+ t.Fatalf("got %v, want %v", z, want)
+ }
+}
+
+func TestPreserveNxL(t *testing.T) {
+ // This operation changes T
+ x := vecOf[int32, Width128](1, 2, 3, 4)
+ want := vecOf[float32, Width128](1, 2, 3, 4)
+ z := ConvertToZ[int32, Width128, float32, Width128](x)
+ if !slices.Equal(z, want) {
+ t.Fatalf("got %v, want %v", z, want)
+ }
+}
+
+func TestWidthRounding(t *testing.T) {
+ // The "natural" result of this is only 64 bits, so it gets rounded up to
+ // 128 bits.
+ x := vecOf[int64, Width128](1, 2)
+ want := vecOf[float32, Width128](1, 2, 0, 0)
+ z := ConvertToZ[int64, Width128, float32, Width128](x)
+ if !slices.Equal(z, want) {
+ t.Fatalf("got %v, want %v", z, want)
+ }
+}
+
+func TestPreserveW(t *testing.T) {
+ x := vecOf[int32, Width128](1, 2, 3, 4)
+ y := vecOf[int32, Width128](2, 3, 4, 5)
+ want := vecOf[int64, Width128](1*2+2*3, 3*4+4*5)
+ z := DotProductPairs[int32, Width128, int64](x, y)
+ if !slices.Equal(z, want) {
+ t.Fatalf("got %v, want %v", z, want)
+ }
+}
diff --git a/src/simd/internal/spec/combinators.go b/src/simd/internal/spec/combinators.go
new file mode 100644
index 0000000..13372eb
--- /dev/null
+++ b/src/simd/internal/spec/combinators.go
@@ -0,0 +1,28 @@
+// 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 spec
+
+// map1 returns the vector z[i] = f(x[i]).
+//
+// z may have more lanes than x, in which case they will be 0.
+func map1[From Elt, FromW Width, To Elt, ToW Width](x Vec[From, FromW], f func(x From) To) (z Vec[To, ToW]) {
+ z = makeVec[To, ToW]()
+ // Loop over x, not z, because width rounding may have expanded z.
+ for i, xi := range x {
+ z[i] = f(xi)
+ }
+ return z
+}
+
+// map2 returns the vector z[i] = f(x[i], y[i]).
+//
+// z may have more lanes than x, in which case they will be 0.
+func map2[From Elt, FromW Width, To Elt, ToW Width](x, y Vec[From, FromW], f func(x, y From) To) (z Vec[To, ToW]) {
+ z = makeVec[To, ToW]()
+ for i := range x {
+ z[i] = f(x[i], y[i])
+ }
+ return z
+}
diff --git a/src/simd/internal/spec/converts.go b/src/simd/internal/spec/converts.go
new file mode 100644
index 0000000..476cf1b
--- /dev/null
+++ b/src/simd/internal/spec/converts.go
@@ -0,0 +1,43 @@
+// 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 spec
+
+// ConvertToZ converts element values to {zE}. The result has the same number of lanes.
+//
+//specgen:name ConvertTo{zE}
+//specgen:require zL=xL zE!=xE
+func ConvertToZ[xE Nums, xW Width, zE Nums, zW Width](x Vec[xE, xW]) (z Vec[zE, zW]) {
+ // Architectures are generally significantly more constrained in what they
+ // will convert between, but this operation describes the full universe of
+ // possible conversions.
+ return map1[xE, xW, zE, zW](x, func(x xE) zE { return zE(x) })
+}
+
+// TODO: ExtendLo* and ConvertLo* don't work for scalable conversions because we
+// put a literal zL in the name. We could instead just call them XLoToZ and drop
+// the number since it's fully implied by the receiver type and the target type.
+// Note that it's not necessarily the low *half*; for example,
+// Uint8x16.ExtendLo2ToUint64 is the low eighth, but that's implied by going
+// from uint8 to uint64 without changing the width.
+
+// ExtendLoLToZ extends the lowest {zL} vector elements to {zE}.
+//
+//specgen:name ExtendLo{zL}To{zE}
+//specgen:require zB=xB zN>xN
+func ExtendLoLToZ[E Ints | Uints, W FixedWidth, zE Ints | Uints](x Vec[E, W]) (z Vec[zE, W]) {
+ z = makeVec[zE, W]()
+ for i := range z {
+ z[i] = zE(x[i])
+ }
+ return z
+}
+
+// ConvertLoLToZ converts the low-indexed {zL} elements of x to {zE}.
+//
+//specgen:name ConvertLo{zL}To{zE}
+//specgen:require zL<xL
+func ConvertLoLToZ[E Nums, W FixedWidth, zE Floats](x Vec[E, W]) (z Vec[zE, W]) {
+ panic("not implemented")
+}
diff --git a/src/simd/internal/spec/doc.go b/src/simd/internal/spec/doc.go
new file mode 100644
index 0000000..35b8675
--- /dev/null
+++ b/src/simd/internal/spec/doc.go
@@ -0,0 +1,145 @@
+// 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 spec describes all possible operations in the SIMD API.
+//
+// The SIMD spec describes the function and method signatures, documentation
+// comments, and behavior (written as a reference Go implementation) of all
+// possible Go SIMD APIs. The archsimd and simd packages are subsets of this
+// specified API. This approach enforces that "one name means one thing" across
+// all platforms and both packages.
+//
+// The spec is written as buildable and executable Go code, but isn't meant to
+// be called directly. Rather, the specgen package interprets this spec package
+// into a full API description, which can then be fed into other generators for
+// the actual SIMD packages. The executable part of the spec serves to precisely
+// specify the semantics of operations, and is intended for conformance testing.
+//
+// To see the spec-generated API and debug issues with it, use [cmd/specls].
+//
+// ## Basic operation specifications
+//
+// Spec operations are written in a stylized form that makes heavy use of type
+// parameters so a single function can describe an operation generalized across
+// many vector types. This is in contrast with the public SIMD API, where every
+// function and method operates on concrete types. The specgen generator bridges
+// this gap, instantiating a single parameterized spec function into many
+// concrete methods.
+//
+// Consider a simple example, the Add operation:
+//
+// // Add adds corresponding elements of two vectors.
+// func Add[E Nums, W Width](x, y Vec[E, W]) (z Vec[E, W]) {
+// ...
+// }
+//
+// All spec operations are written as functions, but if the first parameter has
+// type Vec, then they specify a method of a vector type. Since Add's first
+// parameter (x) is a Vec, this describes a method on vector types.
+//
+// The "E Nums" type parameter controls the allowed element types of the three
+// vector types. Here, it can be any numeric type of any size (uint8, float64,
+// etc). The "W Width" type parameter controls the total bit width of the three
+// vector types. The types that implement Width stand in for 128, 256, or 512
+// bits, or "scalable", which can represent any power of two >= 128. The number
+// of lanes of a vector is derived from the element size and the total vector
+// width.
+//
+// The Add spec expands to all possible types that satisfy the E and W type
+// parameters, which are in turn translated to types in the public API:
+//
+// func (Int8x16) Add(Int8x16) Int8x16
+// func (Int8x32) Add(Int8x32) Int8x32
+// func (Int8x64) Add(Int8x64) Int8x64
+// func (Int8s) Add(Int8s) Int8s
+// ...
+// func (Float64x8) Add(Float64x8) Float64x8
+// func (Float64s) Add(Float64s) Float64s
+//
+// ## Spec constraints
+//
+// For many operations, all possible combinations of their type parameters are
+// valid, but some need to express constraints between type parameters that
+// can't easily be described in the Go type system. For these, we support a
+// `//specgen:requires` directive. Consider DotProductPairs:
+//
+// // DotProductPairs computes the dot product of x and y.
+// //
+// //specgen:require z={xB}{xN*2}x{xL/2}
+// func DotProductPairs[E Nums, W Width, zE Nums](x, y Vec[E, W]) (z Vec[zE, W]) {
+// ...
+// }
+//
+// The require expression refers to the types of each parameter and result by
+// name. For Vec (and Array) arguments, each parameter and result also get
+// several related variables:
+//
+// v = The whole vector type (e.g., Uint32x8)
+// vE = The element type (e.g., uint32)
+// vB = The base type (e.g., uint)
+// vN = The base type size (e.g., 32)
+// vL = The number of lanes in the vector or elements in the array (e.g., 8)
+// vW = The total bit width of the vector or array (e.g., 256)
+//
+// The full syntax for constraints is described in the [specgen/specexpr]
+// package, but they often describe vector shapes like those in the
+// DotProductPairs example. The form of these is:
+//
+// - BaseNxL, which describes a vector with L elements of type BaseN;
+// - BaseNwW, which describes a vector of total width W; or
+// - BaseNs, which describes a scalable vector of BaseN elements.
+//
+// Base, N, L, or W can be either a literal or an expression in {}'s. For
+// example, Uint32x{vL} or {zB}{zN}w128.
+//
+// For DotProductPairs, the constraint "z={vB}{vN*2}x{vL/2}" says the result z
+// must have the same base type as v, but z's element type must be twice as
+// wide, and z must have half the number of lanes of v.
+//
+// The DotProductPairs constraint could also have been written in any of the
+// following equivalent ways:
+//
+// z={vB}{vN*2}w{vW} Constrain the total vector width
+// zB=vB zN=vN*2 zL=vL/2 Constrain each component separately
+// zE={vB}{vN*2} zW=vW Constrain the element type and width separately
+//
+// ## Name and doc templates
+//
+// For some operations, the API name or documentation depends on type
+// parameters. For these we support a simple template system where constraint
+// variables can be referenced in curly braces, like {vE}, similar to {}
+// expressions in shape constraints. For doc comments, these can be included
+// directly in the doc comment. For names, we use a `//specgen:name` directive,
+// such as
+//
+// //specgen:name Load{z}
+// func LoadZ[E Elt, W Width](s []E) (z Vec[E, W]) {
+//
+// In this case, the spec function itself can be named anything (as long as it's
+// exported), and the API name is generated from the directive. For example,
+// when LoadZ is instantiated on uint32 and Width128, the API name generated
+// from the template will be LoadUint32x4. This is particularly useful for
+// constructor functions and conversion functions where types must appear in the
+// name, such as LoadZ.
+//
+// ## The spec type system
+//
+// This package defines a set of types that translate to API types. We saw the
+// [Vec] type above, which translates to a concrete BNxL (or BNs) vector type in
+// the API, where L is determined from E and W.
+//
+// Masks are also represented using the [Vec] type, but with an element type
+// from the [MaskElt] interface, such as Mask8, Mask16, etc. The generator
+// translates these to Mask types in the API. Internal to the spec package,
+// these are like a wide mask, where only 0 and ^0 are legal values for these
+// elements.
+//
+// Similar to [Vec], there an [Array[E,W]] type that translates into a [L]E Go
+// array type in the API.
+//
+// The type [UintN] stands for a uint type whose bit width is determined by spec
+// constraints.
+//
+// Pointer and slice types translate directly to the API.
+package spec
diff --git a/src/simd/internal/spec/loadstore.go b/src/simd/internal/spec/loadstore.go
new file mode 100644
index 0000000..5314370
--- /dev/null
+++ b/src/simd/internal/spec/loadstore.go
@@ -0,0 +1,87 @@
+// 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 spec
+
+// Most of these are top-level functions, not methods, because their first
+// argument is not a Vec.
+
+// BroadcastZ returns a vector with the input x assigned to all elements of the
+// result.
+//
+//specgen:name Broadcast{z}
+func BroadcastZ[E Elt, W Width](x E) (z Vec[E, W]) {
+ z = makeVec[E, W]()
+ for i := range z {
+ z[i] = x
+ }
+ return z
+}
+
+// LoadZ loads a slice into a vector. If len(s) is less than the number of
+// elements in the vector, it panics.
+//
+//specgen:name Load{z}
+func LoadZ[E Elt, W Width](s []E) (z Vec[E, W]) {
+ z = makeVec[E, W]()
+ _ = s[:z.len()]
+ copy(z, s)
+ return z
+}
+
+// LoadZArray loads an array into a vector.
+//
+//specgen:name Load{z}Array
+func LoadZArray[E Elt, W FixedWidth](x *Array[E, W]) (z Vec[E, W]) {
+ z = makeVec[E, W]()
+ if len(*x) != z.len() {
+ panic("bad array size")
+ }
+ copy(z, *x)
+ return z
+}
+
+// LoadZPart loads a slice into a vector and returns the vector and the number
+// of elements loaded from s. If len(s) is less than the number of elements in
+// the vector, the remaining vector elements will be zero-filled.
+//
+//specgen:name Load{z}Part
+func LoadZPart[E Elt, W Width](s []E) (z Vec[E, W], n int) {
+ z = makeVec[E, W]()
+ n = copy(z, s)
+ return z, n
+}
+
+// Store stores the elements of x into a slice. If len(s) is less than x.Len(),
+// it panics.
+func Store[E Elt, W Width](x Vec[E, W], s []E) {
+ _ = s[:x.len()]
+ copy(s, x)
+}
+
+// StoreArray stores the elements of x to an array.
+func StoreArray[E Elt, W FixedWidth](x Vec[E, W], y *Array[E, W]) {
+ if x.len() != len(*y) {
+ panic("bad array size")
+ }
+ copy(*y, x)
+}
+
+// StoreArrayMasked stores the masked elements of x to an array. It does not
+// modify elements of y that are false in the mask.
+//
+//specgen:require maskN=xN
+func StoreArrayMasked[E Elt, W FixedWidth, mE MaskElt](x Vec[E, W], y *Array[E, W], mask Vec[mE, W]) {
+ for i, elt := range x {
+ if mask[i] != 0 {
+ (*y)[i] = elt
+ }
+ }
+}
+
+// StorePart stores at most len(s) elements of x into s and returns the number
+// of elements stored.
+func StorePart[E Elt, W Width](x Vec[E, W], s []E) int {
+ return copy(s, x)
+}
diff --git a/src/simd/internal/spec/masks.go b/src/simd/internal/spec/masks.go
new file mode 100644
index 0000000..7eb4e8b
--- /dev/null
+++ b/src/simd/internal/spec/masks.go
@@ -0,0 +1,60 @@
+// 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 spec
+
+// UintN represents a Go uintN type. An argument x of type UintN introduces a
+// constraint variable named xN that must be resolved to the bit width (8, 16,
+// 32, or 64). Widths smaller than 8 are rounded up to 8. Widths larger than 64
+// are not allowed.
+//
+// This is used by mask operations that convert between bits in a uintN type and
+// elements in a mask.
+//
+// This type is known to specgen.
+type UintN uint64
+
+// MaskFromBits constructs a mask from a bitmap value. If bit i of y is set,
+// then mask element i of the result is set.
+//
+//specgen:name {z}FromBits
+//specgen:require x=uint{zL}
+func MaskFromBits[E MaskElt, W FixedWidth](x UintN) (z Vec[E, W]) {
+ z = makeVec[E, W]()
+ for i := range z {
+ if x&(1<<i) != 0 {
+ z[i] = 1
+ }
+ }
+ return z
+}
+
+// MaskToBits constructs a bitmap from mask x, where bit i is set if mask
+// element i is set.
+//
+//specgen:name ToBits
+//specgen:require z=uint{xL}
+func MaskToBits[E MaskElt, W FixedWidth](x Vec[E, W]) (z UintN) {
+ for i, elt := range x {
+ if elt != 0 {
+ z |= 1 << i
+ }
+ }
+ return z
+}
+
+// MaskToZ converts the mask to a vector, where element i is set to ^0 (all bits
+// set, e.g., -1) if mask element i is "true".
+//
+//specgen:name To{z}
+//specgen:require z=Int{xN}x{xL}
+func MaskToZ[E MaskElt, W Width, zE Ints](x Vec[E, W]) (z Vec[zE, W]) {
+ z = makeVec[zE, W]()
+ for i, val := range x {
+ if val != 0 {
+ z[i] = ^0
+ }
+ }
+ return z
+}
diff --git a/src/simd/internal/spec/math.go b/src/simd/internal/spec/math.go
new file mode 100644
index 0000000..a0b6970
--- /dev/null
+++ b/src/simd/internal/spec/math.go
@@ -0,0 +1,58 @@
+// 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 spec
+
+// Add adds corresponding elements of two vectors.
+//
+// z[i] = x[i] + y[i]
+func Add[E Nums, W Width](x, y Vec[E, W]) (z Vec[E, W]) {
+ return map2[E, W, E, W](x, y, func(x, y E) E { return x + y })
+}
+
+// DotProductPairs multiplies corresponding elements of x and y, and sums
+// adjacent pairs, yielding a vector of half as many elements with twice the
+// input element size.
+//
+// w[i] = x[i] * y[i] // Double width
+// z[i] = w[2*i] + w[2*i+1]
+//
+//specgen:require z={xB}{xN*2}x{xL/2}
+func DotProductPairs[E Nums, W Width, zE Nums](x, y Vec[E, W]) (z Vec[zE, W]) {
+ // TODO: How do we handle/specify overflow? x86 only supports this on signed
+ // types, and the only case that can overflow is if all four elements are
+ // MinInt16 (in which case the true result is MaxInt32+1, which wraps around
+ // to MinInt32). Unsigned types can overflow much more readily.
+ //
+ // Maybe we just leave overflow unspecified (or "architecture dependent").
+ // In which case, we probably need a way to communicate that in the spec
+ // (designated panic?).
+ //
+ // We might also need a way to constraint this to same-signed E and zE,
+ // which the constraint language doesn't currently have a way to say, but we
+ // could add as a built-in projection function in the syntax.
+ z = makeVec[zE, W]()
+ for i := range z {
+ z[i] = zE(x[2*i])*zE(y[2*i]) + zE(x[2*i+1])*zE(y[2*i+1])
+ }
+ return z
+}
+
+// DotProductPairsSaturated multiplies corresponding elements of x and y, and
+// sums adjacent pairs, all with saturation. It yields a vector of half as many
+// elements with twice the input element size.
+//
+// w[i] = x[i] * y[i] // Double width, saturated
+// z[i] = w[2*i] + w[2*i+1] // Saturated
+//
+//specgen:require y=Int{xN}x{xL} z=Int{xN*2}x{xL/2}
+func DotProductPairsSaturated[xE Uints, xW Width, yE Ints, zE Ints](x Vec[xE, xW], y Vec[yE, xW]) (z Vec[zE, xW]) {
+ z = makeVec[zE, xW]()
+ for i := range z {
+ a := mulSaturatedUSS64(uint64(x[2*i]), int64(y[2*i]))
+ b := mulSaturatedUSS64(uint64(x[2*i+1]), int64(y[2*i+1]))
+ z[i] = saturateS[zE](addSaturatedSSS64(a, b))
+ }
+ return z
+}
diff --git a/src/simd/internal/spec/mathlib.go b/src/simd/internal/spec/mathlib.go
new file mode 100644
index 0000000..66f2de8
--- /dev/null
+++ b/src/simd/internal/spec/mathlib.go
@@ -0,0 +1,136 @@
+// 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 spec
+
+import (
+ "math"
+ "math/bits"
+)
+
+func isSigned[T Ints | Uints]() bool {
+ return T(0)-1 < 0
+}
+
+func maxVal[T Ints | Uints]() T {
+ if isSigned[T]() {
+ var zero T
+ switch any(zero).(type) {
+ case int8:
+ return any(int8(math.MaxInt8)).(T)
+ case int16:
+ return any(int16(math.MaxInt16)).(T)
+ case int32:
+ return any(int32(math.MaxInt32)).(T)
+ case int64:
+ return any(int64(math.MaxInt64)).(T)
+ }
+ panic("unhandled type")
+ }
+ return ^T(0)
+}
+
+func minVal[T Ints | Uints]() T {
+ if isSigned[T]() {
+ return ^maxVal[T]()
+ }
+ return 0
+}
+
+// saturate converts x to type T, with saturation.
+func saturate[T Ints | Uints, U Ints | Uints](x T) U {
+ if isSigned[T]() {
+ return saturateS[U](int64(x))
+ }
+ return saturateU[U](uint64(x))
+}
+
+// saturateS converts signed x to type T, with saturation.
+func saturateS[T Ints | Uints](x int64) T {
+ if int64(T(x)) == x && (x >= 0 || isSigned[T]()) {
+ // It's in range.
+ return T(x)
+ }
+
+ // Out of range
+ if x > 0 {
+ return maxVal[T]()
+ }
+ return minVal[T]()
+}
+
+// saturateU converts unsigned x to type T, with saturation.
+func saturateU[T Ints | Uints](x uint64) T {
+ if x < uint64(maxVal[T]()) {
+ return T(x)
+ }
+ return maxVal[T]()
+}
+
+func addSaturated[T Ints | Uints](x, y T) T {
+ if isSigned[T]() {
+ return saturateS[T](addSaturatedSSS64(int64(x), int64(y)))
+ }
+ sum, carry := bits.Add64(uint64(x), uint64(y), 0)
+ if carry > 0 {
+ return maxVal[T]()
+ }
+ return saturateU[T](sum)
+}
+
+func addSaturatedSSS64(x, y int64) int64 {
+ sum := x + y
+
+ // Overflow can only happen if x and y have the same sign, and the sum has a
+ // different sign.
+ //
+ // (x ^ sum) & (y ^ sum) checks if the sign bit of sum matches neither x nor y.
+ if (x^sum)&(y^sum) < 0 {
+ if x > 0 {
+ return math.MaxInt64
+ }
+ return math.MinInt64
+ }
+
+ return sum
+}
+
+func mulSaturatedUSS[X Uints, Y Ints](x X, y Y) Y {
+ // Expand to 64 bits and perform saturated multiplication
+ z := mulSaturatedUSS64(uint64(x), int64(y))
+ return saturateS[Y](z)
+}
+
+func mulSaturatedUSS64(x uint64, y int64) int64 {
+ if x == 0 || y == 0 {
+ return 0
+ }
+
+ // Get the absolute value of i as a uint64
+ var absI uint64
+ if y == math.MinInt64 {
+ absI = math.MaxInt64 + 1
+ } else if y < 0 {
+ absI = uint64(-y)
+ } else {
+ absI = uint64(y)
+ }
+
+ // 128-bit multiplication
+ hi, lo := bits.Mul64(x, absI)
+
+ if y > 0 {
+ // Positive result. Check for overflow.
+ if hi > 0 || lo >= math.MaxInt64 {
+ return math.MaxInt64
+ }
+ return int64(lo)
+ } else {
+ // Negative result. Check for underflow.
+ if hi > 0 || lo >= uint64(math.MaxInt64)+1 {
+ return math.MinInt64
+ }
+ return -int64(lo)
+ }
+}
diff --git a/src/simd/internal/spec/mathlib_test.go b/src/simd/internal/spec/mathlib_test.go
new file mode 100644
index 0000000..0724439
--- /dev/null
+++ b/src/simd/internal/spec/mathlib_test.go
@@ -0,0 +1,124 @@
+// 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 spec
+
+import (
+ "math"
+ "testing"
+)
+
+func assertEq[T comparable](t *testing.T, got, want T, msg string) {
+ t.Helper()
+ if got != want {
+ t.Errorf("%s: got %v, want %v", msg, got, want)
+ }
+}
+
+func TestIsSigned(t *testing.T) {
+ assertEq(t, isSigned[int8](), true, "isSigned[int8]()")
+ assertEq(t, isSigned[uint8](), false, "isSigned[uint8]()")
+ assertEq(t, isSigned[int16](), true, "isSigned[int16]()")
+ assertEq(t, isSigned[uint16](), false, "isSigned[uint16]()")
+ assertEq(t, isSigned[int32](), true, "isSigned[int32]()")
+ assertEq(t, isSigned[uint32](), false, "isSigned[uint32]()")
+ assertEq(t, isSigned[int64](), true, "isSigned[int64]()")
+ assertEq(t, isSigned[uint64](), false, "isSigned[uint64]()")
+}
+
+func TestMaxVal(t *testing.T) {
+ assertEq(t, maxVal[int8](), int8(math.MaxInt8), "maxVal[int8]")
+ assertEq(t, maxVal[uint8](), uint8(math.MaxUint8), "maxVal[uint8]")
+ assertEq(t, maxVal[int16](), int16(math.MaxInt16), "maxVal[int16]")
+ assertEq(t, maxVal[uint16](), uint16(math.MaxUint16), "maxVal[uint16]")
+ assertEq(t, maxVal[int32](), int32(math.MaxInt32), "maxVal[int32]")
+ assertEq(t, maxVal[uint32](), uint32(math.MaxUint32), "maxVal[uint32]")
+ assertEq(t, maxVal[int64](), int64(math.MaxInt64), "maxVal[int64]")
+ assertEq(t, maxVal[uint64](), uint64(math.MaxUint64), "maxVal[uint64]")
+}
+
+func TestMinVal(t *testing.T) {
+ assertEq(t, minVal[int8](), int8(math.MinInt8), "minVal[int8]")
+ assertEq(t, minVal[uint8](), uint8(0), "minVal[uint8]")
+ assertEq(t, minVal[int16](), int16(math.MinInt16), "minVal[int16]")
+ assertEq(t, minVal[uint16](), uint16(0), "minVal[uint16]")
+ assertEq(t, minVal[int32](), int32(math.MinInt32), "minVal[int32]")
+ assertEq(t, minVal[uint32](), uint32(0), "minVal[uint32]")
+ assertEq(t, minVal[int64](), int64(math.MinInt64), "minVal[int64]")
+ assertEq(t, minVal[uint64](), uint64(0), "minVal[uint64]")
+}
+
+func TestSaturate(t *testing.T) {
+ // saturate[T Ints | Uints, U Ints | Uints](x T) U
+
+ // Signed to signed: int16 -> int8
+ assertEq(t, saturate[int16, int8](126), int8(126), "saturate[int16, int8](126)")
+ assertEq(t, saturate[int16, int8](-127), int8(-127), "saturate[int16, int8](-127)")
+ assertEq(t, saturate[int16, int8](128), int8(math.MaxInt8), "saturate[int16, int8](128) overflow")
+ assertEq(t, saturate[int16, int8](-129), int8(math.MinInt8), "saturate[int16, int8](-129) underflow")
+
+ // Unsigned to unsigned: uint16 -> uint8
+ assertEq(t, saturate[uint16, uint8](254), uint8(254), "saturate[uint16, uint8](254)")
+ assertEq(t, saturate[uint16, uint8](256), uint8(math.MaxUint8), "saturate[uint16, uint8](256) overflow")
+
+ // Signed to unsigned: int16 -> uint8
+ assertEq(t, saturate[int16, uint8](1), uint8(1), "saturate[int16, uint8](1)")
+ assertEq(t, saturate[int16, uint8](-1), uint8(0), "saturate[int16, uint8](-1) underflow")
+ assertEq(t, saturate[int16, uint8](254), uint8(254), "saturate[int16, uint8](254)")
+ assertEq(t, saturate[int16, uint8](256), uint8(math.MaxUint8), "saturate[int16, uint8](256) overflow")
+
+ // Unsigned to signed: uint16 -> int8
+ assertEq(t, saturate[uint16, int8](126), int8(126), "saturate[uint16, int8](126)")
+ assertEq(t, saturate[uint16, int8](128), int8(math.MaxInt8), "saturate[uint16, int8](128) overflow")
+
+ // Wider bounds: int64 to int32
+ assertEq(t, saturate[int64, int32](math.MaxInt32-1), int32(math.MaxInt32-1), "saturate[int64, int32](MaxInt32-1)")
+ assertEq(t, saturate[int64, int32](math.MaxInt32+1), int32(math.MaxInt32), "saturate[int64, int32](MaxInt32+1) overflow")
+ assertEq(t, saturate[int64, int32](math.MinInt32+1), int32(math.MinInt32+1), "saturate[int64, int32](MinInt32+1)")
+ assertEq(t, saturate[int64, int32](math.MinInt32-1), int32(math.MinInt32), "saturate[int64, int32](MinInt32-1) underflow")
+}
+
+func TestAddSaturated(t *testing.T) {
+ // Signed int8
+ assertEq(t, addSaturated[int8](125, 1), int8(126), "addSaturated[int8](125, 1)")
+ assertEq(t, addSaturated[int8](120, 10), int8(math.MaxInt8), "addSaturated[int8](120, 10) overflow")
+ assertEq(t, addSaturated[int8](-126, -1), int8(-127), "addSaturated[int8](-126, -1)")
+ assertEq(t, addSaturated[int8](-120, -10), int8(math.MinInt8), "addSaturated[int8](-120, -10) underflow")
+
+ // Unsigned uint8
+ assertEq(t, addSaturated[uint8](253, 1), uint8(254), "addSaturated[uint8](253, 1)")
+ assertEq(t, addSaturated[uint8](250, 10), uint8(math.MaxUint8), "addSaturated[uint8](250, 10) overflow")
+ assertEq(t, addSaturated[uint8](254, 0), uint8(254), "addSaturated[uint8](254, 0)")
+
+ // Signed int64
+ assertEq(t, addSaturated[int64](math.MaxInt64-2, 1), int64(math.MaxInt64-1), "addSaturated[int64](MaxInt64-2, 1)")
+ assertEq(t, addSaturated[int64](math.MaxInt64-5, 10), int64(math.MaxInt64), "addSaturated[int64](max-5, 10) overflow")
+ assertEq(t, addSaturated[int64](math.MinInt64+2, -1), int64(math.MinInt64+1), "addSaturated[int64](MinInt64+2, -1)")
+ assertEq(t, addSaturated[int64](math.MinInt64+5, -10), int64(math.MinInt64), "addSaturated[int64](min+5, -10) underflow")
+
+ // Unsigned uint64
+ assertEq(t, addSaturated[uint64](math.MaxUint64-2, 1), uint64(math.MaxUint64-1), "addSaturated[uint64](MaxUint64-2, 1)")
+ assertEq(t, addSaturated[uint64](math.MaxUint64-5, 10), uint64(math.MaxUint64), "addSaturated[uint64](max-5, 10) overflow")
+}
+
+func TestMulSaturatedUSS(t *testing.T) {
+ // mulSaturatedUSS[X Uints, Y Ints](x X, y Y) Y
+
+ // uint8, int8
+ assertEq(t, mulSaturatedUSS[uint8, int8](0, 10), int8(0), "mulSaturatedUSS[uint8, int8](0, 10)")
+ assertEq(t, mulSaturatedUSS[uint8, int8](10, 0), int8(0), "mulSaturatedUSS[uint8, int8](10, 0)")
+ assertEq(t, mulSaturatedUSS[uint8, int8](2, 63), int8(126), "mulSaturatedUSS[uint8, int8](2, 63)")
+ assertEq(t, mulSaturatedUSS[uint8, int8](1, 126), int8(126), "mulSaturatedUSS[uint8, int8](1, 126)")
+ assertEq(t, mulSaturatedUSS[uint8, int8](1, -127), int8(-127), "mulSaturatedUSS[uint8, int8](1, -127)")
+ assertEq(t, mulSaturatedUSS[uint8, int8](10, 20), int8(math.MaxInt8), "mulSaturatedUSS[uint8, int8](10, 20) positive overflow")
+ assertEq(t, mulSaturatedUSS[uint8, int8](10, -20), int8(math.MinInt8), "mulSaturatedUSS[uint8, int8](10, -20) negative overflow")
+
+ // uint64, int64
+ assertEq(t, mulSaturatedUSS[uint64, int64](2, (math.MaxInt64-1)/2), int64(math.MaxInt64-1), "mulSaturatedUSS[uint64, int64](2, (MaxInt64-1)/2)")
+ assertEq(t, mulSaturatedUSS[uint64, int64](2, math.MaxInt64), int64(math.MaxInt64), "mulSaturatedUSS[uint64, int64](2, MaxInt64) overflow")
+ assertEq(t, mulSaturatedUSS[uint64, int64](2, (math.MinInt64/2)+1), int64(math.MinInt64+2), "mulSaturatedUSS[uint64, int64](2, (MinInt64/2)+1)")
+ assertEq(t, mulSaturatedUSS[uint64, int64](2, math.MinInt64), int64(math.MinInt64), "mulSaturatedUSS[uint64, int64](2, MinInt64) underflow")
+ assertEq(t, mulSaturatedUSS[uint64, int64](math.MaxUint64, 1), int64(math.MaxInt64), "mulSaturatedUSS[uint64, int64](MaxUint64, 1) overflow")
+ assertEq(t, mulSaturatedUSS[uint64, int64](math.MaxUint64, -1), int64(math.MinInt64), "mulSaturatedUSS[uint64, int64](MaxUint64, -1) underflow")
+}
diff --git a/src/simd/internal/spec/reinterp.go b/src/simd/internal/spec/reinterp.go
new file mode 100644
index 0000000..00e2ce4
--- /dev/null
+++ b/src/simd/internal/spec/reinterp.go
@@ -0,0 +1,21 @@
+// 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 spec
+
+// ReshapeToUints reinterprets the bits of x as a {z} vector. The least
+// significant bit of element 0 is bit 0
+//
+//specgen:name ReshapeToUint{zN}s
+//specgen:require xN!=zN
+func ReshapeToUints[xE Uints, xW Width, zE Uints](x Vec[xE, xW]) (z Vec[zE, xW]) {
+ z = makeVec[zE, xW]()
+ xN, zN := elemBits[xE](), elemBits[zE]()
+ // Copy a byte at a time.
+ for bit := 0; bit < width[xW](); bit += 8 {
+ b := byte(x[bit/xN] >> (bit % xN))
+ z[bit/zN] |= zE(b) << (bit % zN)
+ }
+ return z
+}
diff --git a/src/simd/internal/spec/shuffles.go b/src/simd/internal/spec/shuffles.go
new file mode 100644
index 0000000..6578067
--- /dev/null
+++ b/src/simd/internal/spec/shuffles.go
@@ -0,0 +1,18 @@
+// 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 spec
+
+// Permute permutes x.
+//
+// z[i] = x[indices[i] % len(x)]
+//
+//specgen:require indices=Uint{xN}x{xL}
+func Permute[E Elt, W Width, E2 Uints](x Vec[E, W], indices Vec[E2, W]) (z Vec[E, W]) {
+ z = makeVec[E, W]()
+ for i := range z {
+ z[i] = x[uint(indices[i])%uint(x.len())]
+ }
+ return z
+}
diff --git a/src/simd/internal/spec/types.go b/src/simd/internal/spec/types.go
new file mode 100644
index 0000000..c9f535e
--- /dev/null
+++ b/src/simd/internal/spec/types.go
@@ -0,0 +1,146 @@
+// 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 spec
+
+import (
+ "unsafe"
+)
+
+// Element types
+
+// Floats is all float element types.
+type Floats interface {
+ float32 | float64
+}
+
+// Ints is all signed int element types.
+type Ints interface {
+ int8 | int16 | int32 | int64
+}
+
+// Uints is all unsigned uint element types.
+type Uints interface {
+ uint8 | uint16 | uint32 | uint64
+}
+
+// Nums is all numeric element types.
+type Nums interface {
+ Floats | Ints | Uints
+}
+
+// Mask element types can be used in a Vec to represent a mask.
+//
+// We use uintN types for these so that regular math for element width and lanes
+// work as expected. In a sense, these act like a "wide mask": logically these
+// are bool values, represented as either 0 for false or ^0 for true.
+type (
+ Mask8 uint8
+ Mask16 uint16
+ Mask32 uint32
+ Mask64 uint64
+)
+
+// MaskElt is all mask element types. In function signatures, these must always
+// be used as the element to a Vec type. They cannot be standalone.
+type MaskElt interface {
+ Mask8 | Mask16 | Mask32 | Mask64
+}
+
+// Elt is all regular (non-mask) vector element types.
+type Elt interface {
+ Nums
+}
+
+// EltOrMask is a constraint that accepts any regular vector element type or
+// mask element type.
+//
+// This type is known to specgen.
+type EltOrMask interface {
+ Elt | MaskElt
+}
+
+// Widths
+
+// Width is a constraint that accepts any type representing a vector width.
+//
+// This type is known to specgen.
+type Width interface {
+ Width128 | Width256 | Width512 | WidthScalable
+ bits() int // Minimum is 128
+}
+
+// FixedWidth is a constraint for all fixed (non-scalable) vector width types.
+type FixedWidth interface {
+ Width128 | Width256 | Width512
+ bits() int
+}
+
+type Width128 struct{}
+
+func (Width128) bits() int { return 128 }
+
+type Width256 struct{}
+
+func (Width256) bits() int { return 256 }
+
+type Width512 struct{}
+
+func (Width512) bits() int { return 512 }
+
+// scalableWidth is the bit width to use for scalable vectors when executing the
+// spec. This is intentionally set to be large and different from any hardware
+// platform we support.
+//
+// TODO: For testing against the spec, do we need a way to match this to the
+// hardware we're testing on?
+const scalableWidth = 4096
+
+// WidthScalable is the width representing scalable vectors. At a spec level,
+// the actual width this represents is completely symbolic, but when executing
+// the spec, we concretely interpret this as [scalableWidth] bits.
+//
+// This type is known to specgen.
+type WidthScalable struct{}
+
+func (WidthScalable) bits() int { return scalableWidth }
+
+// Vectors
+
+// Vec is a vector or mask consisting of the given element type E expanded out
+// to W total bits.
+//
+// This is implemented as a Go slice, which must have length `width[W]() / elemBits[E]()`.
+//
+// This type is known to specgen.
+type Vec[E EltOrMask, W Width] []E
+
+func (v Vec[E, W]) len() int {
+ return lanes[E, W]()
+}
+
+func makeVec[E EltOrMask, W Width]() Vec[E, W] {
+ return make([]E, lanes[E, W]())
+}
+
+func elemBits[E EltOrMask]() int {
+ return 8 * int(unsafe.Sizeof(*new(E)))
+}
+
+func width[W Width]() int {
+ return (*(new(W))).bits()
+}
+
+func lanes[E EltOrMask, W Width]() int {
+ return width[W]() / elemBits[E]()
+}
+
+// Other types
+
+// Array represents an array of lanes[E,W]() elements.
+//
+// The static generator will translate this to a Go array type.
+//
+// This type is known to specgen.
+type Array[E Elt, W Width] []E
| Inspect html for hidden footers to help with email filtering. To unsubscribe visit settings. |