805 lines
27 KiB
Go
805 lines
27 KiB
Go
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
|
// SPDX-License-Identifier: BSD-3-Clause
|
|
|
|
package main
|
|
|
|
import (
|
|
"fmt"
|
|
"maps"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"slices"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
|
|
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
|
|
)
|
|
|
|
// cmdAuditInstructions cross-checks a gasm encoder against the Go toolchain's
|
|
// own assembler, probed black-box: every mnemonic in the gasm table is offered
|
|
// to go tool asm in its bare form, and a mnemonic counts as known to Go when
|
|
// the error is anything but "unrecognized instruction" (a wrong-shape error
|
|
// still proves the mnemonic exists in Go's tables). The audit answers three
|
|
// questions at a glance:
|
|
//
|
|
// - which mnemonics gasm can encode that go tool asm does not know
|
|
// (superset encodings, usable only through the gasm goobj path);
|
|
// - which mnemonics the architecture table knows but the encoder cannot
|
|
// emit yet (the implementation backlog);
|
|
// - which mnemonics go tool asm knows that gasm cannot encode (feature
|
|
// gaps).
|
|
//
|
|
// The amd64 derived families (Jcc, CMOVcc, SETcc) exist on both sides by
|
|
// construction and are excluded from the diff; the other architectures list
|
|
// their conditional branches outright.
|
|
func cmdAuditInstructions(args []string) error {
|
|
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [dir]] [--list] [-I dir] [amd64|arm64|riscv64|loong64]", `
|
|
Compare the gasm encoder for the given architecture (default amd64) against
|
|
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
|
gasm asm --format goobj) and known-but-unencodable names (the backlog). The
|
|
Go side is probed black-box one bare mnemonic at a time, so the audit tracks
|
|
whatever toolchain `+"`go env GOROOT`"+` provides; the gasm side answers from
|
|
the encoder table on amd64 and from trial assembly over a battery of operand
|
|
shapes elsewhere. Names go tool asm knows and gasm does not cannot be
|
|
enumerated by probing, because Go's table is visible only through names
|
|
already in the gasm table; the report closes with a note saying so.
|
|
|
|
With --corpus the audit changes shape: it assembles every .s file under the
|
|
given directory (default GOROOT/src) with the gasm encoder only, no
|
|
toolchain probing. A file whose name carries a recognisable _arch suffix is
|
|
attempted for that architecture; a file without one is attempted for all
|
|
four, exactly as a GOARCH build would compile it. The report gives the
|
|
per-architecture pass rates and the most common failure reasons, which drive
|
|
the encodability backlog by frequency rather than by table order. With
|
|
-list the report also prints every failing file with its reason, per
|
|
architecture.
|
|
`)
|
|
corpus := fs.Bool("corpus", false, "assemble a corpus of .s files and report pass rates and failure reasons")
|
|
list := fs.Bool("list", false, "with --corpus, list every failing file with its reason, per architecture")
|
|
var dirs includeDirs
|
|
fs.Var(&dirs, "I", "directory to search for #include files (may be repeated)")
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
if *corpus {
|
|
return cmdAuditCorpus(fs.Args(), dirs, *list)
|
|
}
|
|
archName := "amd64"
|
|
switch n := len(fs.Args()); {
|
|
case n > 1:
|
|
return &usageError{fmt.Errorf("audit-instructions takes at most one architecture argument")}
|
|
case n == 1:
|
|
archName = strings.ToLower(fs.Arg(0))
|
|
}
|
|
a, err := auditArch(archName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
tab := arch.ForArch(a)
|
|
var names []string
|
|
seen := map[string]bool{}
|
|
for _, in := range tab.Instructions() {
|
|
name := strings.ToUpper(in.Name)
|
|
if a == arch.AMD64 && derivedFamily(name) || seen[name] {
|
|
continue
|
|
}
|
|
seen[name] = true
|
|
names = append(names, name)
|
|
}
|
|
|
|
goKnown, err := probeGoAsm(goarchName(a), names)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var superset, backlog, shared []string
|
|
for _, name := range names {
|
|
switch {
|
|
case !gasmEncodable(a, name):
|
|
backlog = append(backlog, name)
|
|
case !goKnown[name]:
|
|
superset = append(superset, name)
|
|
default:
|
|
shared = append(shared, name)
|
|
}
|
|
}
|
|
// GO-ONLY is not enumerable by probing: Go's table is only visible
|
|
// through names we already know, so nothing can be reported there.
|
|
|
|
slices.Sort(superset)
|
|
slices.Sort(backlog)
|
|
slices.Sort(shared)
|
|
|
|
w := os.Stdout
|
|
fmt.Fprintf(w, "gasm table (%s, families excluded): %d mnemonics\n", archName, len(names))
|
|
fmt.Fprintf(w, "gasm encodable: %d go tool asm recognised: %d\n", len(shared)+len(superset), countTrue(goKnown))
|
|
fmt.Fprintf(w, "shared: %d\n", len(shared))
|
|
fmt.Fprintf(w, "\nSuperset encodings (gasm-only; ship via gasm asm --format goobj):\n")
|
|
for _, n := range superset {
|
|
fmt.Fprintf(w, " %s\n", n)
|
|
}
|
|
fmt.Fprintf(w, "\nKnown but not encodable (backlog):\n")
|
|
for _, n := range backlog {
|
|
fmt.Fprintf(w, " %s\n", n)
|
|
}
|
|
fmt.Fprintf(w, "\nGo-only names cannot be enumerated by probing; extend the gasm\n")
|
|
fmt.Fprintf(w, "table from the Go release notes when a new instruction family ships.\n")
|
|
return nil
|
|
}
|
|
|
|
// auditArch resolves the audit's architecture argument.
|
|
func auditArch(name string) (arch.Arch, error) {
|
|
switch strings.ToLower(name) {
|
|
case "amd64":
|
|
return arch.AMD64, nil
|
|
case "arm64":
|
|
return arch.ARM64, nil
|
|
case "riscv64", "riscv":
|
|
return arch.RISCV, nil
|
|
case "loong64", "loong":
|
|
return arch.LOONG64, nil
|
|
}
|
|
return arch.Unknown, &usageError{fmt.Errorf("unknown architecture %q: want amd64, arm64, riscv64 or loong64", name)}
|
|
}
|
|
|
|
// goarchName maps an arch identifier onto its GOARCH spelling.
|
|
func goarchName(a arch.Arch) string {
|
|
switch a {
|
|
case arch.ARM64:
|
|
return "arm64"
|
|
case arch.RISCV:
|
|
return "riscv64"
|
|
case arch.LOONG64:
|
|
return "loong64"
|
|
}
|
|
return "amd64"
|
|
}
|
|
|
|
func countTrue(m map[string]bool) int {
|
|
n := 0
|
|
for _, v := range m {
|
|
if v {
|
|
n++
|
|
}
|
|
}
|
|
return n
|
|
}
|
|
|
|
// derivedFamily reports whether a mnemonic belongs to a family both
|
|
// assemblers construct from condition codes rather than list exhaustively
|
|
// (JEQ/CMOVLGT/SETNE and friends). Such names never probe cleanly, so
|
|
// including them in the diff would be noise. amd64 only: the other
|
|
// architectures list their conditional branches outright.
|
|
func derivedFamily(name string) bool {
|
|
if strings.HasPrefix(name, "J") && name != "JMP" && name != "JMPQ" {
|
|
return true
|
|
}
|
|
if strings.HasPrefix(name, "CMOV") || strings.HasPrefix(name, "SET") {
|
|
return true
|
|
}
|
|
return false
|
|
}
|
|
|
|
var unrecognizedRe = regexp.MustCompile(`unrecognized instruction`)
|
|
|
|
// probeGoAsm feeds every mnemonic to go tool asm in one generated file and
|
|
// classifies the diagnostics. "Unrecognized instruction" is a parse-stage
|
|
// verdict on the mnemonic alone, so a single bare-instruction probe per
|
|
// mnemonic decides recognition; the combined file still reports every line's
|
|
// error even when others fail.
|
|
func probeGoAsm(goarch string, names []string) (map[string]bool, error) {
|
|
dir, err := os.MkdirTemp("", "gasm-audit")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer os.RemoveAll(dir)
|
|
|
|
var sb strings.Builder
|
|
sb.WriteString("TEXT ·probe(SB), 4, $0\n\tRET\n")
|
|
lineMnemonic := map[int]string{}
|
|
line := 3
|
|
for _, name := range names {
|
|
fmt.Fprintf(&sb, "TEXT ·p%s%d(SB), 4, $0\n", sanitize(name), line)
|
|
sb.WriteString("\t" + name + "\n\tRET\n")
|
|
lineMnemonic[line+1] = name // the instruction line, after TEXT
|
|
line += 3
|
|
}
|
|
probePath := filepath.Join(dir, "probe.s")
|
|
if err := os.WriteFile(probePath, []byte(sb.String()), 0o644); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
toolDir, err := exec.Command("go", "env", "GOTOOLDIR").Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("go env GOTOOLDIR: %w", err)
|
|
}
|
|
asmBin := filepath.Join(strings.TrimSpace(string(toolDir)), "asm")
|
|
if _, err := os.Stat(asmBin); err != nil {
|
|
return nil, fmt.Errorf("go tool asm not found at %s", asmBin)
|
|
}
|
|
cmd := exec.Command(asmBin, "-p", "probe", "-o", filepath.Join(dir, "probe.o"), probePath)
|
|
cmd.Env = append(os.Environ(), "GOARCH="+goarch, "GOOS="+runtime.GOOS)
|
|
out, _ := cmd.CombinedOutput()
|
|
// The expected failure mode is a non-zero exit with compiler diagnostics
|
|
// on stdout; empty output means the probe broke at the exec level (a
|
|
// killed child, a tool that would not start), and seeding every name as
|
|
// recognized on that silence would fake a clean audit.
|
|
if len(out) == 0 {
|
|
return nil, fmt.Errorf("go tool asm probe for GOARCH=%s produced no output", goarch)
|
|
}
|
|
|
|
result := map[string]bool{}
|
|
for _, name := range names {
|
|
result[name] = true // no news = the name parsed fine
|
|
}
|
|
reParse := regexp.MustCompile(`probe\.s:(\d+):`)
|
|
for l := range strings.SplitSeq(string(out), "\n") {
|
|
m := reParse.FindStringSubmatch(l)
|
|
if m == nil {
|
|
continue
|
|
}
|
|
lineNo, err := strconv.Atoi(m[1])
|
|
if err != nil {
|
|
continue
|
|
}
|
|
if name, ok := lineMnemonic[lineNo]; ok && unrecognizedRe.MatchString(l) {
|
|
result[name] = false
|
|
}
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
// probeShapes lists representative operand shapes for the encodability
|
|
// probe. The assemblers report an unknown mnemonic and a known mnemonic
|
|
// with no supported form alike ("unsupported <arch> instruction"), so only
|
|
// a shape that assembles cleanly counts, and the backlog over-approximates:
|
|
// a name whose real forms the battery misses lands there. amd64 keeps its
|
|
// exact table-driven check.
|
|
func probeShapes(a arch.Arch) []string {
|
|
switch a {
|
|
case arch.ARM64:
|
|
return []string{
|
|
"X0, X1, X2", "X0, X1", "X0", "$1, X0", "X0, (X1)", "(X0), X1",
|
|
"X0, (X1, 8)", "(SP), X0", "F0, F1, F2", "F0, F1", "F0",
|
|
"V0.B16, V1.B16, V2.B16", "p2", "X0, p2", "X0, X1, p2",
|
|
// The conditional select family spells the condition first
|
|
// and takes R register spellings.
|
|
"EQ, R0, R1, R2", "EQ, R0, R1", "EQ, R0",
|
|
"GE, F0, F1, F2", "NE, F0, F1, $0",
|
|
// Pairs, acquire/release and exclusive atomics, LSE-AL forms.
|
|
"(R0), R1", "R0, (R1)", "R1, (R2), R3", "(R2, R3), 8(R1)",
|
|
"8(R1), (R2, R3)", "R1, R2, (R3)", "(R0)",
|
|
// System operations and their register/operand names.
|
|
"$4, R1, p2", "$35943", "$1", "$1, SPSel", "SPSel, R0",
|
|
"IVAC, R0", "(R0), PLDL1KEEP", "R1, R2, R3, R4",
|
|
// SIMD element, structure and literal-pool forms.
|
|
"(R0), [V1.B16]", "[V1.B16], (R0)", "V13.S[0], R1",
|
|
"R1, V2.B[3]", "$4, V1.B16, V2.B16", "V1.B16, (R0)",
|
|
"(R0), V1.B16", "",
|
|
// The spellings GOROOT's own kernels use, from the
|
|
// differential kernels this table was proven against.
|
|
"R0, p2", "R0, R1", "F0, F1, F2, F3", "$4, V1.B16, V2.B16, V3.B16, V4.B16",
|
|
"(R0), [V0.B8, V1.B8, V2.B8, V3.B8]", "$1, $2, V1",
|
|
"R0, R1, p2", "p2, R1", "$1234, R1", "DCZID_EL0, R1",
|
|
"$0", "R1, $4, EQ", "$33, R1, $25, R2", "$4, R1, p2",
|
|
"$4, V1.B8, V2.B8, V3.B8", "$63, V1.D2, V2.D2, V3.D2",
|
|
"V1.B16, [V2.B16], V3.B16", "V1.B8, [V2.B16, V3.B16], V4.B8",
|
|
"$4, V1.B16, V2.B16, V3.B16", "$15, V1", "V1, V2, p2",
|
|
"R0, R1, $1, $4, p2",
|
|
// The landing-pad kind, the compiler's PCDATA
|
|
// bookkeeping and the four-operand bitfield
|
|
// insert/extract family, as the toolchain's own
|
|
// testdata spells them.
|
|
"C", "$1, $0", "$0, R1, $1, R2",
|
|
}
|
|
case arch.RISCV:
|
|
return []string{
|
|
"X5, X6, X7", "X5, X6", "X5", "$1, X5", "X5, (X6)", "$1, X5, X6",
|
|
"(X5), X6", "F0, F1, F2", "F0, F1", "p2", "X1, p2", "X0, p2",
|
|
"X5, X6, p2", "p2(SB)",
|
|
// AMO atomics: destination, base, source.
|
|
"R5, (R4), R6", "X5, (X4), X6",
|
|
// Segment stores take the first vector register aligned
|
|
// to the segment count, as the toolchain requires.
|
|
"(X5), X6, V0, V8", "(X5), X6, V0", "(X5), X0, V4",
|
|
// The FP multiply-add family takes four registers.
|
|
"F0, F1, F2, F3",
|
|
// The RVV slice: register, vector-register and vtype forms.
|
|
"V1, V2, V3", "V1, X5, V2", "V1", "V1, (X5)", "(X5), V1",
|
|
"$15, V1", "$15", "V1, V2", "V1, X5",
|
|
"X5, X6, p2", "R5, R6, p2",
|
|
"X5, E8, M8, TA, MA, X6", "$4, E32, M1, TA, MA, X1",
|
|
"(X5), X6, V1, V2",
|
|
// The CSR immediate forms the toolchain's testdata spells:
|
|
// immediate, CSR name, destination.
|
|
"$2, TIME, X5",
|
|
"",
|
|
}
|
|
case arch.LOONG64:
|
|
return []string{
|
|
"R4, R5, R6", "R4, R5", "R4", "$1, R4", "R4, (R5)", "(R4), R5",
|
|
"F0, F1, F2", "F0, F1", "p2", "R1, p2", "R4, p2",
|
|
"$1, R4, R5, R6", "$65536, R4", "R4, R5, p2", "p2(SB)",
|
|
// AMO atomics: destination, base, source.
|
|
"R5, (R4), R6", "X5, (X4), X6",
|
|
// Segment stores take the first vector register aligned
|
|
// to the segment count, as the toolchain requires.
|
|
"(X5), X6, V0, V8", "(X5), X6, V0", "(X5), X0, V4",
|
|
// The LSX and LASX banks share the 5-bit numbering with F.
|
|
"V1, V2, V3", "X1, X2, X3", "V1, V2", "X1, X2", "V1", "X1",
|
|
// The vector compare-to-flag forms land in an FCC register.
|
|
"V1, FCC0", "X1, FCC0",
|
|
// The compiler's bookkeeping pair and the raw spellings the
|
|
// toolchain's own testdata carries: JIRL rd, rj, offset (the
|
|
// form RET lowers to), the prefetch with a 32-bit address and
|
|
// hint, and the byte-shuffle quads.
|
|
"$1, $0", "R1, R5, 0", "0(R7), $5, $0", "(R7), $5, $0",
|
|
"V1, V2, V3, V4", "X1, X2, X3, X4",
|
|
"",
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// gasmEncodable reports whether the gasm encoder for a can emit the
|
|
// mnemonic, decided by trial assembly over the shape battery.
|
|
func gasmEncodable(a arch.Arch, name string) bool {
|
|
switch a {
|
|
case arch.ARM64, arch.RISCV, arch.LOONG64:
|
|
default:
|
|
return asm.Encodable(name)
|
|
}
|
|
for _, shape := range probeShapes(a) {
|
|
if gasmAssembles(a, name, shape) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// gasmAssembles reports whether a one-instruction probe file containing name
|
|
// with the given operand shape assembles without error.
|
|
func gasmAssembles(a arch.Arch, name, shape string) bool {
|
|
src := "TEXT ·p(SB), NOSPLIT, $0\n\t" + name
|
|
if shape != "" {
|
|
src += " " + shape
|
|
}
|
|
src += "\n\tRET\np2:\n\tRET\n"
|
|
f, errs := parser.Parse("probe.s", src)
|
|
if len(errs) > 0 {
|
|
return false
|
|
}
|
|
var err error
|
|
switch a {
|
|
case arch.ARM64:
|
|
_, err = asm.AssembleFileARM64(f)
|
|
case arch.RISCV:
|
|
_, err = asm.AssembleFileRISCV(f)
|
|
case arch.LOONG64:
|
|
_, err = asm.AssembleFileLOONG64(f)
|
|
}
|
|
return err == nil
|
|
}
|
|
|
|
// sanitize makes a mnemonic safe for use in a Go symbol name.
|
|
func sanitize(name string) string {
|
|
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
|
}
|
|
|
|
// --- corpus audit -----------------------------------------------------------
|
|
|
|
// corpusTarget is one architecture row of the corpus report.
|
|
type corpusTarget struct {
|
|
a arch.Arch
|
|
name string
|
|
}
|
|
|
|
// corpusTally accumulates one architecture's attempts over the corpus.
|
|
type corpusTally struct {
|
|
attempted int
|
|
assembled int
|
|
reasons map[string]int // failure reason → count
|
|
example map[string]string // failure reason → one representative file
|
|
fails []corpusFailure // every failure, in file order, for --list
|
|
}
|
|
|
|
// corpusFailure is one failed attempt, recorded for the --list report.
|
|
type corpusFailure struct {
|
|
path string
|
|
reason string
|
|
detail string
|
|
}
|
|
|
|
func (t *corpusTally) fail(path string, err error) {
|
|
reason := corpusReason(err)
|
|
t.reasons[reason]++
|
|
if t.example[reason] == "" {
|
|
t.example[reason] = path
|
|
}
|
|
t.fails = append(t.fails, corpusFailure{path: path, reason: reason, detail: firstLine(err.Error())})
|
|
}
|
|
|
|
// cmdAuditCorpus implements audit-instructions --corpus. The include
|
|
// directories carry #include resolution over a corpus whose files refer to
|
|
// headers such as GOROOT/pkg/include, the same -I a toolchain comparison
|
|
// needs.
|
|
func cmdAuditCorpus(args []string, dirs includeDirs, list bool) error {
|
|
if len(args) > 1 {
|
|
return &usageError{fmt.Errorf("audit-instructions --corpus takes at most one directory argument")}
|
|
}
|
|
root := ""
|
|
if len(args) == 1 {
|
|
root = args[0]
|
|
} else {
|
|
out, err := exec.Command("go", "env", "GOROOT").Output()
|
|
if err != nil {
|
|
return fmt.Errorf("locate GOROOT: %w", err)
|
|
}
|
|
root = filepath.Join(strings.TrimSpace(string(out)), "src")
|
|
}
|
|
// The toolchain's shipped headers (funcdata.h and friends) define the
|
|
// macros GOROOT files include; a corpus audit measures those files, so
|
|
// the header directory joins the search path automatically. go_asm.h
|
|
// is compiler-generated per package, so it is not resolved from here:
|
|
// files that include it get one generated per target architecture,
|
|
// which runCorpusAudit arranges.
|
|
if out, err := exec.Command("go", "env", "GOROOT").Output(); err == nil {
|
|
pkgInclude := filepath.Join(strings.TrimSpace(string(out)), "pkg", "include")
|
|
if fi, err := os.Stat(pkgInclude); err == nil && fi.IsDir() {
|
|
seen := false
|
|
for _, d := range dirs {
|
|
if d == pkgInclude {
|
|
seen = true
|
|
}
|
|
}
|
|
if !seen {
|
|
dirs = append(dirs, pkgInclude)
|
|
}
|
|
}
|
|
}
|
|
stats, err := runCorpusAudit(root, dirs)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
printCorpusStats(stats, list)
|
|
return nil
|
|
}
|
|
|
|
// corpusStats is the outcome of one corpus audit run.
|
|
type corpusStats struct {
|
|
root string
|
|
files int
|
|
generic int // files attempted for all four architectures
|
|
otherPort int // files named for another Go port: never attempted
|
|
full int // files that assembled for every target architecture
|
|
targets []corpusTarget
|
|
tallies []*corpusTally
|
|
}
|
|
|
|
// runCorpusAudit assembles every .s file under root and returns the stats.
|
|
// goPortSuffixes lists every architecture the Go project ports to. A file
|
|
// named for one of them belongs to that port's build, not to the generic
|
|
// set, even when gasm does not support the architecture.
|
|
var goPortSuffixes = []string{
|
|
"386", "amd64", "arm", "arm64", "loong64", "mips", "mips64",
|
|
"mips64le", "mipsle", "mips64x", "mipsx", "ppc64", "ppc64le",
|
|
"ppc64x", "riscv", "riscv64", "s390x", "wasm",
|
|
}
|
|
|
|
// otherPortFile reports whether the file belongs to a build no supported
|
|
// target ever compiles: either its name carries a Go-architecture suffix
|
|
// gasm does not support, or, for a file with no architecture suffix at all,
|
|
// it names another GOOS, which go/build drops from the file set
|
|
// (rt0_js_wasm.s is a javascript build, not a generic one).
|
|
func otherPortFile(path string) bool {
|
|
if otherGOOSFile(path) {
|
|
return true
|
|
}
|
|
base := path
|
|
if i := strings.LastIndexByte(base, '/'); i >= 0 {
|
|
base = base[i+1:]
|
|
}
|
|
for _, sfx := range goPortSuffixes {
|
|
if strings.HasSuffix(base, "_"+sfx+".s") {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
// goOSNames are the GOOS values go/build recognises in file names.
|
|
var goOSNames = map[string]bool{
|
|
"aix": true, "android": true, "darwin": true, "dragonfly": true,
|
|
"freebsd": true, "hurd": true, "illumos": true, "ios": true,
|
|
"js": true, "linux": true, "nacl": true, "netbsd": true,
|
|
"openbsd": true, "plan9": true, "solaris": true, "wasip1": true,
|
|
"windows": true, "zos": true,
|
|
}
|
|
|
|
// resolveGOOS validates a -GOOS flag value, mirroring the architecture
|
|
// check's surface: a usage error naming what the tool accepts.
|
|
func resolveGOOS(name string) (string, error) {
|
|
lower := strings.ToLower(name)
|
|
if goOSNames[lower] {
|
|
return lower, nil
|
|
}
|
|
return "", &usageError{fmt.Errorf("unknown GOOS %q: want one of %s", name, strings.Join(slices.Sorted(maps.Keys(goOSNames)), ", "))}
|
|
}
|
|
|
|
// goosFromFilename returns the GOOS the file's name carries, by go/build's
|
|
// goodOSArchFile rule: the GOOS segment sits last, or last before the
|
|
// architecture segment (sys_darwin_arm64.s, vlop_arm.s carries none). An
|
|
// empty result means the name names no GOOS and the ambient one applies.
|
|
func goosFromFilename(path string) string {
|
|
base := path
|
|
if i := strings.LastIndexByte(base, '/'); i >= 0 {
|
|
base = base[i+1:]
|
|
}
|
|
base = strings.TrimSuffix(base, ".s")
|
|
// go/build ignores everything before the first underscore, so a GOOS
|
|
// segment is only ever looked for from there on.
|
|
i := strings.IndexByte(base, '_')
|
|
if i < 0 {
|
|
return ""
|
|
}
|
|
segs := strings.Split(base[i:], "_")
|
|
if n := len(segs); n >= 2 && goOSNames[segs[n-2]] && slices.Contains(goPortSuffixes, segs[n-1]) {
|
|
return segs[n-2]
|
|
}
|
|
if goOSNames[segs[len(segs)-1]] {
|
|
return segs[len(segs)-1]
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// otherGOOSFile reports whether the file's name names a GOOS other than the
|
|
// host's, by go/build's file-name rules.
|
|
func otherGOOSFile(path string) bool {
|
|
base := path
|
|
if i := strings.LastIndexByte(base, '/'); i >= 0 {
|
|
base = base[i+1:]
|
|
}
|
|
for seg := range strings.SplitSeq(strings.TrimSuffix(base, ".s"), "_") {
|
|
if goOSNames[seg] && seg != runtime.GOOS {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func runCorpusAudit(root string, dirs includeDirs) (*corpusStats, error) {
|
|
files, err := asmFiles(root)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
targets := []corpusTarget{
|
|
{arch.AMD64, "amd64"},
|
|
{arch.ARM64, "arm64"},
|
|
{arch.RISCV, "riscv64"},
|
|
{arch.LOONG64, "loong64"},
|
|
}
|
|
tallies := make([]*corpusTally, len(targets))
|
|
for i := range tallies {
|
|
tallies[i] = &corpusTally{reasons: map[string]int{}, example: map[string]string{}}
|
|
}
|
|
// full is the north-star number: a file counts when every architecture
|
|
// its name allows assembles it.
|
|
full, generic, otherPort := 0, 0, 0
|
|
|
|
// Header generation is created on first use, so a corpus with no
|
|
// go_asm.h includes never pays for a temp directory.
|
|
var hdr *asmhdrCache
|
|
defer func() {
|
|
if hdr != nil {
|
|
hdr.close()
|
|
}
|
|
}()
|
|
|
|
for _, path := range files {
|
|
src, err := readSource(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// The GOOS the header generation type-checks under follows the
|
|
// file's name when the name carries one; the ambient GOOS is the
|
|
// honest guess otherwise (a build tag naming another GOOS is
|
|
// invisible to a file-name rule).
|
|
goos := goosFromFilename(path)
|
|
|
|
var wanted []int // indexes into targets
|
|
if a := arch.FromFilename(path); a != arch.Unknown {
|
|
for i, tg := range targets {
|
|
if tg.a == a {
|
|
wanted = append(wanted, i)
|
|
}
|
|
}
|
|
} else if otherPortFile(path) {
|
|
// A file named for a Go port gasm does not support (arm,
|
|
// 386, s390x, ...) or for another GOOS is compiled by no
|
|
// supported-arch build, so it is neither generic nor a
|
|
// per-arch attempt: counting it as generic would make the
|
|
// headline unreachably low for reasons no supported target
|
|
// can fix.
|
|
otherPort++
|
|
} else {
|
|
generic++
|
|
for i := range targets {
|
|
wanted = append(wanted, i)
|
|
}
|
|
}
|
|
|
|
// A file that includes go_asm.h parses against a per-target header:
|
|
// the defines differ per architecture (internal/cpu's layout, for
|
|
// one) and per GOOS (sys_darwin_arm64.s's trampoline constants,
|
|
// for another), so the parse cannot be shared the way a
|
|
// header-free file's can. A generation failure is a failure for
|
|
// every target, named for the package rather than a bare "include
|
|
// not found". A header already resolvable in the package
|
|
// directory or the -I list is left alone.
|
|
if len(wanted) > 0 && needsGoAsmHeader(src) && !goAsmHeaderResolved(filepath.Dir(path), dirs) {
|
|
if hdr == nil {
|
|
if hdr, err = newAsmhdrCache(); err != nil {
|
|
return nil, err
|
|
}
|
|
}
|
|
pkgDir := filepath.Dir(path)
|
|
ok := true
|
|
for _, i := range wanted {
|
|
tg, t := targets[i], tallies[i]
|
|
t.attempted++
|
|
hdrDir, err := hdr.dirFor(pkgDir, goos, goarchName(tg.a))
|
|
if err != nil {
|
|
ok = false
|
|
t.fail(path, err)
|
|
continue
|
|
}
|
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{
|
|
Expand: true,
|
|
IncludeDirs: append(slices.Clone(dirs), hdrDir),
|
|
Predefines: platformPredefinesFor(goarchName(tg.a), goos),
|
|
})
|
|
if len(errs) > 0 {
|
|
ok = false
|
|
t.fail(path, errs[0])
|
|
continue
|
|
}
|
|
if _, err := assembleFile(tg.a, f, goos); err != nil {
|
|
ok = false
|
|
t.fail(path, err)
|
|
continue
|
|
}
|
|
t.assembled++
|
|
}
|
|
if ok && len(wanted) > 0 {
|
|
full++
|
|
}
|
|
continue
|
|
}
|
|
|
|
ok := true
|
|
for _, i := range wanted {
|
|
tg, t := targets[i], tallies[i]
|
|
t.attempted++
|
|
// The parse carries the target's platform predefines, so it
|
|
// cannot be shared across targets the way a header-free file's
|
|
// could: a #ifdef GOARCH_arm block must be live on arm64 and
|
|
// dead everywhere else.
|
|
f, errs := parser.ParseWithOptions(path, src, parser.Options{
|
|
Expand: true,
|
|
IncludeDirs: dirs,
|
|
Predefines: platformPredefinesFor(goarchName(tg.a), goos),
|
|
})
|
|
var err error
|
|
if len(errs) > 0 {
|
|
err = errs[0] // a parse failure is a failure for every target
|
|
} else {
|
|
_, err = assembleFile(tg.a, f, goos)
|
|
}
|
|
if err != nil {
|
|
ok = false
|
|
t.fail(path, err)
|
|
continue
|
|
}
|
|
t.assembled++
|
|
}
|
|
if ok && len(wanted) > 0 {
|
|
full++
|
|
}
|
|
}
|
|
|
|
return &corpusStats{
|
|
root: root,
|
|
files: len(files),
|
|
generic: generic,
|
|
otherPort: otherPort,
|
|
full: full,
|
|
targets: targets,
|
|
tallies: tallies,
|
|
}, nil
|
|
}
|
|
|
|
// printCorpusStats renders the corpus audit report.
|
|
func printCorpusStats(s *corpusStats, list bool) {
|
|
fmt.Printf("corpus %s: %d files (%d generic, attempted for all architectures; %d named for other Go ports, never attempted)\n", s.root, s.files, s.generic, s.otherPort)
|
|
// The rate is over the files a supported build would attempt: the
|
|
// other ports' files sit in the count for completeness but can never
|
|
// assemble, so counting them in the denominator would report the gap
|
|
// of architectures gasm deliberately does not target.
|
|
attemptable := max(s.files-s.otherPort, 1)
|
|
fmt.Printf(" assemble for every target architecture: %d of %d attemptable (%.1f%%)\n", s.full, attemptable, 100*float64(s.full)/float64(attemptable))
|
|
for i, tg := range s.targets {
|
|
t := s.tallies[i]
|
|
fmt.Printf(" %s: %d/%d attempted\n", tg.name, t.assembled, t.attempted)
|
|
for _, r := range topReasons(t) {
|
|
fmt.Printf(" %4d %s\n", t.reasons[r], r)
|
|
fmt.Printf(" e.g. %s\n", t.example[r])
|
|
}
|
|
if !list {
|
|
continue
|
|
}
|
|
for _, f := range t.fails {
|
|
fmt.Printf(" FAIL %s\n", f.path)
|
|
fmt.Printf(" %s: %s\n", f.reason, f.detail)
|
|
}
|
|
}
|
|
}
|
|
|
|
// corpusReason buckets an assembly or parse failure for the histogram.
|
|
func corpusReason(err error) string {
|
|
msg := err.Error()
|
|
switch {
|
|
case strings.Contains(msg, "go_asm.h for GOARCH"):
|
|
return "go_asm.h generation failed"
|
|
case strings.Contains(msg, "unsupported"), strings.Contains(msg, "cannot encode"):
|
|
return "instruction not encodable"
|
|
case strings.Contains(msg, "undefined label"):
|
|
return "undefined label"
|
|
case strings.Contains(msg, "undefined symbol"), strings.Contains(msg, "external symbol"), strings.Contains(msg, "file-level assembly"):
|
|
return "undefined symbol or external"
|
|
case strings.Contains(msg, "operand"), strings.Contains(msg, "operand form"):
|
|
return "unsupported operand form"
|
|
default:
|
|
return "other: " + firstLine(msg)
|
|
}
|
|
}
|
|
|
|
// topReasons returns at most five reasons, most frequent first.
|
|
func topReasons(t *corpusTally) []string {
|
|
type kv struct {
|
|
k string
|
|
n int
|
|
}
|
|
var kvs []kv
|
|
for k, n := range t.reasons {
|
|
kvs = append(kvs, kv{k, n})
|
|
}
|
|
slices.SortFunc(kvs, func(a, b kv) int { return b.n - a.n })
|
|
if len(kvs) > 5 {
|
|
kvs = kvs[:5]
|
|
}
|
|
out := make([]string, len(kvs))
|
|
for i, kv := range kvs {
|
|
out[i] = kv.k
|
|
}
|
|
return out
|
|
}
|
|
|
|
// firstLine returns the first line of an error message, truncated.
|
|
func firstLine(msg string) string {
|
|
if i := strings.IndexByte(msg, '\n'); i >= 0 {
|
|
msg = msg[:i]
|
|
}
|
|
if len(msg) > 80 {
|
|
msg = msg[:80]
|
|
}
|
|
return msg
|
|
}
|