// Copyright (c) 2026 Petr Balvín (https://petrbalvin.org) // SPDX-License-Identifier: BSD-3-Clause package main import ( "fmt" "os" "os/exec" "path/filepath" "regexp" "runtime" "sort" "strings" "sourcedock.dev/petrbalvin/gasm-devkit/arch" "sourcedock.dev/petrbalvin/gasm-devkit/asm" ) // cmdAuditInstructions cross-checks the gasm amd64 encoder against the Go // toolchain's own assembler, probed black-box: every mnemonic in the gasm // table is offered to go tool asm in a battery of representative operand // shapes, and a mnemonic counts as known to Go when at least one shape // produces an error other than "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). // // Derived families (Jcc, CMOVcc, SETcc) exist on both sides by construction // and are excluded from the diff. func cmdAuditInstructions(args []string) error { fs := newCommand("audit-instructions", "gasm audit-instructions", ` Compare the gasm amd64 encoder 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 operand shapes per mnemonic, so the audit tracks whatever toolchain ` + "`go env GOROOT`" + ` names. `) if err := fs.Parse(args); err != nil { return err } if len(fs.Args()) > 0 { return fmt.Errorf("audit-instructions takes no file arguments") } tab := arch.ForArch(arch.AMD64) var names []string seen := map[string]bool{} for _, in := range tab.Instructions() { name := strings.ToUpper(in.Name) if derivedFamily(name) || seen[name] { continue } seen[name] = true names = append(names, name) } goKnown, err := probeGoAsm(names) if err != nil { return err } var superset, backlog, shared, goOnly []string for _, name := range names { switch { case !asm.Encodable(name): backlog = append(backlog, name) case !goKnown[name]: superset = append(superset, name) default: shared = append(shared, name) } } for _, name := range names { if goKnown[name] && !seen[name] { goOnly = append(goOnly, name) } } // GO-ONLY is computed from the complement of the gasm table, which this // audit cannot enumerate (Go's table is only visible through probes of // names we already know). Report it as informational zero instead of a // misleading list. goOnly = nil sort.Strings(superset) sort.Strings(backlog) sort.Strings(shared) w := os.Stdout fmt.Fprintf(w, "gasm table (amd64, families excluded): %d mnemonics\n", len(names)) fmt.Fprintf(w, "gasm encodable: %d go tool asm recognized: %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 } 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. 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(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="+runtime.GOARCH, "GOOS="+runtime.GOOS) out, _ := cmd.CombinedOutput() 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.Split(string(out), "\n") { m := reParse.FindStringSubmatch(l) if m == nil { continue } var lineNo int fmt.Sscanf(m[1], "%d", &lineNo) if name, ok := lineMnemonic[lineNo]; ok && unrecognizedRe.MatchString(l) { result[name] = false } } return result, nil } // sanitize makes a mnemonic safe for use in a Go symbol name. func sanitize(name string) string { return strings.NewReplacer(".", "_", "$", "_").Replace(name) }