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
|
// construction and are excluded from the diff; the other architectures list
|
||||||
// their conditional branches outright.
|
// their conditional branches outright.
|
||||||
func cmdAuditInstructions(args []string) error {
|
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
|
Compare the gasm encoder for the given architecture (default amd64) against
|
||||||
go tool asm and print the diff: superset encodings (gasm-only, shippable via
|
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-
|
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
|
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`"+`
|
of bare mnemonics, so the audit tracks whatever toolchain `+"`go env GOROOT`"+`
|
||||||
provides.
|
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 {
|
if err := fs.Parse(args); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if *corpus {
|
||||||
|
return cmdAuditCorpus(fs.Args())
|
||||||
|
}
|
||||||
archName := "amd64"
|
archName := "amd64"
|
||||||
switch n := len(fs.Args()); {
|
switch n := len(fs.Args()); {
|
||||||
case n > 1:
|
case n > 1:
|
||||||
@@ -305,3 +317,196 @@ func gasmAssembles(a arch.Arch, name, shape string) bool {
|
|||||||
func sanitize(name string) string {
|
func sanitize(name string) string {
|
||||||
return strings.NewReplacer(".", "_", "$", "_").Replace(name)
|
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)
|
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
|
## audit-instructions
|
||||||
|
|
||||||
```text
|
```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
|
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
|
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
|
## scaffold
|
||||||
|
|
||||||
```text
|
```text
|
||||||
|
|||||||
Reference in New Issue
Block a user