| // 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() | |
| } |