// 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" "path/filepath" "regexp" "simd/archsimd/_gen/specgen" "slices" "strings" "gopkg.in/yaml.v3" ) // specOp is one spec operation -- one function in package spec. It is the grain // spec is authored at, and the only grain at which "implemented nowhere" means // anything: one operation expands into many API names and many (receiver, name) // pairs. The API has no corresponding notion, so this is a spec-side diagnostic // rather than a fourth coverage grain (SPEC-TRANSITION.md §1.3). type specOp struct { name string // the operation's name in package spec covPairs int // pairs it generates that the API also declares } // Spec and the API are each a map from name to the set of receivers that name // is declared on. A relation is how those two sets stand for one name, and // walking the union of the two key sets classifies every name into exactly one // -- which the older "divergence classes" did not do, since a name whose sets // cross contributes to both sides at once. type relation int const ( relEqual relation = iota // api = spec relSubset // api is a proper subset of spec: spec covers more receivers relSuperset // api is a proper superset of spec: spec covers fewer relOverlap // they share receivers, neither contains the other relDisjoint // they share the name and not one receiver relSpecOnly // api = ∅: no target declares the name relAPIOnly // spec = ∅: spec has never defined the name numRelations ) // shared reports whether both sides define the name. The api-only receivers of // a shared name are defects; under spec = ∅ they are ordinary coverage. func (r relation) shared() bool { return r != relSpecOnly && r != relAPIOnly } var relationName = [numRelations]string{ relEqual: "api = spec", relSubset: "api ⊂ spec", relSuperset: "api ⊃ spec", relOverlap: "overlapping", relDisjoint: "disjoint", relSpecOnly: "api = ∅", relAPIOnly: "spec = ∅", } // nameRel is one name, and how the two receiver sets stand under it. type nameRel struct { name string rel relation api, spec int // receiver-set sizes apiOnly []string // receivers only the API declares, sorted specOnly int // receivers only spec generates } // relTotal is one row of the relation summary: how many names stand in it, and // how many receivers each side holds alone across them. type relTotal struct{ names, apiOnly, specOnly int } // fileCount pairs a filename with a count, for per-file breakdowns. type fileCount struct { file string n int } // metrics is the whole report as data. Every field is a tally or a pivot, so // the summary and the sections below it read the same numbers and cannot // disagree. type metrics struct { facts []fact decls []*decl // the facts' declarations, deduplicated across configurations spec map[methodKey]*specgen.Func // Coverage, at three grains and split two ways. covTriple *tally[archMethodKey] covPair *tally[methodKey] covName *tally[string] covTarget *pivot[archMethodKey] covGen *pivot[methodKey] covMulti *tally[string] // names emitted by more than one tool covCommon *tally[string] // comments.yaml's .common_methods, spec-common's scope specPairs, specNames int // Divergence classes ops []specOp // spec source functions, sorted by name specOnly []string // spec operations no target implements names []nameRel // every name either side defines, sorted rel [numRelations]relTotal // m.names summed by relation apiOnly, apiOnlyDecls int // the defect: over shared names only // Agreement with spec where the two overlap. Names are split the way // specdoc.Fill treats them: a named argument that disagrees with spec is a // mismatch, and an unnamed one is a name Fill will fill in from spec. agree, typeOK *tally[declKey] paramNameOK, resultNameOK *tally[declKey] paramsFilled, resultsFilled *tally[declKey] // The invariant, checked without reference to spec. consistent, typeSame, paramNameSame, resultNameSame *tally[methodKey] // Documentation. fromSpec *tally[declKey] bounded *tally[declKey] handConflict int handConflictByFile []fileCount crossPkg *tally[methodKey] mirror *tally[methodKey] // The inputs being retired. categories map[string]int catEntries, catUniq, catExported int catCompilerOnly int catExportedFields map[string]int comments int commentsSpecCovered int commutDrift int } func measure(api []*decl, specFuncs []*specgen.Func, genDir string) *metrics { m := &metrics{spec: map[methodKey]*specgen.Func{}} specNames := map[string]bool{} for _, f := range specFuncs { m.spec[specPair(f)] = f specNames[f.Name] = true } m.specPairs, m.specNames = len(m.spec), len(specNames) // The fact table: one row per declaration per configuration it builds in. for _, d := range api { if nonSpecOp(d.name) { continue } m.decls = append(m.decls, d) for _, t := range d.targets { m.facts = append(m.facts, fact{d, t}) } } covered := func(f fact) bool { return m.spec[f.methodKey()] != nil } // 1. Coverage. Same predicate, three grains, two dimensions. m.covTriple = tallyBy(m.facts, fact.archMethodKey, covered) m.covPair = tallyBy(m.facts, func(f fact) methodKey { return f.methodKey() }, covered) m.covName = tallyBy(m.facts, func(f fact) string { return f.name }, func(f fact) bool { return specNames[f.name] }) m.covTarget = pivotBy(m.facts, func(f fact) string { return f.target }, fact.archMethodKey, covered) m.covGen = pivotBy(m.facts, func(f fact) string { return f.gen }, func(f fact) methodKey { return f.methodKey() }, covered) for _, g := range m.covGen.dims() { if t, _, _ := strings.Cut(g, "/"); t != g { m.covGen.fold(g, t) } } // Names emitted by more than one tool are where the invariant is most // exposed, because no single generator can see the conflict. simdgen's own // runs share their inputs, so they do not count as more than one. tools := groupBy(m.facts, func(f fact) string { return f.name }) m.covMulti = new(tally[string]) for name, fs := range tools { seen := map[string]bool{} for _, f := range fs { if f.gen != "hand-written" { seen[f.tool()] = true } } if len(seen) > 1 { m.covMulti.add(name, specNames[name]) } } m.covCommon = new(tally[string]) for _, n := range commonMethods(api) { m.covCommon.add(n, m.covName.done(n)) } // 2. Divergence classes. byOp := map[string]*specOp{} for _, f := range specFuncs { src, _, _ := f.SpecFunc() op := byOp[src] if op == nil { op = &specOp{name: src} byOp[src] = op } if m.covPair.has(specPair(f)) { op.covPairs++ } } for _, op := range byOp { if op.covPairs == 0 { // Operations implemented by spec but not by the API on any platform // is measured at the grain spec is authored at: the source // function. A spec operation with no covered declaration anywhere // is likely a defect, because that takes four simultaneous // omissions to explain away. m.specOnly = append(m.specOnly, op.name) } m.ops = append(m.ops, *op) } slices.Sort(m.specOnly) slices.SortFunc(m.ops, func(a, b specOp) int { return cmp.Compare(a.name, b.name) }) // The two maps the rest of this section is about, and the union of their // keys. Everything below -- the defect count, the review queue, and the // coverage gap -- is a statement about how one name's two receiver sets // stand, so they are derived here once rather than counted three ways. apiRecv, specRecv := map[string]map[string]bool{}, map[string]map[string]bool{} add := func(m map[string]map[string]bool, name, recv string) { if m[name] == nil { m[name] = map[string]bool{} } m[name][recv] = true } for _, d := range m.decls { add(apiRecv, d.name, d.recv) } for p := range m.spec { if !nonSpecOp(p.name) { add(specRecv, p.name, p.recv) } } for name := range apiRecv { m.names = append(m.names, nameRelation(name, apiRecv[name], specRecv[name])) } for name := range specRecv { if apiRecv[name] == nil { m.names = append(m.names, nameRelation(name, nil, specRecv[name])) } } slices.SortFunc(m.names, func(a, b nameRel) int { return cmp.Compare(a.name, b.name) }) for _, r := range m.names { t := &m.rel[r.rel] t.names++ t.apiOnly += len(r.apiOnly) t.specOnly += r.specOnly if r.rel.shared() { m.apiOnly += len(r.apiOnly) } } // The same api-only receivers counted as declarations rather than pairs: one // pair can be declared once per target. m.apiOnlyDecls = countBy(filter(m.facts, func(f fact) bool { return !covered(f) && specNames[f.name] }), fact.declKey) // 3. Agreement with spec, over the declarations spec covers. overlap := filter(m.facts, covered) specIn := func(f fact) []param { return specParams(m.spec[f.methodKey()].In) } specOut := func(f fact) []param { return specParams(m.spec[f.methodKey()].Out) } typeOK := func(f fact) bool { return equalBy(f.in, specIn(f), paramType) && equalBy(f.out, specOut(f), paramType) } resultNameOK := func(f fact) bool { return !namesClash(f.out, specOut(f)) } paramNameOK := func(f fact) bool { return !namesClash(f.in, specIn(f)) } resultsFilled := func(f fact) bool { return !namesMissing(f.out, specOut(f)) } paramsFilled := func(f fact) bool { return !namesMissing(f.in, specIn(f)) } m.typeOK = tallyBy(overlap, fact.declKey, typeOK) m.resultNameOK = tallyBy(overlap, fact.declKey, resultNameOK) m.paramNameOK = tallyBy(overlap, fact.declKey, paramNameOK) m.resultsFilled = tallyBy(overlap, fact.declKey, resultsFilled) m.paramsFilled = tallyBy(overlap, fact.declKey, paramsFilled) m.agree = tallyBy(overlap, fact.declKey, func(f fact) bool { return typeOK(f) && resultNameOK(f) && paramNameOK(f) }) // 4. The invariant: where a pair is declared more than once, do the // declarations agree with each other? An unnamed argument agrees with any // name, since only names that are used -- by a doc or a body -- need to agree. m.consistent = new(tally[methodKey]) m.typeSame = new(tally[methodKey]) m.paramNameSame = new(tally[methodKey]) m.resultNameSame = new(tally[methodKey]) for p, ds := range groupBy(m.decls, (*decl).methodKey) { if len(ds) < 2 { continue } params := func(d *decl) []param { return d.in } results := func(d *decl) []param { return d.out } typesAgree := func(get func(*decl) []param) bool { return !slices.ContainsFunc(ds[1:], func(d *decl) bool { return !equalBy(get(ds[0]), get(d), paramType) }) } namesAgree := func(get func(*decl) []param) bool { lists := make([][]param, len(ds)) for i, d := range ds { lists[i] = get(d) } return namesConsistent(lists) } t := typesAgree(params) && typesAgree(results) pn, rn := namesAgree(params), namesAgree(results) m.typeSame.add(p, t) m.paramNameSame.add(p, pn) m.resultNameSame.add(p, rn) m.consistent.add(p, t && pn && rn) } // 5. Documentation. // A declaration is sourced from spec when its doc begins with spec's text -- // a prefix test, because Fill leaves the generator's notes below what it // injects. Prose that disagrees instead is a conflict for doc-triage; a missing // doc is not, since Fill would simply add one. documented := filter(overlap, func(f fact) bool { return len(m.spec[f.methodKey()].Doc) > 0 }) saysSpec := func(f fact) bool { specDoc, err := specgen.FormatComment(m.spec[f.methodKey()].Doc) if err != nil { return false } return strings.HasPrefix(f.doc, strings.TrimRight(specDoc, "\n")) } m.fromSpec = tallyBy(documented, fact.declKey, saysSpec) handConflicts := filter(documented, func(f fact) bool { return f.gen == "hand-written" && f.doc != "" && !saysSpec(f) }) m.handConflict = countBy(handConflicts, fact.declKey) byFile := map[string]int{} seenByFile := map[declKey]bool{} for _, f := range handConflicts { dk := f.declKey() if !seenByFile[dk] { seenByFile[dk] = true byFile[f.file]++ } } for _, k := range sortedKeys(byFile) { m.handConflictByFile = append(m.handConflictByFile, fileCount{k, byFile[k]}) } m.bounded = tallyBy(filter(m.facts, func(f fact) bool { return f.doc != "" }), fact.declKey, func(f fact) bool { return f.hasSep }) // simd and archsimd document the same operations from different sources. // Target-specific implementation notes (such as Asm: and CPU Feature:) // and directives are ignored when comparing docs. m.crossPkg = new(tally[methodKey]) docs := map[string]map[methodKey]string{"simd": {}, "archsimd": {}} for _, d := range m.decls { if d.doc != "" { docs[d.pkg][d.methodKey()] = d.doc } } for p, sd := range docs["simd"] { if ad, ok := docs["archsimd"][p]; ok { ssd, sad := stripDocNotes(sd), stripDocNotes(ad) m.crossPkg.add(p, ssd != "" && ssd == sad) } } // The hand-maintained mirror in package simd. This one deliberately spans // the whole of both files, residue included: midway owns those docs too. m.mirror = new(tally[methodKey]) stubs, emul := map[methodKey]string{}, map[methodKey]string{} for _, d := range api { switch d.file { case "simd_stubs.go": stubs[d.methodKey()] = d.doc case "simd_emulated.go": emul[d.methodKey()] = d.doc } } for p, s := range stubs { if e, ok := emul[p]; ok { m.mirror.add(p, s == e) } } // 7. The inputs being retired. cat := makeCategoriesInventory(genDir) m.categories = cat.allFields m.catEntries = cat.entries m.catUniq = cat.uniq m.catExported = cat.exported m.catCompilerOnly = cat.compilerOnly m.catExportedFields = cat.exportedFields m.comments = countMidwayComments(genDir) // Comments.yaml entries that spec already covers, by name. root := readYAML(commentsPath(genDir)) eachEntry(root, func(k, v *yaml.Node) { if isString(v) && specNames[k.Value] { m.commentsSpecCovered++ } }) // Commutativity drift: spec vs categories.yaml exported entries. m.commutDrift = commutativityDrift(genDir, m.spec) return m } // nameRelation classifies one name by how its two receiver sets stand. func nameRelation(name string, api, spec map[string]bool) nameRel { r := nameRel{name: name, api: len(api), spec: len(spec)} shared := 0 for recv := range api { if spec[recv] { shared++ } else { r.apiOnly = append(r.apiOnly, recv) } } slices.Sort(r.apiOnly) r.specOnly = len(spec) - shared switch { case len(spec) == 0: r.rel = relAPIOnly case len(api) == 0: r.rel = relSpecOnly case len(r.apiOnly) == 0 && r.specOnly == 0: r.rel = relEqual case len(r.apiOnly) == 0: r.rel = relSubset case r.specOnly == 0: r.rel = relSuperset case shared == 0: r.rel = relDisjoint default: r.rel = relOverlap } return r } // commutativityDrift counts exported categories.yaml entries whose commutative field // disagrees with spec. Some go: values are regexps, so each is tried as one. func commutativityDrift(genDir string, spec map[methodKey]*specgen.Func) int { var n int 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 } isExported := goName.Value != "" && !(goName.Value[0] >= 'a' && goName.Value[0] <= 'z') if !isExported || mapValue(entry, "noTypes") != nil { continue } commNode := mapValue(entry, "commutative") if commNode == nil { continue } catCommut := commNode.Value == "true" re, err := regexp.Compile("^" + goName.Value + "$") if err != nil { continue } for p, f := range spec { if re.MatchString(p.name) && f.Commutative != catCommut { n++ break } } } } return n } func filter[T any](s []T, keep func(T) bool) []T { var out []T for _, v := range s { if keep(v) { out = append(out, v) } } return out } // specPair returns the (receiver, name) key for a spec function. func specPair(f *specgen.Func) methodKey { var recv string if f.Recv.Type != nil { recv = f.Recv.Type.String() } return methodKey{recv, f.Name} } // stripDocNotes returns doc with target-specific implementation notes and // directives removed, leaving only spec-owned paragraphs. func stripDocNotes(doc string) string { var keptParas []string var curPara []string flush := func() bool { if len(curPara) == 0 { return true } first := strings.TrimSpace(curPara[0]) if strings.HasPrefix(first, "//go:") || strings.HasPrefix(first, "//line ") { return false } text := strings.TrimPrefix(first, "// ") text = strings.TrimPrefix(text, "//") if specgen.HasNotePrefix(text) { return false } keptParas = append(keptParas, strings.Join(curPara, "\n")) curPara = nil return true } for _, line := range strings.Split(doc, "\n") { trimmed := strings.TrimSpace(line) if strings.HasPrefix(trimmed, "//go:") || strings.HasPrefix(trimmed, "//line ") { if !flush() { return strings.Join(keptParas, "\n//\n") } break } if trimmed == "//" { if !flush() { return strings.Join(keptParas, "\n//\n") } continue } curPara = append(curPara, line) } flush() return strings.Join(keptParas, "\n//\n") }