refactor(cmd): consolidate cross-arch verify and drop dead code

This commit is contained in:
2026-09-14 18:22:00 +02:00
parent 4f190ee1a2
commit 2db563be07
2 changed files with 106 additions and 300 deletions
+1 -4
View File
@@ -84,7 +84,7 @@ REPL commands:
if *timeout > 0 {
go func() {
time.Sleep(*timeout)
fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s) — killing the debuggee\n", *timeout)
fmt.Fprintf(os.Stderr, "gasm debug: timeout (%s), killing the debuggee\n", *timeout)
os.Exit(3)
}()
}
@@ -140,7 +140,6 @@ REPL commands:
}
// Construct the argument block with buffer pointers at the correct positions.
bufIdx := 0
for _, arg := range layout {
if !arg.IsPtr {
continue
@@ -170,12 +169,10 @@ REPL commands:
argBlock[off+16+j] = byte(size >> (j * 8))
}
}
bufIdx++
break
}
}
}
_ = bufIdx
} else {
argBlock = make([]byte, fl.Args)
sess, err = debug.Launch("", path, *funcName, argBlock)
+81 -272
View File
@@ -1,7 +1,7 @@
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
// SPDX-License-Identifier: BSD-3-Clause
// Command gasm is the developer frontend for GAsm — Go's Plan 9 assembler.
// Command gasm is the developer frontend for GAsm, Go's Plan 9 assembler.
// It bundles a token dumper, a parser, a formatter, a linter and a language
// server into one binary. Every subcommand works headlessly so it can be
// driven from scripts and CI as well as from an editor.
@@ -81,7 +81,7 @@ func main() {
case "help", "--help", "-h":
usage(os.Stdout)
default:
fmt.Fprintf(os.Stderr, "gasm: unknown command %q — run \"gasm --help\" for usage\n", os.Args[1])
fmt.Fprintf(os.Stderr, "gasm: unknown command %q; run \"gasm --help\" for usage\n", os.Args[1])
os.Exit(2)
}
}
@@ -117,7 +117,7 @@ func usage(w io.Writer) {
bold, cyan, yellow, gray, reset = colorBold, colorCyan, colorYellow, colorGray, colorReset
}
fmt.Fprintf(w, "%sgasm %s%s — developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version, reset, reset)
fmt.Fprintf(w, "%sgasm %s%s: developer tooling for Go's Plan 9 assembler (GAsm)%s\n\n", bold, version, reset, reset)
fmt.Fprintf(w, "gasm bundles a lexer, parser, formatter, linter, standalone assembler and\n")
fmt.Fprintf(w, "language server for Plan 9 assembly into one self-contained binary.\n\n")
@@ -263,7 +263,7 @@ standard input.
funcs++
}
}
fmt.Printf("%s: OK — %d declarations, %d functions\n", path, len(file.Decls), funcs)
fmt.Printf("%s: OK, %d declarations, %d functions\n", path, len(file.Decls), funcs)
return 0
}
@@ -274,7 +274,7 @@ spacing, per-function mnemonic alignment and blank-line layout (exactly one
blank line before each label, TEXT and GLOBL block). Formatting is
idempotent and preserves every line, comments included.
With no paths — or a directory path — every .s file below it is reformatted
With no paths, or a directory path, every .s file below it is reformatted
in place and the changed files are listed, the way go fmt does; "." and "_"
directories are skipped. Explicit file paths print to stdout unless -w is
given.
@@ -337,7 +337,7 @@ given.
}
// asmFiles collects the .s files below dir, skipping directories whose name
// starts with "." or "_" — as the go tooling does, which keeps .git and
// starts with "." or "_", as the go tooling does, which keeps .git and
// scratch or reference trees (e.g. _refs) untouched.
func asmFiles(dir string) ([]string, error) {
var out []string
@@ -419,6 +419,7 @@ hover, document symbols, diagnostics and semantic-token highlighting.
`)
fs.Parse(args)
srv := lsp.New(os.Stdin, os.Stdout)
srv.SetVersion(version)
if err := srv.Run(); err != nil {
fmt.Fprintln(os.Stderr, "gasm lsp:", err)
return 1
@@ -765,9 +766,11 @@ gasm verify --fuzz which exercises the code paths.
return 0
}
// cmdVerifyRISCV handles the verify subcommand for RISC-V files.
// JIT requires RISC-V hardware; only ground-truth and profile are available.
func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
// cmdVerifyNonJIT handles the verify subcommand for files whose architecture
// the host cannot execute: only the ground-truth comparison and the static
// profile are available there. Relocation sites are masked before the byte
// comparison, as the toolchain leaves them zero for the linker.
func cmdVerifyNonJIT(path string, targetArch arch.Arch, groundTruth, profile bool) int {
src, err := readSource(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
@@ -780,19 +783,58 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
if len(errs) > 0 {
return 1
}
img, err := asm.AssembleFileRISCV(f)
img, err := assembleFile(targetArch, f)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
if groundTruth {
gt, err := verify.GroundTruthRISCV(path)
var gt map[string][]byte
switch targetArch {
case arch.RISCV:
gt, err = verify.GroundTruthRISCV(path)
case arch.LOONG64:
gt, err = verify.GroundTruthLOONG64(path)
case arch.ARM64:
gt, err = verify.GroundTruthARM64(path)
default:
gt, err = verify.GroundTruth(path)
}
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
return 1
}
matched, total := 0, 0
matched, total, diffs := compareGroundTruth(img, gt)
if diffs > 0 {
printCodeDiff(img, gt)
}
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
if matched < total || diffs > 0 {
return 1
}
return 0
}
if profile {
for _, fn := range img.Funcs {
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
}
return 0
}
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
for _, fn := range img.Funcs {
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
}
return 0
}
// compareGroundTruth compares the image's functions against the go tool asm
// output byte-for-byte, masking relocation sites (disp32 fields the Go linker
// fills at link time). It prints one line per function and returns the
// matched and compared counts plus the number of functions with byte diffs.
func compareGroundTruth(img *asm.Image, gt map[string][]byte) (matched, total, diffs int) {
for _, fn := range img.Funcs {
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
goCode, ok := gt[fn.Name]
@@ -821,100 +863,22 @@ func cmdVerifyRISCV(path string, groundTruth, profile bool) int {
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
}
} else {
diffs++
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
var gb, gs string
for j := i; j < i+16 && j < len(gasmCode); j++ {
gb += fmt.Sprintf(" %02x", gasmCode[j])
}
for j := i; j < i+16 && j < len(goCode); j++ {
gs += fmt.Sprintf(" %02x", goCode[j])
}
fmt.Printf(" %04x: gasm:%s\n", i, gb)
fmt.Printf(" %04x: gt: %s\n", i, gs)
}
}
}
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
if matched < total {
return 1
}
return 0
return matched, total, diffs
}
if profile {
for _, fn := range img.Funcs {
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
}
return 0
}
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
for _, fn := range img.Funcs {
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
}
return 0
}
// cmdVerifyLOONG64 verifies a loong64 source file against `go tool asm`
// (GOARCH=loong64) — the ground-truth oracle — since gasm cannot JIT-load
// LoongArch code on an amd64 host. Relocation sites are masked before the
// byte comparison, as the toolchain leaves them zero for the linker.
func cmdVerifyLOONG64(path string, groundTruth, profile bool) int {
src, err := readSource(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
f, errs := parser.Parse(path, src)
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
}
if len(errs) > 0 {
return 1
}
img, err := asm.AssembleFileLOONG64(f)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
if groundTruth {
gt, err := verify.GroundTruthLOONG64(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
return 1
}
matched, total := 0, 0
// printCodeDiff shows a 16-byte hex dump per function whose gasm bytes differ
// from the go tool asm output.
func printCodeDiff(img *asm.Image, gt map[string][]byte) {
for _, fn := range img.Funcs {
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
goCode, ok := gt[fn.Name]
if !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
if !ok || bytes.Equal(gasmCode, goCode) {
continue
}
total++
gasmCmp := make([]byte, len(gasmCode))
goCmp := make([]byte, len(goCode))
copy(gasmCmp, gasmCode)
copy(goCmp, goCode)
for _, r := range fn.Relocs {
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
gasmCmp[j] = 0
}
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
goCmp[j] = 0
}
}
if bytes.Equal(gasmCmp, goCmp) {
matched++
if len(fn.Relocs) > 0 {
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
} else {
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
}
} else {
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
var gb, gs string
for j := i; j < i+16 && j < len(gasmCode); j++ {
@@ -928,121 +892,12 @@ func cmdVerifyLOONG64(path string, groundTruth, profile bool) int {
}
}
}
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
if matched < total {
return 1
}
return 0
}
if profile {
for _, fn := range img.Funcs {
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
}
return 0
}
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
for _, fn := range img.Funcs {
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
}
return 0
}
func cmdVerifyARM64(path string, groundTruth, profile bool) int {
src, err := readSource(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
f, errs := parser.Parse(path, src)
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
}
if len(errs) > 0 {
return 1
}
img, err := asm.AssembleFileARM64(f)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: %v\n", err)
return 1
}
if groundTruth {
gt, err := verify.GroundTruthARM64(path)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
return 1
}
matched, total := 0, 0
for _, fn := range img.Funcs {
gasmCode := img.Code[fn.Offset : fn.Offset+fn.Size]
goCode, ok := gt[fn.Name]
if !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", fn.Name)
continue
}
total++
gasmCmp := make([]byte, len(gasmCode))
goCmp := make([]byte, len(goCode))
copy(gasmCmp, gasmCode)
copy(goCmp, goCode)
for _, r := range fn.Relocs {
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
gasmCmp[j] = 0
}
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
goCmp[j] = 0
}
}
if bytes.Equal(gasmCmp, goCmp) {
matched++
if len(fn.Relocs) > 0 {
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", fn.Name, fn.Size, len(fn.Relocs))
} else {
fmt.Printf(" %s: MATCH (%d bytes)\n", fn.Name, fn.Size)
}
} else {
fmt.Printf(" %s: MISMATCH (%d vs %d bytes)\n", fn.Name, fn.Size, len(goCode))
for i := 0; i < len(gasmCode) || i < len(goCode); i += 16 {
var gb, gs string
for j := i; j < i+16 && j < len(gasmCode); j++ {
gb += fmt.Sprintf(" %02x", gasmCode[j])
}
for j := i; j < i+16 && j < len(goCode); j++ {
gs += fmt.Sprintf(" %02x", goCode[j])
}
fmt.Printf(" %04x: gasm:%s\n", i, gb)
fmt.Printf(" %04x: gt: %s\n", i, gs)
}
}
}
fmt.Printf("%s: %d/%d matched\n", path, matched, total)
if matched < total {
return 1
}
return 0
}
if profile {
for _, fn := range img.Funcs {
fmt.Printf("%s: %d bytes, labels: %v\n", fn.Name, fn.Size, fn.Labels)
}
return 0
}
fmt.Printf("%s: %d functions assembled\n", path, len(img.Funcs))
for _, fn := range img.Funcs {
fmt.Printf(" %s: %d bytes\n", fn.Name, fn.Size)
}
return 0
}
func cmdVerify(args []string) int {
set := newCommand("verify", "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>", `
Assemble FILE (amd64), map it into executable memory and report the available
functions. This confirms the assembled image is self-consistent (no
unresolved external symbols) and executable — the prerequisite for dynamic
unresolved external symbols) and executable, the prerequisite for dynamic
testing.
With -smoke, each NOSPLIT function is called with a zeroed argument block to
@@ -1100,17 +955,12 @@ each entry reproduces.
// under the available loong64 emulators), so those kernels take the
// toolchain-comparison path.
if targetArch != hostArch() || targetArch == arch.LOONG64 {
// No JIT on this host: ground truth and profile remain available.
// (loong64 is ground-truth-only everywhere for now: its trampoline
// is implemented but not yet validated against real hardware.)
switch targetArch {
case arch.RISCV:
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
return cmdVerifyRISCV(path, *groundTruth, *profile)
case arch.LOONG64:
// LoongArch: ground-truth only (trampoline not yet
// hardware-validated).
return cmdVerifyLOONG64(path, *groundTruth, *profile)
case arch.ARM64:
// AArch64: ground-truth only (no JIT on non-ARM64 hosts).
return cmdVerifyARM64(path, *groundTruth, *profile)
case arch.RISCV, arch.LOONG64, arch.ARM64:
return cmdVerifyNonJIT(path, targetArch, *groundTruth, *profile)
case arch.AMD64:
fmt.Fprintln(os.Stderr, "gasm verify: JIT-based checks need an amd64 host; use --ground-truth here")
return 1
@@ -1224,50 +1074,13 @@ each entry reproduces.
fmt.Fprintf(os.Stderr, "gasm verify: ground truth: %v\n", err)
return 1
}
matched, total := 0, 0
for _, name := range names {
fl, _ := k.Func(name)
gasmCode := k.Image().Code[fl.Offset : fl.Offset+fl.Size]
goCode, ok := gt[name]
if !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
continue
}
total++
// Compare, masking relocation sites (disp32 fields that the
// Go linker fills at link time — gasm resolves them internally).
gasmCmp := make([]byte, len(gasmCode))
goCmp := make([]byte, len(goCode))
copy(gasmCmp, gasmCode)
copy(goCmp, goCode)
for _, r := range fl.Relocs {
for j := r.Off; j < r.Off+4 && j < len(gasmCmp); j++ {
gasmCmp[j] = 0
}
for j := r.Off; j < r.Off+4 && j < len(goCmp); j++ {
goCmp[j] = 0
}
}
if bytes.Equal(gasmCmp, goCmp) {
matched++
if len(fl.Relocs) > 0 {
fmt.Printf(" %s: MATCH (%d bytes, %d relocs masked)\n", name, fl.Size, len(fl.Relocs))
} else {
fmt.Printf(" %s: MATCH (%d bytes)\n", name, fl.Size)
}
} else {
fmt.Printf(" %s: MISMATCH (gasm %d bytes, go %d bytes)\n", name, fl.Size, len(goCode))
for i := 0; i < len(gasmCmp) && i < len(goCmp); i++ {
if gasmCmp[i] != goCmp[i] {
fmt.Printf(" first diff at byte %d: gasm=%02x go=%02x\n", i, gasmCmp[i], goCmp[i])
break
}
}
matched, total, diffs := compareGroundTruth(k.Image(), gt)
if diffs > 0 {
printCodeDiff(k.Image(), gt)
rc = 1
}
}
fmt.Printf("ground truth: %d/%d functions byte-identical\n", matched, total)
if matched < total {
if matched < total || diffs > 0 {
rc = 1
}
}
@@ -1289,13 +1102,11 @@ each entry reproduces.
sigs := verify.ExtractSignatures(src)
fuzzed := 0
for _, name := range names {
sig, ok := sigs[name]
if !ok {
if _, ok := sigs[name]; !ok {
fmt.Printf(" %s: SKIP (no // func signature)\n", name)
continue
}
goCode, ok := gt[name]
if !ok {
if _, ok := gt[name]; !ok {
fmt.Printf(" %s: SKIP (not in go tool asm output)\n", name)
continue
}
@@ -1312,8 +1123,6 @@ each entry reproduces.
rc = 1
}
}
_ = sig
_ = goCode
fuzzed++
}
fmt.Printf("fuzz: %d functions tested, %d iterations each\n", fuzzed, *fuzzN)
@@ -1338,7 +1147,7 @@ each entry reproduces.
// Run smoke and ABI checks in parallel, each function in its own child
// process: the JIT'd code runs with zeroed or fuzzed arguments, and a
// function that dereferences them faults — the crash is reported as a
// function that dereferences them faults, the crash is reported as a
// CRASH line instead of killing this process (mirrors fuzzInSubprocess).
if *smoke || *abi {
type checkResult struct {
@@ -1479,7 +1288,7 @@ func fuzzInSubprocess(path, funcName string, n int, extra ...string) string {
if exitErr, ok := err.(*exec.ExitError); ok {
ws := exitErr.Sys().(syscall.WaitStatus)
if ws.Signaled() {
return fmt.Sprintf("%s: CRASH (%v — partial function, use --ground-truth)", funcName, ws.Signal())
return fmt.Sprintf("%s: CRASH (%v; partial function, use --ground-truth)", funcName, ws.Signal())
}
}
// Non-zero exit without a signal: the fuzz reported mismatches.
@@ -1504,12 +1313,12 @@ func fuzzInSubprocess(path, funcName string, n int, extra ...string) string {
// sweepInSubprocess runs the smoke/abi checks for a single function in a
// child process. If the child is killed by a signal (e.g. SIGSEGV from a
// function that dereferences its zeroed or fuzzed arguments), it returns a
// CRASH report instead of dying — the same isolation fuzzInSubprocess
// CRASH report instead of dying, the same isolation fuzzInSubprocess
// provides for the fuzz sweep.
func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string, bool) {
self, err := os.Executable()
if err != nil {
return fmt.Sprintf(" smoke/abi: FAIL — cannot find self: %v", err), true
return fmt.Sprintf(" smoke/abi: FAIL: cannot find self: %v", err), true
}
args := []string{"verify"}
if smoke {
@@ -1526,7 +1335,7 @@ func sweepInSubprocess(path, funcName string, smoke, abi bool, abiN int) (string
if exitErr, ok := err.(*exec.ExitError); ok {
ws, ok := exitErr.Sys().(syscall.WaitStatus)
if ok && ws.Signaled() {
return fmt.Sprintf(" smoke/abi: CRASH (%v — the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true
return fmt.Sprintf(" smoke/abi: CRASH (%v: the function faults on zeroed or fuzzed\n arguments; verify it with -call and valid buffers)", ws.Signal()), true
}
}
// Non-zero exit without a signal: the checks themselves failed and
@@ -1550,7 +1359,7 @@ func sweepCheckLines(out []byte) string {
}
// runSweepChecks performs the in-process smoke and ABI checks for one
// function — the child half of sweepInSubprocess.
// function, the child half of sweepInSubprocess.
func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smoke, abi bool, abiN int) ([]string, bool) {
var msgs []string
failed := false
@@ -1559,7 +1368,7 @@ func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smok
args := make([]byte, fl.Args)
_, err := k.CallFunc(name, args)
if err != nil {
msgs = append(msgs, fmt.Sprintf(" smoke: FAIL — %v", err))
msgs = append(msgs, fmt.Sprintf(" smoke: FAIL: %v", err))
failed = true
} else {
msgs = append(msgs, " smoke: OK")
@@ -1579,7 +1388,7 @@ func runSweepChecks(k *verify.Kernel, path, name string, fl asm.FuncLayout, smok
args := make([]byte, fl.Args)
_, report, err := k.CallFuncChecked(name, args)
if err != nil {
msgs = append(msgs, fmt.Sprintf(" abi: FAIL — %v", err))
msgs = append(msgs, fmt.Sprintf(" abi: FAIL: %v", err))
failed = true
} else if !report.OK() {
msgs = append(msgs, fmt.Sprintf(" abi: %s", report))
@@ -1669,7 +1478,7 @@ func cmdVerifyCall(k *verify.Kernel, path, funcName, bufSpec, scalarSpec string,
for i := range repeat {
out, err := k.CallFunc(funcName, args)
if err != nil {
fmt.Printf(" call %d: FAIL — %v\n", i+1, err)
fmt.Printf(" call %d: FAIL: %v\n", i+1, err)
rc = 1
continue
}