blob: 740b9f6358d81c0020c9df4e2c425d49dadc6afe [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 specdoc validates exported SIMD declarations against package
// simd/internal/spec and manages the composition and injection of spec-derived
// documentation into Go source files.
//
// Generators and tools pass Go source buffers through [Fill] before writing
// them to disk. Comparing generated or hand-written declarations against spec
// ensures that the API implementation and the specification agree on types and
// parameter names, and allows shared documentation to be maintained in a single
// place.
//
// # Doc-Ownership Convention
//
// Doc comments follow a three-part structural convention:
//
// 1. Spec-owned paragraphs: Describe the behavioral semantics of the operation.
// These are managed by specdoc and replaced with documentation from spec when a
// matching spec entry exists.
//
// 2. Implementation notes: Describe target-specific implementation details,
// such as assembly instructions, required CPU features, emulation notes,
// performance caveats, or deprecations. An implementation note is any paragraph
// whose first line begins with one of a fixed set of prefixes: "Asm:", "CPU
// Feature:", "Emulated", "Deprecated:", "Performance:", or "Note:".
//
// 3. Directive comments: Tool and compiler directives, such as //go:noescape.
//
// These sections must appear strictly in this order: spec-owned paragraphs
// strictly before implementation notes, which must appear strictly before
// directive comments.
//
// # Rewriting and Idempotence
//
// When rewriting a declaration, [Fill] replaces existing spec-owned paragraphs
// with the corresponding spec documentation (unless [Options.NoFillDoc] is set)
// and fills unnamed parameters and results with canonical names from spec
// (unless [Options.NoFillNames] is set), while preserving existing
// implementation notes and directives unchanged. Because spec documentation is
// guaranteed never to begin with an implementation note prefix, the rewrite
// operation is idempotent: running [Fill] repeatedly on rewritten source
// produces byte-identical output.
package specdoc
import (
"bytes"
"cmp"
"fmt"
"go/ast"
"go/format"
"go/parser"
"go/token"
"path/filepath"
"slices"
"strings"
"simd/archsimd/_gen/specgen"
)
// Options configures the behavior of [Fill].
type Options struct {
// RejectUnknown controls whether an exported declaration with no matching
// spec entry is reported as an error.
//
// When false, un-specced declarations are tolerated and omitted from
// signature and doc checks, and passed through the rewrite verbatim. When
// true, any exported declaration lacking a spec definition causes
// generation to fail, preventing new API operations from escaping spec
// coverage.
RejectUnknown bool
// AllowDocRewrite controls whether an exported declaration containing
// existing spec-owned doc comments is tolerated.
//
// When false (default), any spec-owned doc comment found in the input is
// reported as an error (recorded in [Report.UnexpectedDocs]). This is used
// by generators to ensure they do not emit doc comments that would be
// overwritten by spec. Implementation notes and directives are still
// permitted.
//
// When true, existing spec-owned doc comments are permitted and rewritten.
// This is used when processing hand-written source files.
AllowDocRewrite bool
// AllowNameMismatches controls whether parameter and result name mismatches
// between an AST declaration and spec are tolerated.
//
// When false (default), name mismatches are reported as errors.
// When true, name mismatches are omitted from the report.
AllowNameMismatches bool
// NoFillDoc disables replacing doc comments with spec-derived comments.
//
// When false, Fill updates spec-owned doc paragraphs.
NoFillDoc bool
// NoFillNames disables filling unnamed parameters and results with names
// from spec.
//
// When false, Fill fills unnamed parameters and results in exported
// declarations from the matching spec function.
NoFillNames bool
// Filename optionally provides the name of the file being processed,
// used for positions in report diagnostics.
Filename string
}
type textEdit struct {
start int
end int
text string
}
// Filler returns a post-processing hook that runs [Fill] on generated Go source files.
// The returned function satisfies [gentools.PostProcessor].
func Filler(idx *specgen.Index, opts Options) func(relPath string, isGo bool, content []byte) ([]byte, error) {
return func(relPath string, isGo bool, src []byte) ([]byte, error) {
if !isGo {
return src, nil
}
dir := filepath.ToSlash(filepath.Dir(relPath))
if dir != "simd" && dir != "simd/archsimd" {
return src, nil
}
fileOpts := opts
fileOpts.Filename = filepath.ToSlash(relPath)
return Fill(src, idx, fileOpts)
}
}
// Fill parses Go source code, extracts exported declarations, verifies their
// signatures and doc comments against spec, and returns the modified source.
// If any verification checks fail, Fill returns a [*Report] error containing
// all findings.
func Fill(src []byte, spec *specgen.Index, opts Options) ([]byte, error) {
fset := token.NewFileSet()
filename := opts.Filename
if filename == "" {
filename = "<unknown>"
}
file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
if err != nil {
return src, fmt.Errorf("parsing %s: %w", filename, err)
}
var report Report
var edits []textEdit
for _, d := range file.Decls {
fd, ok := d.(*ast.FuncDecl)
if !ok || !fd.Name.IsExported() {
continue
}
decl := Decl{
Pos: fset.Position(fd.Pos()),
Recv: recvTypeName(fd.Recv),
Name: fd.Name.Name,
}
// Find the spec function
specFn := spec.Lookup(decl.Recv, decl.Name)
if specFn == nil {
if opts.RejectUnknown {
report.UnknownDecls = append(report.UnknownDecls, UnknownDecl{decl})
}
continue
}
// Now we know this is a spec API function and not some other one-off
// that might not even be representable in spec.
//
// Parse the API declaration into a specgen.Func.
doc := parseCommentGroup(fd.Doc)
if !opts.AllowDocRewrite {
for _, p := range doc {
if p.Kind == specgen.SpecOwned {
report.UnexpectedDocs = append(report.UnexpectedDocs, UnexpectedDoc{decl})
break
}
}
}
var recv specgen.Arg
if fd.Recv != nil && len(fd.Recv.List) == 1 {
recvs, err := fieldListArgs(fd.Recv)
if err != nil {
return src, fmt.Errorf("%s: %s receiver: %w", decl.Pos, decl.Name, err)
}
recv = recvs[0]
}
in, err := fieldListArgs(fd.Type.Params)
if err != nil {
return src, fmt.Errorf("%s: %s params: %w", decl.Pos, decl.Name, err)
}
out, err := fieldListArgs(fd.Type.Results)
if err != nil {
return src, fmt.Errorf("%s: %s results: %w", decl.Pos, decl.Name, err)
}
// Create a skeleton Func for the actual declaration
declFn := &specgen.Func{Doc: doc, Recv: recv, Name: decl.Name, In: in, Out: out}
// Check comment section ordering
var orderErr error
if len(declFn.Doc) > 0 {
if _, err := specgen.FormatComment(doc); err != nil {
orderErr = err
report.DocOrderViolations = append(report.DocOrderViolations, DocOrderViolation{
D: decl,
Err: orderErr,
})
}
}
// Compare function signatures
nameDetails, typeDetails := compareFuncs(declFn, specFn, opts.NoFillNames)
if len(nameDetails) > 0 && !opts.AllowNameMismatches {
report.NameMismatches = append(report.NameMismatches, NameMismatch{
D: decl,
DeclSig: declFn.Signature(),
SpecSig: specFn.Signature(),
Details: nameDetails,
})
}
if len(typeDetails) > 0 {
report.TypeMismatches = append(report.TypeMismatches, TypeMismatch{
D: decl,
DeclSig: declFn.Signature(),
SpecSig: specFn.Signature(),
Details: typeDetails,
})
}
// Compose replacement comment and record text edits
if !opts.NoFillDoc && orderErr == nil {
if edit, ok := rewriteDoc(fset, src, fd, doc, specFn.Doc); ok {
edits = append(edits, edit)
}
}
// Rewrite parameter and result names
if !opts.NoFillNames && len(nameDetails) == 0 && len(typeDetails) == 0 {
if edit, ok := rewriteSignature(fset, src, fd, declFn, specFn); ok {
edits = append(edits, edit)
}
}
}
var repErr error
if !report.Empty() {
repErr = &report
}
if len(edits) == 0 {
return src, repErr
}
rewritten := applyEdits(src, edits)
formatted, err := format.Source(rewritten)
if err != nil {
if repErr != nil {
return rewritten, repErr
}
return rewritten, fmt.Errorf("formatting source: %w", err)
}
return formatted, repErr
}
func namedArg(arg specgen.Arg) bool {
return !(arg.Name == "" || arg.Name == "_")
}
// compareFuncs compares the receiver, parameters, and results of declFn against
// specFn and returns details of any mismatches found.
func compareFuncs(declFn, specFn *specgen.Func, noFillNames bool) (nameDetails, typeDetails []string) {
if (declFn.Recv.Type != nil) != (specFn.Recv.Type != nil) {
// This should never happen because we look up by receiver.
panic("cannot compare function and method")
}
compareArg := func(decl, spec specgen.Arg, label string, args ...any) {
if namedArg(spec) && decl.Name != spec.Name {
if !namedArg(decl) && !noFillNames {
// An unnamed parameter or result is an opportunity to fill
// from spec, not an error, when name filling is active.
} else {
var buf strings.Builder
fmt.Fprintf(&buf, label, args...)
fmt.Fprintf(&buf, ": API name %q != spec name %q", decl.Name, spec.Name)
nameDetails = append(nameDetails, buf.String())
}
}
if decl.Type != spec.Type {
var buf strings.Builder
fmt.Fprintf(&buf, label, args...)
if spec.Name != "" {
fmt.Fprintf(&buf, " (%s)", spec.Name)
}
fmt.Fprintf(&buf, ": API type %s != spec type %s", decl.Type, spec.Type)
typeDetails = append(typeDetails, buf.String())
}
}
compareArgs := func(decl, spec []specgen.Arg, label string) {
if len(decl) != len(spec) {
typeDetails = append(typeDetails, fmt.Sprintf("%s count mismatch: API has %d, spec has %d", label, len(decl), len(spec)))
return
}
for i := range decl {
compareArg(decl[i], spec[i], "%s %d", label, i)
}
}
if specFn.Recv.Type != nil {
compareArg(declFn.Recv, specFn.Recv, "receiver")
}
compareArgs(declFn.In, specFn.In, "parameter")
compareArgs(declFn.Out, specFn.Out, "result")
return nameDetails, typeDetails
}
// recvTypeName extracts the receiver type of a method. It returns "" for
// package-level functions (nil or empty FieldList).
func recvTypeName(fl *ast.FieldList) string {
if fl == nil || len(fl.List) == 0 {
return ""
}
t := fl.List[0].Type
for {
switch tt := t.(type) {
case *ast.Ident:
return tt.Name
case *ast.StarExpr:
t = tt.X
case *ast.ParenExpr:
t = tt.X
case *ast.IndexExpr:
t = tt.X
case *ast.IndexListExpr:
t = tt.X
default:
return "<unknown>"
}
}
}
// rewriteDoc computes the textEdit to replace fd's doc comment with
// the formatted combination of spec-owned doc paragraphs and existing
// implementation notes and directives.
func rewriteDoc(fset *token.FileSet, src []byte, fd *ast.FuncDecl, declDoc, specDoc []specgen.Paragraph) (textEdit, bool) {
newParas := append([]specgen.Paragraph(nil), specDoc...)
for _, p := range declDoc {
if p.Kind == specgen.ImplementationNote || p.Kind == specgen.DirectiveComment {
newParas = append(newParas, p)
}
}
replacementComment, err := specgen.FormatComment(newParas)
if err != nil {
return textEdit{}, false
}
if fd.Doc != nil {
// Replace existing doc.
start := fset.Position(fd.Doc.Pos()).Offset
end := fset.Position(fd.Doc.End()).Offset + 1 // Include final \n
if string(src[start:end]) != replacementComment {
return textEdit{start: start, end: end, text: replacementComment}, true
}
} else if replacementComment != "" {
// Pure insertion.
start := fset.Position(fd.Pos()).Offset
return textEdit{start: start, end: start, text: replacementComment}, true
}
return textEdit{}, false
}
// rewriteSignature computes the textEdit to fill unnamed parameter and result
// names in fd.
func rewriteSignature(fset *token.FileSet, src []byte, fd *ast.FuncDecl, declFn, specFn *specgen.Func) (textEdit, bool) {
// Create a synthetic specgen.Func with merged arguments.
mergedFn := &specgen.Func{
Recv: declFn.Recv,
Name: declFn.Name,
In: mergeArgs(declFn.In, specFn.In),
Out: mergeArgs(declFn.Out, specFn.Out),
}
newSig := mergedFn.Signature()
start := fset.Position(fd.Pos()).Offset
end := fset.Position(fd.Type.End()).Offset
if string(src[start:end]) != newSig {
return textEdit{start: start, end: end, text: newSig}, true
}
return textEdit{}, false
}
func mergeArgs(declArgs, specArgs []specgen.Arg) []specgen.Arg {
merged := slices.Clone(declArgs)
// Fill any unnamed arguments from spec
for i := range merged {
if namedArg(specArgs[i]) && !namedArg(merged[i]) {
merged[i].Name = specArgs[i].Name
}
}
return merged
}
// applyEdits applies text edits to src in ascending offset order and returns
// the modified byte slice.
func applyEdits(src []byte, edits []textEdit) []byte {
if len(edits) == 0 {
return src
}
// Apply edits in ascending offset order
slices.SortFunc(edits, func(a, b textEdit) int {
return cmp.Compare(a.start, b.start)
})
var out bytes.Buffer
last := 0
for _, e := range edits {
out.Write(src[last:e.start])
out.WriteString(e.text)
last = e.end
}
out.Write(src[last:])
return out.Bytes()
}