feat(cli): add gasm diff command for comparing assembly encodings

Assisted-by: DeepSeek V4 Pro
This commit is contained in:
2026-08-05 14:23:00 +02:00
parent 2d45dbf7ff
commit c0e280ee3c
+109
View File
@@ -55,6 +55,8 @@ func main() {
os.Exit(cmdVerify(os.Args[2:]))
case "debug":
os.Exit(cmdDebug(os.Args[2:]))
case "diff":
os.Exit(cmdDiff(os.Args[2:]))
case "lsp":
os.Exit(cmdLSP(os.Args[2:]))
case "version", "--version", "-V":
@@ -115,6 +117,7 @@ func usage(w io.Writer) {
{"asm", "assemble .s files to machine code (amd64, riscv64)"},
{"verify", "JIT-assemble and run dynamic checks (amd64, riscv64)"},
{"debug", "interactive source-level debugger (amd64)"},
{"diff", "compare machine code of two .s files"},
{"lsp", "run the language server over stdio"},
{"version", "print the version (same as --version)"},
}
@@ -528,6 +531,112 @@ requires -p, the package path, and the installed Go toolchain).
return 0
}
// cmdDiff compares the machine code of two assembly files.
func cmdDiff(args []string) int {
fs := newCommand("diff", "gasm diff <file1.s> <file2.s>", `
Compare the machine code produced by assembling two files.
Shows which functions differ and the byte-level differences.
Useful for verifying that two implementations produce identical code,
or for tracking encoding changes between Go assembler versions.
`)
fs.Parse(args)
if fs.NArg() != 2 {
fmt.Fprintln(os.Stderr, "usage: gasm diff <file1.s> <file2.s>")
return 2
}
path1, path2 := fs.Arg(0), fs.Arg(1)
// Assemble both files.
img1, err := assembleFile(path1)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path1, err)
return 1
}
img2, err := assembleFile(path2)
if err != nil {
fmt.Fprintf(os.Stderr, "gasm diff: %s: %v\n", path2, err)
return 1
}
// Compare functions by name.
funcs1 := make(map[string][]byte)
for _, fn := range img1.Funcs {
funcs1[fn.Name] = img1.Code[fn.Offset : fn.Offset+fn.Size]
}
funcs2 := make(map[string][]byte)
for _, fn := range img2.Funcs {
funcs2[fn.Name] = img2.Code[fn.Offset : fn.Offset+fn.Size]
}
// Find common functions and compare.
diffs := 0
for name, code1 := range funcs1 {
code2, ok := funcs2[name]
if !ok {
fmt.Printf("%s: only in %s\n", name, path1)
diffs++
continue
}
if !bytes.Equal(code1, code2) {
fmt.Printf("%s: DIFFERS (%d vs %d bytes)\n", name, len(code1), len(code2))
printByteDiff(code1, code2)
diffs++
} else {
fmt.Printf("%s: identical (%d bytes)\n", name, len(code1))
}
}
for name := range funcs2 {
if _, ok := funcs1[name]; !ok {
fmt.Printf("%s: only in %s\n", name, path2)
diffs++
}
}
if diffs == 0 {
fmt.Println("all functions identical")
return 0
}
return 1
}
// assembleFile assembles a file and returns the image.
func assembleFile(path string) (*asm.Image, error) {
src, err := readSource(path)
if err != nil {
return nil, err
}
f, errs := parser.Parse(path, src)
for _, e := range errs {
fmt.Fprintf(os.Stderr, "%s: %v\n", path, e)
}
if len(errs) > 0 {
return nil, fmt.Errorf("parse errors")
}
targetArch := arch.FromFilename(path)
if targetArch == arch.RISCV {
return asm.AssembleFileRISCV(f)
}
return asm.AssembleFile(f)
}
// printByteDiff shows the first few byte differences between two code blocks.
func printByteDiff(a, b []byte) {
maxLen := len(a)
if len(b) < maxLen {
maxLen = len(b)
}
shown := 0
for i := 0; i < maxLen && shown < 8; i++ {
if a[i] != b[i] {
fmt.Printf(" offset %#04x: %02x vs %02x\n", i, a[i], b[i])
shown++
}
}
if len(a) != len(b) {
fmt.Printf(" length: %d vs %d\n", len(a), len(b))
}
}
// 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 {