Source file src/simd/archsimd/_gen/specdoc/fill.go

     1  // Copyright 2026 The Go Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Package specdoc validates exported SIMD declarations against package
     6  // simd/internal/spec and manages the composition and injection of spec-derived
     7  // documentation into Go source files.
     8  //
     9  // Generators and tools pass Go source buffers through [Fill] before writing
    10  // them to disk. Comparing generated or hand-written declarations against spec
    11  // ensures that the API implementation and the specification agree on types and
    12  // parameter names, and allows shared documentation to be maintained in a single
    13  // place.
    14  //
    15  // # Doc-Ownership Convention
    16  //
    17  // Doc comments follow a three-part structural convention:
    18  //
    19  // 1. Spec-owned paragraphs: Describe the behavioral semantics of the operation.
    20  // These are managed by specdoc and replaced with documentation from spec when a
    21  // matching spec entry exists.
    22  //
    23  // 2. Implementation notes: Describe target-specific implementation details,
    24  // such as assembly instructions, required CPU features, emulation notes,
    25  // performance caveats, or deprecations. An implementation note is any paragraph
    26  // whose first line begins with one of a fixed set of prefixes: "Asm:", "CPU
    27  // Feature:", "Emulated", "Deprecated:", "Performance:", or "Note:".
    28  //
    29  // 3. Directive comments: Tool and compiler directives, such as //go:noescape.
    30  //
    31  // These sections must appear strictly in this order: spec-owned paragraphs
    32  // strictly before implementation notes, which must appear strictly before
    33  // directive comments.
    34  //
    35  // # Rewriting and Idempotence
    36  //
    37  // When rewriting a declaration, [Fill] replaces existing spec-owned paragraphs
    38  // with the corresponding spec documentation (unless [Options.NoFillDoc] is set)
    39  // and fills unnamed parameters and results with canonical names from spec
    40  // (unless [Options.NoFillNames] is set), while preserving existing
    41  // implementation notes and directives unchanged. Because spec documentation is
    42  // guaranteed never to begin with an implementation note prefix, the rewrite
    43  // operation is idempotent: running [Fill] repeatedly on rewritten source
    44  // produces byte-identical output.
    45  package specdoc
    46  
    47  import (
    48  	"bytes"
    49  	"cmp"
    50  	"fmt"
    51  	"go/ast"
    52  	"go/format"
    53  	"go/parser"
    54  	"go/token"
    55  	"path/filepath"
    56  	"slices"
    57  	"strings"
    58  
    59  	"simd/archsimd/_gen/specgen"
    60  )
    61  
    62  // Options configures the behavior of [Fill].
    63  type Options struct {
    64  	// RejectUnknown controls whether an exported declaration with no matching
    65  	// spec entry is reported as an error.
    66  	//
    67  	// When false, un-specced declarations are tolerated and omitted from
    68  	// signature and doc checks, and passed through the rewrite verbatim. When
    69  	// true, any exported declaration lacking a spec definition causes
    70  	// generation to fail, preventing new API operations from escaping spec
    71  	// coverage.
    72  	RejectUnknown bool
    73  
    74  	// AllowDocRewrite controls whether an exported declaration containing
    75  	// existing spec-owned doc comments is tolerated.
    76  	//
    77  	// When false (default), any spec-owned doc comment found in the input is
    78  	// reported as an error (recorded in [Report.UnexpectedDocs]). This is used
    79  	// by generators to ensure they do not emit doc comments that would be
    80  	// overwritten by spec. Implementation notes and directives are still
    81  	// permitted.
    82  	//
    83  	// When true, existing spec-owned doc comments are permitted and rewritten.
    84  	// This is used when processing hand-written source files.
    85  	AllowDocRewrite bool
    86  
    87  	// AllowNameMismatches controls whether parameter and result name mismatches
    88  	// between an AST declaration and spec are tolerated.
    89  	//
    90  	// When false (default), name mismatches are reported as errors.
    91  	// When true, name mismatches are omitted from the report.
    92  	AllowNameMismatches bool
    93  
    94  	// NoFillDoc disables replacing doc comments with spec-derived comments.
    95  	//
    96  	// When false, Fill updates spec-owned doc paragraphs.
    97  	NoFillDoc bool
    98  
    99  	// NoFillNames disables filling unnamed parameters and results with names
   100  	// from spec.
   101  	//
   102  	// When false, Fill fills unnamed parameters and results in exported
   103  	// declarations from the matching spec function.
   104  	NoFillNames bool
   105  
   106  	// Filename optionally provides the name of the file being processed,
   107  	// used for positions in report diagnostics.
   108  	Filename string
   109  }
   110  
   111  type textEdit struct {
   112  	start int
   113  	end   int
   114  	text  string
   115  }
   116  
   117  // Filler returns a post-processing hook that runs [Fill] on generated Go source files.
   118  // The returned function satisfies [gentools.PostProcessor].
   119  func Filler(idx *specgen.Index, opts Options) func(relPath string, isGo bool, content []byte) ([]byte, error) {
   120  	return func(relPath string, isGo bool, src []byte) ([]byte, error) {
   121  		if !isGo {
   122  			return src, nil
   123  		}
   124  		dir := filepath.ToSlash(filepath.Dir(relPath))
   125  		if dir != "simd" && dir != "simd/archsimd" {
   126  			return src, nil
   127  		}
   128  		fileOpts := opts
   129  		fileOpts.Filename = filepath.ToSlash(relPath)
   130  		return Fill(src, idx, fileOpts)
   131  	}
   132  }
   133  
   134  // Fill parses Go source code, extracts exported declarations, verifies their
   135  // signatures and doc comments against spec, and returns the modified source.
   136  // If any verification checks fail, Fill returns a [*Report] error containing
   137  // all findings.
   138  func Fill(src []byte, spec *specgen.Index, opts Options) ([]byte, error) {
   139  	fset := token.NewFileSet()
   140  	filename := opts.Filename
   141  	if filename == "" {
   142  		filename = "<unknown>"
   143  	}
   144  	file, err := parser.ParseFile(fset, filename, src, parser.ParseComments)
   145  	if err != nil {
   146  		return src, fmt.Errorf("parsing %s: %w", filename, err)
   147  	}
   148  
   149  	var report Report
   150  	var edits []textEdit
   151  
   152  	for _, d := range file.Decls {
   153  		fd, ok := d.(*ast.FuncDecl)
   154  		if !ok || !fd.Name.IsExported() {
   155  			continue
   156  		}
   157  
   158  		decl := Decl{
   159  			Pos:  fset.Position(fd.Pos()),
   160  			Recv: recvTypeName(fd.Recv),
   161  			Name: fd.Name.Name,
   162  		}
   163  
   164  		// Find the spec function
   165  		specFn := spec.Lookup(decl.Recv, decl.Name)
   166  		if specFn == nil {
   167  			if opts.RejectUnknown {
   168  				report.UnknownDecls = append(report.UnknownDecls, UnknownDecl{decl})
   169  			}
   170  			continue
   171  		}
   172  
   173  		// Now we know this is a spec API function and not some other one-off
   174  		// that might not even be representable in spec.
   175  		//
   176  		// Parse the API declaration into a specgen.Func.
   177  
   178  		doc := parseCommentGroup(fd.Doc)
   179  		if !opts.AllowDocRewrite {
   180  			for _, p := range doc {
   181  				if p.Kind == specgen.SpecOwned {
   182  					report.UnexpectedDocs = append(report.UnexpectedDocs, UnexpectedDoc{decl})
   183  					break
   184  				}
   185  			}
   186  		}
   187  
   188  		var recv specgen.Arg
   189  		if fd.Recv != nil && len(fd.Recv.List) == 1 {
   190  			recvs, err := fieldListArgs(fd.Recv)
   191  			if err != nil {
   192  				return src, fmt.Errorf("%s: %s receiver: %w", decl.Pos, decl.Name, err)
   193  			}
   194  			recv = recvs[0]
   195  		}
   196  
   197  		in, err := fieldListArgs(fd.Type.Params)
   198  		if err != nil {
   199  			return src, fmt.Errorf("%s: %s params: %w", decl.Pos, decl.Name, err)
   200  		}
   201  		out, err := fieldListArgs(fd.Type.Results)
   202  		if err != nil {
   203  			return src, fmt.Errorf("%s: %s results: %w", decl.Pos, decl.Name, err)
   204  		}
   205  
   206  		// Create a skeleton Func for the actual declaration
   207  		declFn := &specgen.Func{Doc: doc, Recv: recv, Name: decl.Name, In: in, Out: out}
   208  
   209  		// Check comment section ordering
   210  		var orderErr error
   211  		if len(declFn.Doc) > 0 {
   212  			if _, err := specgen.FormatComment(doc); err != nil {
   213  				orderErr = err
   214  				report.DocOrderViolations = append(report.DocOrderViolations, DocOrderViolation{
   215  					D:   decl,
   216  					Err: orderErr,
   217  				})
   218  			}
   219  		}
   220  
   221  		// Compare function signatures
   222  		nameDetails, typeDetails := compareFuncs(declFn, specFn, opts.NoFillNames)
   223  		if len(nameDetails) > 0 && !opts.AllowNameMismatches {
   224  			report.NameMismatches = append(report.NameMismatches, NameMismatch{
   225  				D:       decl,
   226  				DeclSig: declFn.Signature(),
   227  				SpecSig: specFn.Signature(),
   228  				Details: nameDetails,
   229  			})
   230  		}
   231  		if len(typeDetails) > 0 {
   232  			report.TypeMismatches = append(report.TypeMismatches, TypeMismatch{
   233  				D:       decl,
   234  				DeclSig: declFn.Signature(),
   235  				SpecSig: specFn.Signature(),
   236  				Details: typeDetails,
   237  			})
   238  		}
   239  
   240  		// Compose replacement comment and record text edits
   241  		if !opts.NoFillDoc && orderErr == nil {
   242  			if edit, ok := rewriteDoc(fset, src, fd, doc, specFn.Doc); ok {
   243  				edits = append(edits, edit)
   244  			}
   245  		}
   246  
   247  		// Rewrite parameter and result names
   248  		if !opts.NoFillNames && len(nameDetails) == 0 && len(typeDetails) == 0 {
   249  			if edit, ok := rewriteSignature(fset, src, fd, declFn, specFn); ok {
   250  				edits = append(edits, edit)
   251  			}
   252  		}
   253  	}
   254  
   255  	var repErr error
   256  	if !report.Empty() {
   257  		repErr = &report
   258  	}
   259  
   260  	if len(edits) == 0 {
   261  		return src, repErr
   262  	}
   263  
   264  	rewritten := applyEdits(src, edits)
   265  	formatted, err := format.Source(rewritten)
   266  	if err != nil {
   267  		if repErr != nil {
   268  			return rewritten, repErr
   269  		}
   270  		return rewritten, fmt.Errorf("formatting source: %w", err)
   271  	}
   272  	return formatted, repErr
   273  }
   274  
   275  func namedArg(arg specgen.Arg) bool {
   276  	return !(arg.Name == "" || arg.Name == "_")
   277  }
   278  
   279  // compareFuncs compares the receiver, parameters, and results of declFn against
   280  // specFn and returns details of any mismatches found.
   281  func compareFuncs(declFn, specFn *specgen.Func, noFillNames bool) (nameDetails, typeDetails []string) {
   282  	if (declFn.Recv.Type != nil) != (specFn.Recv.Type != nil) {
   283  		// This should never happen because we look up by receiver.
   284  		panic("cannot compare function and method")
   285  	}
   286  
   287  	compareArg := func(decl, spec specgen.Arg, label string, args ...any) {
   288  		if namedArg(spec) && decl.Name != spec.Name {
   289  			if !namedArg(decl) && !noFillNames {
   290  				// An unnamed parameter or result is an opportunity to fill
   291  				// from spec, not an error, when name filling is active.
   292  			} else {
   293  				var buf strings.Builder
   294  				fmt.Fprintf(&buf, label, args...)
   295  				fmt.Fprintf(&buf, ": API name %q != spec name %q", decl.Name, spec.Name)
   296  				nameDetails = append(nameDetails, buf.String())
   297  			}
   298  		}
   299  		if decl.Type != spec.Type {
   300  			var buf strings.Builder
   301  			fmt.Fprintf(&buf, label, args...)
   302  			if spec.Name != "" {
   303  				fmt.Fprintf(&buf, " (%s)", spec.Name)
   304  			}
   305  			fmt.Fprintf(&buf, ": API type %s != spec type %s", decl.Type, spec.Type)
   306  			typeDetails = append(typeDetails, buf.String())
   307  		}
   308  	}
   309  	compareArgs := func(decl, spec []specgen.Arg, label string) {
   310  		if len(decl) != len(spec) {
   311  			typeDetails = append(typeDetails, fmt.Sprintf("%s count mismatch: API has %d, spec has %d", label, len(decl), len(spec)))
   312  			return
   313  		}
   314  		for i := range decl {
   315  			compareArg(decl[i], spec[i], "%s %d", label, i)
   316  		}
   317  	}
   318  
   319  	if specFn.Recv.Type != nil {
   320  		compareArg(declFn.Recv, specFn.Recv, "receiver")
   321  	}
   322  	compareArgs(declFn.In, specFn.In, "parameter")
   323  	compareArgs(declFn.Out, specFn.Out, "result")
   324  
   325  	return nameDetails, typeDetails
   326  }
   327  
   328  // recvTypeName extracts the receiver type of a method. It returns "" for
   329  // package-level functions (nil or empty FieldList).
   330  func recvTypeName(fl *ast.FieldList) string {
   331  	if fl == nil || len(fl.List) == 0 {
   332  		return ""
   333  	}
   334  	t := fl.List[0].Type
   335  	for {
   336  		switch tt := t.(type) {
   337  		case *ast.Ident:
   338  			return tt.Name
   339  
   340  		case *ast.StarExpr:
   341  			t = tt.X
   342  		case *ast.ParenExpr:
   343  			t = tt.X
   344  		case *ast.IndexExpr:
   345  			t = tt.X
   346  		case *ast.IndexListExpr:
   347  			t = tt.X
   348  
   349  		default:
   350  			return "<unknown>"
   351  		}
   352  	}
   353  }
   354  
   355  // rewriteDoc computes the textEdit to replace fd's doc comment with
   356  // the formatted combination of spec-owned doc paragraphs and existing
   357  // implementation notes and directives.
   358  func rewriteDoc(fset *token.FileSet, src []byte, fd *ast.FuncDecl, declDoc, specDoc []specgen.Paragraph) (textEdit, bool) {
   359  	newParas := append([]specgen.Paragraph(nil), specDoc...)
   360  	for _, p := range declDoc {
   361  		if p.Kind == specgen.ImplementationNote || p.Kind == specgen.DirectiveComment {
   362  			newParas = append(newParas, p)
   363  		}
   364  	}
   365  
   366  	replacementComment, err := specgen.FormatComment(newParas)
   367  	if err != nil {
   368  		return textEdit{}, false
   369  	}
   370  
   371  	if fd.Doc != nil {
   372  		// Replace existing doc.
   373  		start := fset.Position(fd.Doc.Pos()).Offset
   374  		end := fset.Position(fd.Doc.End()).Offset + 1 // Include final \n
   375  		if string(src[start:end]) != replacementComment {
   376  			return textEdit{start: start, end: end, text: replacementComment}, true
   377  		}
   378  	} else if replacementComment != "" {
   379  		// Pure insertion.
   380  		start := fset.Position(fd.Pos()).Offset
   381  		return textEdit{start: start, end: start, text: replacementComment}, true
   382  	}
   383  
   384  	return textEdit{}, false
   385  }
   386  
   387  // rewriteSignature computes the textEdit to fill unnamed parameter and result
   388  // names in fd.
   389  func rewriteSignature(fset *token.FileSet, src []byte, fd *ast.FuncDecl, declFn, specFn *specgen.Func) (textEdit, bool) {
   390  	// Create a synthetic specgen.Func with merged arguments.
   391  	mergedFn := &specgen.Func{
   392  		Recv: declFn.Recv,
   393  		Name: declFn.Name,
   394  		In:   mergeArgs(declFn.In, specFn.In),
   395  		Out:  mergeArgs(declFn.Out, specFn.Out),
   396  	}
   397  	newSig := mergedFn.Signature()
   398  	start := fset.Position(fd.Pos()).Offset
   399  	end := fset.Position(fd.Type.End()).Offset
   400  	if string(src[start:end]) != newSig {
   401  		return textEdit{start: start, end: end, text: newSig}, true
   402  	}
   403  	return textEdit{}, false
   404  }
   405  
   406  func mergeArgs(declArgs, specArgs []specgen.Arg) []specgen.Arg {
   407  	merged := slices.Clone(declArgs)
   408  	// Fill any unnamed arguments from spec
   409  	for i := range merged {
   410  		if namedArg(specArgs[i]) && !namedArg(merged[i]) {
   411  			merged[i].Name = specArgs[i].Name
   412  		}
   413  	}
   414  	return merged
   415  }
   416  
   417  // applyEdits applies text edits to src in ascending offset order and returns
   418  // the modified byte slice.
   419  func applyEdits(src []byte, edits []textEdit) []byte {
   420  	if len(edits) == 0 {
   421  		return src
   422  	}
   423  
   424  	// Apply edits in ascending offset order
   425  	slices.SortFunc(edits, func(a, b textEdit) int {
   426  		return cmp.Compare(a.start, b.start)
   427  	})
   428  
   429  	var out bytes.Buffer
   430  	last := 0
   431  	for _, e := range edits {
   432  		out.Write(src[last:e.start])
   433  		out.WriteString(e.text)
   434  		last = e.end
   435  	}
   436  	out.Write(src[last:])
   437  	return out.Bytes()
   438  }
   439  

View as plain text