blob: 5fddc814892fc12290528d200e8afdf347b91ddb [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.
// specfill checks the public SIMD API against internal/spec and reports
// signature mismatches, comment structure violations, and documentation diffs.
package main
import (
"bytes"
"errors"
"flag"
"fmt"
"os"
"os/exec"
"path/filepath"
"simd/archsimd/_gen/gentools"
"simd/archsimd/_gen/specdoc"
"simd/archsimd/_gen/specgen"
"strings"
)
func main() {
genOpts := gentools.RegisterFlags(nil)
noFillDoc := flag.Bool("no-fill-doc", false, "disable filling doc comments from spec")
noFillNames := flag.Bool("no-fill-names", false, "disable filling parameter and result names from spec")
dirs := []string{
filepath.Join(genOpts.GOROOT, "src/simd"),
filepath.Join(genOpts.GOROOT, "src/simd/archsimd"),
}
flag.Usage = func() {
w := flag.CommandLine.Output()
fmt.Fprintf(w, "usage: specfill [flags] [files...]\n\n")
fmt.Fprintf(w, "If files are not given, specfill applies to all non-test, non-generated files in:\n")
for _, d := range dirs {
fmt.Fprintf(w, " %s\n", d)
}
fmt.Fprintln(w)
flag.CommandLine.PrintDefaults()
}
flag.Parse()
specDir := specgen.MustFindSpecDir(genOpts.GOROOT)
specFuncs, err := specgen.Load(specDir, nil)
if err != nil {
fmt.Fprintf(os.Stderr, "loading spec: %v\n", err)
os.Exit(1)
}
specIdx := specgen.NewIndex(specFuncs)
var targetFiles []string
filesArgs := false
if flag.NArg() > 0 {
filesArgs = true
for _, arg := range flag.Args() {
p, err := filepath.Abs(arg)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", arg, err)
os.Exit(1)
}
targetFiles = append(targetFiles, p)
}
} else {
for _, dir := range dirs {
entries, err := os.ReadDir(dir)
if err != nil {
fmt.Fprintf(os.Stderr, "reading %s: %v\n", dir, err)
os.Exit(1)
}
for _, e := range entries {
if e.IsDir() || !strings.HasSuffix(e.Name(), ".go") || strings.HasSuffix(e.Name(), "_test.go") {
continue
}
targetFiles = append(targetFiles, filepath.Join(dir, e.Name()))
}
}
}
var files gentools.Files
defer files.FlushOrExit()
var combinedReport specdoc.Report
for _, filePath := range targetFiles {
relPath, err := filepath.Rel(genOpts.GOROOT, filePath)
if err != nil {
relPath = filePath
}
srcRelPath, err := filepath.Rel(filepath.Join(genOpts.GOROOT, "src"), filePath)
if err != nil {
srcRelPath = strings.TrimPrefix(filepath.ToSlash(relPath), "src/")
}
src, err := os.ReadFile(filePath)
if err != nil {
fmt.Fprintf(os.Stderr, "reading %s: %v\n", filePath, err)
os.Exit(1)
}
gen, err := isGenerated(src)
if err != nil {
fmt.Fprintf(os.Stderr, "%s: %v\n", filePath, err)
os.Exit(1)
}
if gen {
if filesArgs {
fmt.Fprintf(os.Stderr, "%s: file is generated\n", filePath)
os.Exit(1)
}
continue
}
rewritten, err := specdoc.Fill(src, specIdx, specdoc.Options{
Filename: filepath.ToSlash(relPath),
AllowDocRewrite: true,
NoFillDoc: *noFillDoc,
NoFillNames: *noFillNames,
})
var rep *specdoc.Report
if errors.As(err, &rep) {
combinedReport.Merge(rep)
} else if err != nil {
fmt.Fprintf(os.Stderr, "checking %s: %v\n", filePath, err)
os.Exit(1)
}
if rewritten != nil && !bytes.Equal(src, rewritten) {
buf := files.NewGoFile(filepath.ToSlash(srcRelPath))
buf.Write(rewritten)
}
}
combinedReport.Print(os.Stderr)
if genOpts.Write {
if err := checkCleanWorkingCopy(genOpts.GOROOT); err != nil {
fmt.Fprintf(os.Stderr, "refusing to write: %v\n", err)
os.Exit(1)
}
if !combinedReport.Empty() {
fmt.Fprintln(os.Stderr, "errors reported; not writing files")
os.Exit(1)
}
}
}
// checkCleanWorkingCopy verifies that the working directory has no uncommitted
// changes before specfill rewrites files in place (-w).
//
// We rely on "git status --porcelain" rather than invoking "jj" commands directly.
// In a standard Git repository or a colocated jj/git repository, "git status"
// checks disk cleanliness without side-effects.
//
// # Interoperation with colocated jj repos
//
// Jujutsu keeps Git's HEAD and index in sync with the parent of the working-copy
// commit (@-). If the user starts from a fresh, empty commit (e.g. via "jj new"),
// the working copy matches Git's HEAD and git status reports clean. If there are
// modified or untracked files on disk (or if @ contains changes), git status
// reports them as uncommitted changes. By querying git rather than running "jj"
// commands (like "jj diff" or "jj status"), we avoid jj's snapshotting side-effects,
// such as automatically snapshotting dirty files into @, writing to the operation
// log, and auto-tracking newly created files.
func checkCleanWorkingCopy(dir string) error {
cmd := exec.Command("git", "status", "--porcelain")
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
// Not a git repository or git binary not available; skip the check.
return nil
}
if len(bytes.TrimSpace(out)) > 0 {
return fmt.Errorf("working copy has uncommitted changes:\n%s", out)
}
return nil
}
// isGenerated reports whether src is a generated Go file.
//
// It checks for the standard "// Code generated by" comment marker at the beginning
// of the file or at the beginning of any line. If missing, it checks for
// "DO NOT EDIT" as a backstop; if found, it returns an error to catch non-standard
// or malformed generator headers rather than erroneously treating the file as hand-written.
func isGenerated(src []byte) (bool, error) {
const marker = "// Code generated by"
if bytes.HasPrefix(src, []byte(marker)) || bytes.Contains(src, []byte("\n"+marker)) {
return true, nil
}
if bytes.Contains(src, []byte("DO NOT EDIT")) {
return false, errors.New("file contains 'DO NOT EDIT' but lacks '// Code generated by' header")
}
return false, nil
}