blob: 94e08f7625c2f049ce4c0f09de8e48779299b723 [file]
// 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 main
import (
"cmp"
"errors"
"go/ast"
"go/token"
"go/types"
"io/fs"
"log"
"os"
"path/filepath"
"slices"
"strings"
"golang.org/x/tools/go/packages"
"gopkg.in/yaml.v3"
"simd/archsimd/_gen/gentools"
)
// buildConfigs are the configurations we report API coverage for. The API is spread
// across files selected by build constraints, and the only authority on which
// files belong to which configuration is the go command -- so rather than
// interpreting //go:build lines here, we ask go/packages once per configuration
// and use the file set it hands back.
//
// SVE is a target in SPEC-TRANSITION.md's sense but not a GOARCH: its files are
// built for arm64 like NEON's. It separates out along the generator dimension
// instead, because simdgen records its -arch in the generated-by header.
var buildConfigs = []struct{ name, goos, goarch string }{
{"amd64", "linux", "amd64"},
{"arm64", "linux", "arm64"},
{"wasm", "js", "wasm"},
// Package simd falls back to a pure-Go implementation on architectures with
// no SIMD support. 386 is an arbitrary representative of those.
{"portable", "linux", "386"},
}
// A decl is one exported function or method declaration in the public API.
type decl struct {
pkg string // "simd" or "archsimd"
file string // path relative to the simd directory
gen string // generator that emitted it, or "hand-written"
targets []string // names of build configurations it compiles in
recv string // receiver type name, empty for plain functions
name string
in []param
out []param
doc string // doc comment text, comment markers stripped
hasSep bool // doc contains a bare "//" separator line
}
type param struct{ name, typ string }
// The grains the report counts at, coarsest last. Spec describes an operation
// once, the API declares it once per target, and a name collapses receivers
// too; the gaps between the three are diagnostic (SPEC-TRANSITION.md §1.2).
type (
// methodKey is the (receiver, name) key spec is unique on.
methodKey struct{ recv, name string }
// archMethodKey is a method as it appears in one build configuration.
//
// It carries the package because simd and archsimd share their scalable
// type names.
archMethodKey struct {
target string
pkg string
methodKey
}
// declKey identifies one declaration in the source, which is what collapses
// the several configurations it may build in.
declKey struct {
file string
methodKey
}
)
// A fact is one declaration as seen in one build configuration: the grain of
// the fact table, and the row type every figure in the report pivots over.
type fact struct {
*decl
target string
}
func (d *decl) methodKey() methodKey { return methodKey{d.recv, d.name} }
func (f fact) declKey() declKey { return declKey{f.file, f.methodKey()} }
func (f fact) archMethodKey() archMethodKey { return archMethodKey{f.target, f.pkg, f.methodKey()} }
// tool strips the invocation from a generator label, so that simdgen's three
// runs count as one tool.
func (f fact) tool() string { t, _, _ := strings.Cut(f.gen, "/"); return t }
// nonSpecOps are the operations that will never come from spec: they are Go
// conventions, not SIMD operations. They are excluded from the fact table, and
// so from every denominator. See SPEC-TRANSITION.md §2.7.
var nonSpecOps = map[string]bool{"String": true, "Len": true}
// loadOverlay builds a packages.Config.Overlay map from the scratch overlay
// directory if one is active.
func loadOverlay(genOpts *gentools.Options) map[string][]byte {
if genOpts == nil {
return nil
}
dir := genOpts.OverlayDir()
if dir == "" {
return nil
}
root := filepath.Join(dir, "src")
overlay := make(map[string][]byte)
_ = filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil || d.IsDir() {
return nil
}
rel, err := filepath.Rel(root, path)
if err != nil {
return nil
}
slashRel := filepath.ToSlash(rel)
if !strings.HasPrefix(slashRel, "simd/") {
return nil
}
b, err := genOpts.ReadFile(rel)
if err != nil {
return nil
}
gorootPath := filepath.Join(genOpts.GOROOT, "src", filepath.FromSlash(rel))
overlay[gorootPath] = b
return nil
})
return overlay
}
func loadAPI(genOpts *gentools.Options, simdDir string) []*decl {
fset := token.NewFileSet()
byKey := map[declKey]*decl{}
overlay := loadOverlay(genOpts)
for _, bc := range buildConfigs {
cfg := &packages.Config{
// Syntax only: we compare declarations across configurations, which
// type-checking would not help with and would slow down. See
// SPEC-TRANSITION.md §2.4.
Mode: packages.NeedName | packages.NeedFiles | packages.NeedCompiledGoFiles | packages.NeedSyntax,
Dir: simdDir,
Fset: fset,
Env: append(os.Environ(), "GOOS="+bc.goos, "GOARCH="+bc.goarch, "GOEXPERIMENT=simd"),
Overlay: overlay,
}
pkgs, err := packages.Load(cfg, ".", "./archsimd")
if err != nil {
log.Fatalf("loading simd packages for %s/%s: %s", bc.goos, bc.goarch, err)
}
for _, p := range pkgs {
for _, e := range p.Errors {
log.Fatalf("loading %s for %s/%s: %s", p.PkgPath, bc.goos, bc.goarch, e)
}
for _, f := range p.Syntax {
file := fset.Position(f.Pos()).Filename
if rel, err := filepath.Rel(simdDir, file); err == nil {
file = filepath.ToSlash(rel)
}
gen := generatorOf(f)
for _, d := range f.Decls {
fd, ok := d.(*ast.FuncDecl)
if !ok || !fd.Name.IsExported() {
continue
}
dec := declOf(fd, p.Name, file, gen)
key := declKey{file, dec.methodKey()}
if prev, ok := byKey[key]; ok {
dec = prev
} else {
byKey[key] = dec
}
dec.targets = append(dec.targets, bc.name)
}
}
}
}
decls := make([]*decl, 0, len(byKey))
for _, d := range byKey {
slices.Sort(d.targets)
d.targets = slices.Compact(d.targets)
decls = append(decls, d)
}
slices.SortFunc(decls, func(a, b *decl) int {
return cmp.Or(cmp.Compare(a.file, b.file), cmp.Compare(a.recv, b.recv), cmp.Compare(a.name, b.name))
})
return decls
}
func declOf(fd *ast.FuncDecl, pkg, file, gen string) *decl {
d := &decl{pkg: pkg, file: file, gen: gen, name: fd.Name.Name}
if fd.Recv != nil && len(fd.Recv.List) == 1 {
// Receivers in this API are always by value and never generic.
if id, ok := fd.Recv.List[0].Type.(*ast.Ident); ok {
d.recv = id.Name
} else {
d.recv = types.ExprString(fd.Recv.List[0].Type)
}
}
d.in = fields(fd.Type.Params)
d.out = fields(fd.Type.Results)
if fd.Doc != nil {
var lines []string
for _, c := range fd.Doc.List {
lines = append(lines, c.Text)
if strings.TrimSpace(c.Text) == "//" {
d.hasSep = true
}
}
d.doc = strings.Join(lines, "\n")
}
return d
}
// fields flattens a parameter or result list, preserving declaration order and
// expanding grouped names ("x, y Vec") into one param each.
func fields(fl *ast.FieldList) []param {
if fl == nil {
return nil
}
var ps []param
for _, f := range fl.List {
typ := types.ExprString(f.Type)
if len(f.Names) == 0 {
ps = append(ps, param{"", typ})
continue
}
for _, n := range f.Names {
ps = append(ps, param{n.Name, typ})
}
}
return ps
}
// generatorOf returns the tool named in a "Code generated by" header, or
// "hand-written" if there is none.
//
// Since simdgen runs separately per target, for simdgen this returns
// "simdgen/<arch>", where arch is the -arch flag passed to simdgen.
func generatorOf(f *ast.File) string {
for _, cg := range f.Comments {
for _, c := range cg.List {
if !strings.Contains(c.Text, "Code generated by") {
continue
}
for _, tool := range []string{"simdgen", "tmplgen", "wasmgen", "midway", "refgen"} {
if !strings.Contains(c.Text, tool) {
continue
}
if args := strings.Fields(c.Text); tool == "simdgen" {
for i, a := range args {
if a == "-arch" && i+1 < len(args) {
return tool + "/" + args[i+1]
}
}
}
return tool
}
return "generated"
}
break // only the leading comment block can carry the header
}
return "hand-written"
}
// readYAML parses one YAML document into a node tree and returns its root
// content node. The generators' inputs carry custom tags (!sum, !string, and
// the merge keys in comments.yaml), but a node tree keeps a tag as data rather
// than trying to resolve it, so this reads them without knowing their schemas.
//
// A missing file counts as empty: these inputs are deleted by comments-yaml and cat-delete,
// and their figures should read zero rather than crash once they are gone. A
// malformed file is fatal, because that is a real problem with the input.
func readYAML(path string) *yaml.Node {
b, err := os.ReadFile(path)
if errors.Is(err, fs.ErrNotExist) {
return nil
} else if err != nil {
log.Fatal(err)
}
var doc yaml.Node
if err := yaml.Unmarshal(b, &doc); err != nil {
log.Fatalf("%s: %s", path, err)
}
if doc.Kind == yaml.DocumentNode && len(doc.Content) == 1 {
return doc.Content[0]
}
return &doc
}
// eachEntry calls fn for every key/value pair of every mapping under n, depth
// first.
//
// It does not follow aliases. comments.yaml merges a shared block into fourteen
// receivers with "<<: *common_methods", and those entries are counted where
// they are defined; expanding the merge would count them fifteen times over.
func eachEntry(n *yaml.Node, fn func(key, val *yaml.Node)) {
if n == nil || n.Kind == yaml.AliasNode {
return
}
if n.Kind == yaml.MappingNode {
for i := 0; i+1 < len(n.Content); i += 2 {
fn(n.Content[i], n.Content[i+1])
}
}
for _, c := range n.Content {
eachEntry(c, fn)
}
}
// mapValue returns the value of key in a mapping node, or nil.
func mapValue(n *yaml.Node, key string) *yaml.Node {
if n == nil || n.Kind != yaml.MappingNode {
return nil
}
for i := 0; i+1 < len(n.Content); i += 2 {
if n.Content[i].Value == key {
return n.Content[i+1]
}
}
return nil
}
// isString reports whether n is a string scalar, which in these files means a
// documentation string. Block scalars ("|-") and quoted scalars both qualify.
func isString(n *yaml.Node) bool {
return n != nil && n.Kind == yaml.ScalarNode && n.ShortTag() == "!!str"
}
// categoriesInventory holds the inventory of categories.yaml.
type categoriesInventory struct {
allFields map[string]int // field → count across all entries
exportedFields map[string]int // field → count across exported entries only
entries int
uniq int
exported int
compilerOnly int
}
func makeCategoriesInventory(genDir string) categoriesInventory {
r := categoriesInventory{
allFields: map[string]int{},
exportedFields: map[string]int{},
}
names := map[string]bool{}
paths, _ := filepath.Glob(filepath.Join(genDir, "simdgen", "ops", "*", "categories.yaml"))
slices.Sort(paths)
for _, p := range paths {
root := readYAML(p)
if root == nil || root.Kind != yaml.SequenceNode {
continue
}
for _, entry := range root.Content {
if entry.Kind != yaml.MappingNode {
continue
}
goName := mapValue(entry, "go")
if goName == nil {
continue
}
r.entries++
names[goName.Value] = true
isExported := goName.Value != "" && !(goName.Value[0] >= 'a' && goName.Value[0] <= 'z')
hasNoTypes := mapValue(entry, "noTypes") != nil
if !isExported || hasNoTypes {
r.compilerOnly++
}
eachEntry(entry, func(k, _ *yaml.Node) {
r.allFields[k.Value]++
if isExported && !hasNoTypes {
r.exportedFields[k.Value]++
}
})
}
}
for n := range names {
r.uniq++
if n != "" && !(n[0] >= 'a' && n[0] <= 'z') {
r.exported++
}
}
return r
}
// countMidwayComments counts the doc-bearing entries in midway/comments.yaml:
// every key whose value is a string, wherever it appears.
func countMidwayComments(genDir string) (n int) {
eachEntry(readYAML(commentsPath(genDir)), func(_, v *yaml.Node) {
if isString(v) {
n++
}
})
return n
}
// commonMethods returns the method names in comments.yaml's .common_methods
// block, less [nonSpecOps]. That set is spec-common's scope, and the portable
// core a new architecture implements first.
func commonMethods(genDir string) []string {
block := mapValue(readYAML(commentsPath(genDir)), ".common_methods")
var names []string
eachEntry(block, func(k, _ *yaml.Node) {
if !nonSpecOps[k.Value] {
names = append(names, k.Value)
}
})
return names
}
func commentsPath(genDir string) string {
return filepath.Join(genDir, "midway", "comments.yaml")
}