From c0e280ee3cdf08c5c11180a57871a85821ea9245 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Wed, 5 Aug 2026 14:23:00 +0200 Subject: [PATCH] feat(cli): add gasm diff command for comparing assembly encodings Assisted-by: DeepSeek V4 Pro --- cmd/gasm/main.go | 109 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) diff --git a/cmd/gasm/main.go b/cmd/gasm/main.go index 1964a92..0ee0612 100644 --- a/cmd/gasm/main.go +++ b/cmd/gasm/main.go @@ -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 ", ` +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 ") + 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 {