feat(gasm): audit a .s corpus with audit-instructions --corpus
Assisted-by: GLM 5.3 Flash
This commit is contained in:
+206
-1
@@ -37,17 +37,29 @@ import (
|
||||
// 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 [amd64|arm64|riscv64|loong64]", `
|
||||
fs := newCommand("audit-instructions", "gasm audit-instructions [--corpus [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), known-but-unencodable names (the backlog) and go-
|
||||
only names (feature gaps). The Go side is probed black-box with a battery
|
||||
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||
provides.
|
||||
|
||||
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.
|
||||
`)
|
||||
corpus := fs.Bool("corpus", false, "assemble a corpus of .s files and report pass rates and failure reasons")
|
||||
if err := fs.Parse(args); err != nil {
|
||||
return err
|
||||
}
|
||||
if *corpus {
|
||||
return cmdAuditCorpus(fs.Args())
|
||||
}
|
||||
archName := "amd64"
|
||||
switch n := len(fs.Args()); {
|
||||
case n > 1:
|
||||
@@ -305,3 +317,196 @@ func gasmAssembles(a arch.Arch, name, shape string) bool {
|
||||
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
|
||||
}
|
||||
|
||||
func (t *corpusTally) fail(path, reason string) {
|
||||
t.reasons[reason]++
|
||||
if t.example[reason] == "" {
|
||||
t.example[reason] = path
|
||||
}
|
||||
}
|
||||
|
||||
// cmdAuditCorpus implements audit-instructions --corpus.
|
||||
func cmdAuditCorpus(args []string) error {
|
||||
if len(args) > 1 {
|
||||
return 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")
|
||||
}
|
||||
stats, err := runCorpusAudit(root)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
printCorpusStats(stats)
|
||||
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
|
||||
full int // files that assembled for every target architecture
|
||||
targets []corpusTarget
|
||||
tallies []*corpusTally
|
||||
}
|
||||
|
||||
// runCorpusAudit assembles every .s file under root and returns the stats.
|
||||
func runCorpusAudit(root string) (*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 := 0, 0
|
||||
|
||||
for _, path := range files {
|
||||
src, err := readSource(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f, errs := parser.Parse(path, src)
|
||||
|
||||
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 {
|
||||
generic++
|
||||
for i := range targets {
|
||||
wanted = append(wanted, i)
|
||||
}
|
||||
}
|
||||
|
||||
ok := true
|
||||
for _, i := range wanted {
|
||||
tg, t := targets[i], tallies[i]
|
||||
t.attempted++
|
||||
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)
|
||||
}
|
||||
if err != nil {
|
||||
ok = false
|
||||
t.fail(path, corpusReason(err))
|
||||
continue
|
||||
}
|
||||
t.assembled++
|
||||
}
|
||||
if ok && len(wanted) > 0 {
|
||||
full++
|
||||
}
|
||||
}
|
||||
|
||||
return &corpusStats{
|
||||
root: root,
|
||||
files: len(files),
|
||||
generic: generic,
|
||||
full: full,
|
||||
targets: targets,
|
||||
tallies: tallies,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// printCorpusStats renders the corpus audit report.
|
||||
func printCorpusStats(s *corpusStats) {
|
||||
fmt.Printf("corpus %s: %d files (%d generic, attempted for all architectures)\n", s.root, s.files, s.generic)
|
||||
fmt.Printf(" assemble for every target architecture: %d (%.1f%%)\n", s.full, 100*float64(s.full)/float64(max(s.files, 1)))
|
||||
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])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// corpusReason buckets an assembly or parse failure for the histogram.
|
||||
func corpusReason(err error) string {
|
||||
msg := err.Error()
|
||||
switch {
|
||||
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
|
||||
}
|
||||
|
||||
@@ -293,3 +293,58 @@ func TestSweepCheckLines(t *testing.T) {
|
||||
t.Errorf("sweepCheckLines = %q, want %q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// TestRunCorpusAudit drives the corpus audit over a small fixture tree: one
|
||||
// suffixed amd64 file, one suffixed arm64 file whose body is not arm64, one
|
||||
// generic file, and one file that does not parse.
|
||||
func TestRunCorpusAudit(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
write := func(name, src string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(filepath.Join(dir, name), []byte(src), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write("good_amd64.s", "#include \"textflag.h\"\nTEXT ·add(SB), NOSPLIT, $0-0\n\tMOVQ AX, BX\n\tRET\n")
|
||||
write("bad_arm64.s", "#include \"textflag.h\"\nTEXT ·f(SB), NOSPLIT, $0-0\n\tMOVQ AX, BX\n\tRET\n")
|
||||
write("generic.s", "#include \"textflag.h\"\nTEXT ·g(SB), NOSPLIT, $0-0\n\tRET\n")
|
||||
write("broken.s", "#include \"textflag.h\"\nTEXT ·b(SB), NOSPLIT, $0-0\n\tJMP nowhere\n\tRET\n")
|
||||
|
||||
stats, err := runCorpusAudit(dir)
|
||||
if err != nil {
|
||||
t.Fatalf("runCorpusAudit: %v", err)
|
||||
}
|
||||
if stats.files != 4 {
|
||||
t.Errorf("files = %d, want 4", stats.files)
|
||||
}
|
||||
if stats.generic != 2 {
|
||||
t.Errorf("generic = %d, want 2 (generic.s and broken.s)", stats.generic)
|
||||
}
|
||||
// good_amd64 and generic.s assemble everywhere they are attempted.
|
||||
if stats.full != 2 {
|
||||
t.Errorf("full = %d, want 2", stats.full)
|
||||
}
|
||||
get := func(name string) *corpusTally {
|
||||
for i, tg := range stats.targets {
|
||||
if tg.name == name {
|
||||
return stats.tallies[i]
|
||||
}
|
||||
}
|
||||
t.Fatalf("no tally for %s", name)
|
||||
return nil
|
||||
}
|
||||
// amd64: good_amd64 + generic.s + broken.s; the broken file fails to parse.
|
||||
if a := get("amd64"); a.attempted != 3 || a.assembled != 2 {
|
||||
t.Errorf("amd64 = %d/%d, want 2/3", a.assembled, a.attempted)
|
||||
}
|
||||
// arm64: bad_arm64 (MOVQ is not arm64) + generic.s + broken.s.
|
||||
if a := get("arm64"); a.attempted != 3 || a.assembled != 1 {
|
||||
t.Errorf("arm64 = %d/%d, want 1/3", a.assembled, a.attempted)
|
||||
}
|
||||
if r := get("amd64").reasons["instruction not encodable"]; r != 0 {
|
||||
t.Errorf("amd64 unexpected unencodable reason: %d", r)
|
||||
}
|
||||
if r := get("arm64").reasons["instruction not encodable"]; r != 1 {
|
||||
t.Errorf("arm64 unencodable reasons = %d, want 1", r)
|
||||
}
|
||||
}
|
||||
|
||||
+25
-1
@@ -338,7 +338,7 @@ add: 16 bytes, args=24, frame=0 NOSPLIT
|
||||
## audit-instructions
|
||||
|
||||
```text
|
||||
Usage: gasm audit-instructions [amd64|arm64|riscv64|loong64]
|
||||
Usage: gasm audit-instructions [--corpus [dir]] [amd64|arm64|riscv64|loong64]
|
||||
```
|
||||
|
||||
Compare the gasm encoder for the given architecture (default amd64) against the
|
||||
@@ -361,6 +361,30 @@ gasm encodable: 580 go tool asm recognized: 1542
|
||||
shared: 580
|
||||
```
|
||||
|
||||
With `--corpus` the audit changes shape: it assembles every `.s` file under
|
||||
DIR (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 headline number (files
|
||||
that assemble for every target architecture), the per-architecture pass rates
|
||||
and the most common failure reasons with one representative file each, which
|
||||
drive the encodability backlog by frequency rather than by table order. A run
|
||||
over GOROOT takes under a second.
|
||||
|
||||
```sh
|
||||
gasm audit-instructions --corpus
|
||||
gasm audit-instructions --corpus "$(go env GOROOT)/src/crypto"
|
||||
```
|
||||
|
||||
```text
|
||||
corpus /usr/local/go/src: 627 files (365 generic, attempted for all architectures)
|
||||
assemble for every target architecture: 108 (17.2%)
|
||||
amd64: 77/464 attempted
|
||||
148 instruction not encodable
|
||||
e.g. /usr/local/go/src/cmd/asm/internal/asm/testdata/386enc.s
|
||||
...
|
||||
```
|
||||
|
||||
## scaffold
|
||||
|
||||
```text
|
||||
|
||||
Reference in New Issue
Block a user