2026-07-06 09:49:50 +02:00
// 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.
// 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.
package main
import (
2026-07-30 12:55:54 +02:00
"bytes"
2026-07-06 09:49:50 +02:00
"flag"
"fmt"
"io"
2026-07-12 21:24:41 +02:00
"io/fs"
2026-07-06 09:49:50 +02:00
"os"
2026-08-01 18:08:10 +02:00
"os/exec"
2026-07-12 21:24:41 +02:00
"path/filepath"
2026-08-21 01:10:14 +02:00
"runtime"
2026-08-05 16:41:00 +02:00
"sort"
2026-08-01 18:08:10 +02:00
"strconv"
2026-07-06 09:49:50 +02:00
"strings"
2026-08-21 01:10:14 +02:00
"sync"
2026-08-01 18:08:10 +02:00
"syscall"
2026-07-06 09:49:50 +02:00
"sourcedock.dev/petrbalvin/gasm-devkit/arch"
"sourcedock.dev/petrbalvin/gasm-devkit/asm"
"sourcedock.dev/petrbalvin/gasm-devkit/ast"
"sourcedock.dev/petrbalvin/gasm-devkit/format"
"sourcedock.dev/petrbalvin/gasm-devkit/lexer"
"sourcedock.dev/petrbalvin/gasm-devkit/lint"
"sourcedock.dev/petrbalvin/gasm-devkit/lsp"
"sourcedock.dev/petrbalvin/gasm-devkit/parser"
2026-07-22 09:28:11 +02:00
"sourcedock.dev/petrbalvin/gasm-devkit/verify"
2026-07-06 09:49:50 +02:00
)
// version is the release version, stamped at build time via
// -ldflags "-X main.version=…" (defaulting to the current release).
2026-08-24 21:03:30 +02:00
var version = "0.32.0-dev"
2026-07-06 09:49:50 +02:00
func main () {
if len ( os . Args ) < 2 {
usage ( os . Stderr )
os . Exit ( 2 )
}
switch os . Args [ 1 ] {
case "tokens" :
os . Exit ( cmdTokens ( os . Args [ 2 :]))
case "parse" :
os . Exit ( cmdParse ( os . Args [ 2 :]))
case "fmt" :
os . Exit ( cmdFmt ( os . Args [ 2 :]))
case "lint" :
os . Exit ( cmdLint ( os . Args [ 2 :]))
case "asm" :
os . Exit ( cmdAsm ( os . Args [ 2 :]))
2026-07-22 09:28:11 +02:00
case "verify" :
os . Exit ( cmdVerify ( os . Args [ 2 :]))
2026-08-01 02:34:00 +02:00
case "debug" :
os . Exit ( cmdDebug ( os . Args [ 2 :]))
2026-08-05 14:23:00 +02:00
case "diff" :
os . Exit ( cmdDiff ( os . Args [ 2 :]))
2026-08-05 16:41:00 +02:00
case "profile" :
os . Exit ( cmdProfile ( os . Args [ 2 :]))
2026-08-29 13:52:32 +02:00
case "audit-instructions" :
if err := cmdAuditInstructions ( os . Args [ 2 :]); err != nil {
fmt . Fprintln ( os . Stderr , err )
os . Exit ( 1 )
}
case "scaffold" :
if err := cmdScaffold ( os . Args [ 2 :]); err != nil {
fmt . Fprintln ( os . Stderr , err )
os . Exit ( 1 )
}
2026-07-06 09:49:50 +02:00
case "lsp" :
os . Exit ( cmdLSP ( os . Args [ 2 :]))
case "version" , "--version" , "-V" :
2026-07-13 19:50:38 +02:00
os . Exit ( cmdVersion ())
case "help" , "--help" , "-h" :
2026-07-06 09:49:50 +02:00
usage ( os . Stdout )
default :
2026-07-13 19:50:38 +02:00
fmt . Fprintf ( os . Stderr , "gasm: unknown command %q — run \"gasm --help\" for usage\n" , os . Args [ 1 ])
2026-07-06 09:49:50 +02:00
os . Exit ( 2 )
}
}
2026-07-13 19:50:38 +02:00
// cmdVersion prints the release version.
func cmdVersion () int {
fmt . Printf ( "gasm %s\n" , version )
return 0
}
2026-08-03 19:42:05 +02:00
// ANSI color helpers for terminal output.
const (
colorReset = "\033[0m"
colorBold = "\033[1m"
colorCyan = "\033[36m"
colorYellow = "\033[33m"
colorGray = "\033[90m"
)
// isTTY reports whether the writer is a terminal (for color output).
func isTTY ( w io . Writer ) bool {
if f , ok := w .( * os . File ); ok {
stat , _ := f . Stat ()
return ( stat . Mode () & os . ModeCharDevice ) != 0
}
return false
}
2026-07-06 09:49:50 +02:00
func usage ( w io . Writer ) {
2026-08-03 19:42:05 +02:00
useColor := isTTY ( w )
bold , cyan , yellow , gray , reset := "" , "" , "" , "" , ""
if useColor {
bold , cyan , yellow , gray , reset = colorBold , colorCyan , colorYellow , colorGray , colorReset
}
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
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" )
2026-07-06 09:49:50 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "%sUsage:%s\n" , yellow , reset )
fmt . Fprintf ( w , " gasm <command> [arguments]\n" )
fmt . Fprintf ( w , " gasm [flags]\n\n" )
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "%sCommands:%s\n" , yellow , reset )
commands := [] struct { name , desc string }{
{ "tokens" , "print the lexical token stream" },
{ "parse" , "parse and report syntax errors" },
{ "fmt" , "canonicalise formatting (gofmt for assembly)" },
{ "lint" , "run static checks" },
2026-08-21 00:50:31 +02:00
{ "asm" , "assemble .s files to machine code (amd64, arm64, riscv64, loong64)" },
{ "verify" , "JIT-assemble and run dynamic checks (amd64, arm64, riscv64, loong64)" },
{ "debug" , "interactive source-level debugger (amd64, arm64, riscv64, loong64)" },
2026-08-05 14:23:00 +02:00
{ "diff" , "compare machine code of two .s files" },
2026-08-05 16:41:00 +02:00
{ "profile" , "show basic-block structure of functions" },
2026-08-29 13:52:32 +02:00
{ "audit-instructions" , "diff the encoder against the Go toolchain's name table" },
{ "scaffold" , "generate a differential test skeleton for a kernel file" },
2026-08-03 19:42:05 +02:00
{ "lsp" , "run the language server over stdio" },
{ "version" , "print the version (same as --version)" },
}
for _ , c := range commands {
fmt . Fprintf ( w , " %s%-10s%s %s%s%s\n" , cyan , c . name , reset , gray , c . desc , reset )
}
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "\n%sFlags:%s\n" , yellow , reset )
fmt . Fprintf ( w , " %s-h, --help%s %sshow this help%s\n" , cyan , reset , gray , reset )
fmt . Fprintf ( w , " %s-V, --version%s %sprint the version%s\n" , cyan , reset , gray , reset )
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "\nRun \"gasm <command> -h\" for a command's usage and flags.\n\n" )
2026-07-13 19:50:38 +02:00
2026-08-03 19:42:05 +02:00
fmt . Fprintf ( w , "%sExamples:%s\n" , yellow , reset )
examples := [] struct { cmd , desc string }{
{ "gasm fmt" , "reformat every .s below the current directory" },
{ "gasm lint go-flac/*.s" , "run static checks over the kernels" },
{ "gasm asm -o k.bin kern_amd64.s" , "" },
{ "gasm asm --format elf -o k.o kern_amd64.s" , "" },
{ "gasm asm --format goobj -p pkg/path -o k.o kern_amd64.s" , "" },
}
for _ , e := range examples {
if e . desc != "" {
fmt . Fprintf ( w , " %s%s%s %s%s%s\n" , cyan , e . cmd , reset , gray , e . desc , reset )
} else {
fmt . Fprintf ( w , " %s%s%s\n" , cyan , e . cmd , reset )
}
}
2026-07-06 09:49:50 +02:00
}
2026-07-13 19:50:38 +02:00
// newCommand returns the FlagSet of a subcommand whose -h/--help prints a
// proper usage block: the one-line usage, the long description and the flag
// defaults. The flag package routes -h/--help to fs.Usage and exits 0.
func newCommand ( name , usageLine , long string ) * flag . FlagSet {
fs := flag . NewFlagSet ( name , flag . ExitOnError )
fs . Usage = func () {
w := fs . Output ()
fmt . Fprintf ( w , "Usage: %s\n\n%s\n" , usageLine , strings . TrimSpace ( long ))
hasFlags := false
fs . VisitAll ( func ( * flag . Flag ) { hasFlags = true })
if hasFlags {
fmt . Fprintln ( w , "\nFlags:" )
fs . PrintDefaults ()
}
}
return fs
}
2026-07-06 09:49:50 +02:00
// readSource returns the contents of path, or stdin when path is "-".
func readSource ( path string ) ( string , error ) {
if path == "-" {
b , err := io . ReadAll ( os . Stdin )
return string ( b ), err
}
b , err := os . ReadFile ( path )
return string ( b ), err
}
func cmdTokens ( args [] string ) int {
2026-07-13 19:50:38 +02:00
fs := newCommand ( "tokens" , "gasm tokens <file>" , `
Print the lexical token stream of FILE: position, token kind and text, one
token per line. FILE may be "-" to read standard input.
` )
2026-07-06 09:49:50 +02:00
fs . Parse ( args )
if fs . NArg () != 1 {
fmt . Fprintln ( os . Stderr , "usage: gasm tokens <file>" )
return 2
}
src , err := readSource ( fs . Arg ( 0 ))
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
return 1
}
for _ , tok := range lexer . Tokenize ( src ) {
fmt . Printf ( "%s\t%s\t%q\n" , tok . Pos , tok . Kind , tok . Text )
}
return 0
}
func cmdParse ( args [] string ) int {
2026-07-13 19:50:38 +02:00
fs := newCommand ( "parse" , "gasm parse <file>" , `
Parse FILE and report syntax errors on stderr. On success, print how many
declarations and TEXT functions the file contains. FILE may be "-" to read
standard input.
` )
2026-07-06 09:49:50 +02:00
fs . Parse ( args )
if fs . NArg () != 1 {
fmt . Fprintln ( os . Stderr , "usage: gasm parse <file>" )
return 2
}
path := fs . Arg ( 0 )
src , err := readSource ( path )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
return 1
}
file , errs := parser . Parse ( path , src )
for _ , e := range errs {
fmt . Fprintf ( os . Stderr , "%s: %v\n" , path , e )
}
if len ( errs ) > 0 {
return 1
}
funcs := 0
for _ , d := range file . Decls {
if _ , ok := d .( * ast . Text ); ok {
funcs ++
}
}
fmt . Printf ( "%s: OK — %d declarations, %d functions\n" , path , len ( file . Decls ), funcs )
return 0
}
func cmdFmt ( args [] string ) int {
2026-07-13 19:50:38 +02:00
fs := newCommand ( "fmt" , "gasm fmt [-w] [path...]" , `
Canonicalise the formatting of Plan 9 assembly sources: indentation, operand
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
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.
` )
write := fs . Bool ( "w" , false , "write result to the source file" )
fs . Parse ( args )
2026-07-12 21:24:41 +02:00
// Like go fmt: with no arguments, or with a directory argument, every .s
// file below the directory is formatted in place and the names of the
// changed files are listed; explicit file arguments keep the -w / stdout
// behaviour.
2026-07-13 19:50:38 +02:00
paths := fs . Args ()
2026-07-12 21:24:41 +02:00
dirMode := len ( paths ) == 0
if dirMode {
paths = [] string { "." }
}
var files [] string
for _ , p := range paths {
info , err := os . Stat ( p )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
return 1
}
if info . IsDir () {
dirMode = true
found , err := asmFiles ( p )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
return 1
}
files = append ( files , found ... )
continue
}
files = append ( files , p )
2026-07-06 09:49:50 +02:00
}
rc := 0
2026-07-12 21:24:41 +02:00
for _ , path := range files {
2026-07-06 09:49:50 +02:00
src , err := readSource ( path )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
rc = 1
continue
}
out := format . Source ( path , src )
2026-07-12 21:24:41 +02:00
if dirMode || * write {
2026-07-06 09:49:50 +02:00
if out != src {
if err := os . WriteFile ( path , [] byte ( out ), 0 o644 ); err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
rc = 1
2026-07-12 21:24:41 +02:00
continue
}
if dirMode {
fmt . Println ( path )
2026-07-06 09:49:50 +02:00
}
}
continue
}
fmt . Print ( out )
}
return rc
}
2026-07-12 21:24:41 +02:00
// asmFiles collects the .s files below dir, skipping directories whose name
// 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
err := filepath . WalkDir ( dir , func ( path string , d fs . DirEntry , err error ) error {
if err != nil {
return err
}
if d . IsDir () {
if path != dir && ( strings . HasPrefix ( d . Name (), "." ) || strings . HasPrefix ( d . Name (), "_" )) {
return filepath . SkipDir
}
return nil
}
if strings . HasSuffix ( d . Name (), ".s" ) {
out = append ( out , path )
}
return nil
})
return out , err
}
2026-07-06 09:49:50 +02:00
func cmdLint ( args [] string ) int {
2026-07-13 19:50:38 +02:00
fs := newCommand ( "lint" , "gasm lint <file...>" , `
Run the static checks over the given files and print diagnostics as
"file:line:col: severity: message [code]". The exit status is non-zero when
an error-severity diagnostic is found; warnings (e.g. the register-clobber
audit) do not affect it.
Rules include unknown-instruction, operand-count, undefined-label,
duplicate-label, missing-ret, missing-textflag-include, abi-argsize,
unreachable-code, register-clobber and funcdata-pcdata.
` )
2026-07-06 09:49:50 +02:00
disable := fs . String ( "disable" , "" , "comma-separated rule codes to disable" )
fs . Parse ( args )
if fs . NArg () == 0 {
fmt . Fprintln ( os . Stderr , "usage: gasm lint <file...>" )
return 2
}
disabled := map [ string ] bool {}
2026-08-29 15:40:31 +02:00
for code := range strings . SplitSeq ( * disable , "," ) {
2026-07-06 09:49:50 +02:00
if code = strings . TrimSpace ( code ); code != "" {
disabled [ code ] = true
}
}
hadError := false
for _ , path := range fs . Args () {
src , err := readSource ( path )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , err )
hadError = true
continue
}
file , errs := parser . Parse ( path , src )
for _ , e := range errs {
fmt . Fprintf ( os . Stderr , "%s: %v\n" , path , e )
hadError = true
}
diags := lint . File ( file , lint . Config { Arch : arch . FromFilename ( path ), Disable : disabled })
for _ , d := range diags {
fmt . Printf ( "%s:%d:%d: %s: %s [%s]\n" , path , d . Pos . Line , d . Pos . Column , d . Severity , d . Message , d . Code )
if d . Severity == lint . Error {
hadError = true
}
}
}
if hadError {
return 1
}
return 0
}
func cmdLSP ( args [] string ) int {
2026-07-13 19:50:38 +02:00
fs := newCommand ( "lsp" , "gasm lsp" , `
Run the language server over standard input/output: JSON-RPC 2.0 with
Content-Length framing. Point an LSP-capable editor at the binary and
associate it with .s files; the target architecture is inferred from the file
suffix (_amd64.s, _arm64.s, _riscv64.s, _loong64.s). Provides completion,
hover, document symbols, diagnostics and semantic-token highlighting.
` )
2026-07-06 09:49:50 +02:00
fs . Parse ( args )
srv := lsp . New ( os . Stdin , os . Stdout )
if err := srv . Run (); err != nil {
fmt . Fprintln ( os . Stderr , "gasm lsp:" , err )
return 1
}
return 0
}
func cmdAsm ( args [] string ) int {
2026-08-07 22:20:26 +02:00
fs := newCommand ( "asm" , "gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>" , `
2026-08-07 22:43:40 +02:00
Assemble FILE without the Go toolchain: every TEXT function is encoded to
machine code and printed as a hex dump. Supported architectures: amd64
2026-08-20 22:55:49 +02:00
(including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer, FP,
conditional select, CRC32, and MOV pseudo), riscv64 (RV64IMAFDC + RVC)
and loong64 (LoongArch base ISA).
2026-07-16 20:52:20 +02:00
With -o the output is written to a file instead. The --format flag selects
what is written: raw (the default) concatenates the functions and the data
2026-08-07 22:20:26 +02:00
section into one self-consistent image; elf emits a relocatable object
(.text/.data sections, a symbol table and one PC32 relocation per
2026-07-17 18:57:04 +02:00
static-symbol reference) that links with the system toolchain; goobj emits
the Go toolchain's own object format, which cmd/link consumes directly (it
requires -p, the package path, and the installed Go toolchain).
2026-07-13 19:50:38 +02:00
` )
2026-07-16 20:52:20 +02:00
out := fs . String ( "o" , "" , "write the output to this file" )
2026-08-07 22:20:26 +02:00
format := fs . String ( "format" , "raw" , "output format: raw (concatenated image), elf or goobj (Go object)" )
2026-07-17 18:57:04 +02:00
pkg := fs . String ( "p" , "" , "package path for --format goobj (qualifies the exported symbols)" )
2026-07-06 09:49:50 +02:00
fs . Parse ( args )
if fs . NArg () != 1 {
2026-08-07 22:20:26 +02:00
fmt . Fprintln ( os . Stderr , "usage: gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>" )
2026-07-06 09:49:50 +02:00
return 2
}
path := fs . Arg ( 0 )
2026-08-02 00:18:00 +02:00
targetArch := arch . FromFilename ( path )
2026-07-06 09:49:50 +02:00
src , err := readSource ( path )
if err != nil {
fmt . Fprintln ( os . Stderr , "gasm:" , 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
}
2026-08-07 22:43:40 +02:00
img , err := assembleFile ( path , targetArch , f )
2026-07-09 15:56:03 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "%s: %v\n" , path , err )
return 1
}
if len ( img . Funcs ) == 0 {
fmt . Fprintln ( os . Stderr , "gasm asm: no assemblable TEXT functions found" )
return 1
}
for _ , fn := range img . Funcs {
code := img . Code [ fn . Offset : fn . Offset + fn . Size ]
fmt . Printf ( "%s: %d bytes\n" , fn . Name , fn . Size )
2026-07-06 09:49:50 +02:00
for i := 0 ; i < len ( code ); i += 16 {
2026-08-29 15:40:31 +02:00
end := min ( i + 16 , len ( code ))
2026-07-06 09:49:50 +02:00
fmt . Printf ( " %04x:" , i )
for _ , b := range code [ i : end ] {
fmt . Printf ( " %02x" , b )
}
fmt . Println ()
}
}
2026-07-09 15:56:03 +02:00
if len ( img . Data ) > 0 {
fmt . Printf ( "data: %d bytes at 0x%x\n" , len ( img . Data ), len ( img . Code ))
for _ , d := range f . Decls {
g , ok := d .( * ast . Globl )
if ! ok || g . Name == nil || g . Name . Pseudo != "SB" {
continue
}
size := 0
if g . Size != nil && g . Size . Imm . HasVal {
size = int ( g . Size . Imm . Val )
}
fmt . Printf ( " %s: %d bytes at 0x%x\n" , g . Name . Name , size , img . Symbols [ g . Name . Name ])
}
for i := 0 ; i < len ( img . Data ); i += 16 {
2026-08-29 15:40:31 +02:00
end := min ( i + 16 , len ( img . Data ))
2026-07-09 15:56:03 +02:00
fmt . Printf ( " %04x:" , len ( img . Code ) + i )
for _ , b := range img . Data [ i : end ] {
fmt . Printf ( " %02x" , b )
}
fmt . Println ()
}
2026-07-06 09:49:50 +02:00
}
if * out != "" {
2026-07-16 20:52:20 +02:00
var obj [] byte
var err error
var kind string
switch * format {
case "raw" :
if len ( img . Externals ) > 0 {
2026-08-07 22:20:26 +02:00
fmt . Fprintf ( os . Stderr , "gasm asm: external symbol %q needs an object file (use --format elf)\n" , img . Externals [ 0 ])
2026-07-16 20:52:20 +02:00
return 1
}
obj , kind = img . Bytes (), "raw image"
case "elf" :
2026-08-13 11:24:44 +02:00
switch targetArch {
case arch . RISCV :
2026-08-03 08:51:00 +02:00
obj , err = img . ELFRISCVObject ()
2026-08-13 11:24:44 +02:00
case arch . LOONG64 :
obj , err = img . ELFLOONG64Object ()
2026-08-20 13:32:52 +02:00
case arch . ARM64 :
obj , err = img . ELFAARCH64Object ()
2026-08-13 11:24:44 +02:00
default :
2026-08-03 08:51:00 +02:00
obj , err = img . ELFObject ()
}
2026-07-16 20:52:20 +02:00
kind = "ELF object"
2026-07-17 18:57:04 +02:00
case "goobj" :
2026-08-13 11:24:44 +02:00
switch targetArch {
case arch . RISCV :
2026-08-05 09:27:00 +02:00
obj , err = img . GOObjectRISCV ( * pkg , path )
2026-08-13 11:24:44 +02:00
case arch . LOONG64 :
obj , err = img . GOObjectLOONG64 ( * pkg , path )
2026-08-20 13:32:52 +02:00
case arch . ARM64 :
obj , err = img . GOObjectAARCH64 ( * pkg , path )
2026-08-13 11:24:44 +02:00
default :
2026-08-05 09:27:00 +02:00
obj , err = img . GOObject ( * pkg , path )
}
2026-07-17 18:57:04 +02:00
kind = "Go object"
2026-07-16 20:52:20 +02:00
default :
2026-08-07 22:20:26 +02:00
fmt . Fprintf ( os . Stderr , "gasm asm: unknown format %q (want raw, elf or goobj)\n" , * format )
2026-07-16 20:52:20 +02:00
return 2
}
if err != nil {
2026-07-06 09:49:50 +02:00
fmt . Fprintln ( os . Stderr , "gasm asm:" , err )
return 1
}
2026-07-16 20:52:20 +02:00
if err := os . WriteFile ( * out , obj , 0 o644 ); err != nil {
fmt . Fprintln ( os . Stderr , "gasm asm:" , err )
return 1
}
fmt . Printf ( "wrote %d bytes to %s (%s)\n" , len ( obj ), * out , kind )
2026-07-06 09:49:50 +02:00
}
return 0
}
2026-07-22 09:28:11 +02:00
2026-08-05 14:23:00 +02:00
// 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.
2026-08-05 20:20:47 +02:00
Use --map to compare functions whose names differ between the files,
e.g. --map wideCopyAVX2=wideCopyAVX512 pairs the two regardless of suffix.
2026-08-05 14:23:00 +02:00
` )
2026-08-05 20:20:47 +02:00
mapSpec := fs . String ( "map" , "" , "comma-separated old=new pairs to match functions with different names" )
2026-08-05 14:23:00 +02:00
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 )
2026-08-05 20:20:47 +02:00
// Parse the name mapping (file1 name → file2 name).
nameMap := make ( map [ string ] string )
if * mapSpec != "" {
2026-08-29 15:40:31 +02:00
for pair := range strings . SplitSeq ( * mapSpec , "," ) {
2026-08-05 20:20:47 +02:00
old , new , ok := strings . Cut ( pair , "=" )
if ! ok || old == "" || new == "" {
fmt . Fprintf ( os . Stderr , "gasm diff: invalid --map pair %q (expected old=new)\n" , pair )
return 2
}
nameMap [ old ] = new
}
}
2026-08-05 14:23:00 +02:00
// Assemble both files.
2026-08-07 22:43:40 +02:00
img1 , err := assemblePath ( path1 )
2026-08-05 14:23:00 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm diff: %s: %v\n" , path1 , err )
return 1
}
2026-08-07 22:43:40 +02:00
img2 , err := assemblePath ( path2 )
2026-08-05 14:23:00 +02:00
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm diff: %s: %v\n" , path2 , err )
return 1
}
2026-08-05 20:20:47 +02:00
// Compare functions by name, honouring the --map overrides.
2026-08-05 14:23:00 +02:00
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 ]
}
2026-08-05 20:20:47 +02:00
// Track which file2 functions were consumed (by direct match or via --map)
// so the "only in file2" pass skips them.
matched2 := make ( map [ string ] bool )
2026-08-05 14:23:00 +02:00
diffs := 0
for name , code1 := range funcs1 {
2026-08-05 20:20:47 +02:00
target := name
if mapped , ok := nameMap [ name ]; ok {
target = mapped
}
code2 , ok := funcs2 [ target ]
2026-08-05 14:23:00 +02:00
if ! ok {
fmt . Printf ( "%s: only in %s\n" , name , path1 )
diffs ++
continue
}
2026-08-05 20:20:47 +02:00
matched2 [ target ] = true
label := name
if target != name {
label = name + " → " + target
}
2026-08-05 14:23:00 +02:00
if ! bytes . Equal ( code1 , code2 ) {
2026-08-05 20:20:47 +02:00
fmt . Printf ( "%s: DIFFERS (%d vs %d bytes)\n" , label , len ( code1 ), len ( code2 ))
2026-08-05 14:23:00 +02:00
printByteDiff ( code1 , code2 )
diffs ++
} else {
2026-08-05 20:20:47 +02:00
fmt . Printf ( "%s: identical (%d bytes)\n" , label , len ( code1 ))
2026-08-05 14:23:00 +02:00
}
}
for name := range funcs2 {
2026-08-05 20:20:47 +02:00
if ! matched2 [ name ] {
2026-08-05 14:23:00 +02:00
fmt . Printf ( "%s: only in %s\n" , name , path2 )
diffs ++
}
}
if diffs == 0 {
fmt . Println ( "all functions identical" )
return 0
}
return 1
}
2026-08-07 22:43:40 +02:00
// assembleFile assembles a parsed file for the given architecture and returns the image.
func assembleFile ( path string , targetArch arch . Arch , f * ast . File ) ( * asm . Image , error ) {
switch targetArch {
case arch . AMD64 :
return asm . AssembleFile ( f )
case arch . RISCV :
return asm . AssembleFileRISCV ( f )
case arch . ARM64 :
return asm . AssembleFileARM64 ( f )
case arch . LOONG64 :
return asm . AssembleFileLOONG64 ( f )
default :
return nil , fmt . Errorf ( "unsupported architecture %q" , targetArch )
}
}
// assemblePath reads, parses and assembles a file (used by cmdDiff).
func assemblePath ( path string ) ( * asm . Image , error ) {
2026-08-05 14:23:00 +02:00
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" )
}
2026-08-07 22:43:40 +02:00
return assembleFile ( path , arch . FromFilename ( path ), f )
2026-08-05 14:23:00 +02:00
}
// printByteDiff shows the first few byte differences between two code blocks.
func printByteDiff ( a , b [] byte ) {
2026-08-29 15:40:31 +02:00
maxLen := min ( len ( b ), len ( a ))
2026-08-05 14:23:00 +02:00
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 ))
}
}
2026-08-05 16:41:00 +02:00
// cmdProfile shows the basic-block structure of functions in an assembly file.
func cmdProfile ( args [] string ) int {
fs := newCommand ( "profile" , "gasm profile <file.s>" , `
Show the basic-block structure of functions in an assembly file.
Lists each function's labels, their offsets, and the block boundaries.
This is the static structure; for runtime execution counts, use
gasm verify --fuzz which exercises the code paths.
` )
fs . Parse ( args )
if fs . NArg () != 1 {
fmt . Fprintln ( os . Stderr , "usage: gasm profile <file.s>" )
return 2
}
path := fs . Arg ( 0 )
// Load the file to get function metadata.
k , err := verify . Load ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm profile: %v\n" , err )
return 1
}
defer k . Close ()
for _ , name := range k . FuncNames () {
fl , err := k . Func ( name )
if err != nil {
continue
}
fmt . Printf ( "%s: %d bytes, args=%d, frame=%d" , name , fl . Size , fl . Args , fl . Frame )
if fl . NoSplit {
fmt . Printf ( " NOSPLIT" )
}
fmt . Println ()
// Show labels and their offsets.
if len ( fl . Labels ) > 0 {
fmt . Println ( " labels:" )
// Sort labels by offset.
type labelOff struct {
name string
off int
}
var labels [] labelOff
for name , off := range fl . Labels {
labels = append ( labels , labelOff { name , off })
}
sort . Slice ( labels , func ( i , j int ) bool { return labels [ i ]. off < labels [ j ]. off })
for _ , l := range labels {
fmt . Printf ( " %-20s +%#04x\n" , l . name , l . off )
}
}
// Show basic blocks.
blocks , err := k . Blocks ( name )
if err == nil && len ( blocks ) > 0 {
fmt . Printf ( " basic blocks: %d\n" , len ( blocks ))
}
}
return 0
}
2026-08-03 08:51:00 +02:00
// 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 {
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 . AssembleFileRISCV ( f )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
if groundTruth {
gt , err := verify . GroundTruthRISCV ( 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
}
2026-08-13 11:24:44 +02:00
// 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
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
}
2026-08-20 13:32:52 +02:00
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
}
2026-07-22 09:28:11 +02:00
func cmdVerify ( args [] string ) int {
2026-08-05 20:46:39 +02:00
fs := newCommand ( "verify" , "gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>" , `
2026-07-22 09:28:11 +02:00
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
testing.
With -smoke, each NOSPLIT function is called with a zeroed argument block to
confirm the JIT trampoline works end-to-end. This is safe only for functions
that tolerate nil pointers and zero lengths in their arguments.
2026-07-26 16:13:11 +02:00
With -abi, each function is called with sentinel values in the callee-saved
registers (BP, R14) and a red-zone canary below SP; violations are reported.
2026-08-05 20:46:39 +02:00
With -fuzz, each function with a // func signature is differentially fuzzed
against the go-tool-asm version in a subprocess (so a crash on a partial
function is reported, not fatal).
With -ground-truth, the assembled machine code is compared byte-for-byte
against go tool asm (relocation sites masked), reporting any encoding drift.
2026-07-26 16:13:11 +02:00
With -profile, the static basic-block structure is listed for each function.
2026-08-05 20:46:39 +02:00
With -call, a single function is invoked with user-supplied buffers (-buf)
instead of the smoke/abi/fuzz sweeps. Useful for partial functions (e.g.
decoders) that crash on random input but should succeed on valid data.
2026-07-22 09:28:11 +02:00
` )
smoke := fs . Bool ( "smoke" , false , "call each NOSPLIT function with zeroed args" )
2026-07-26 16:13:11 +02:00
abi := fs . Bool ( "abi" , false , "run ABI-checking calls (sentinel registers + red zone)" )
2026-08-04 22:26:04 +02:00
abiN := fs . Int ( "abi-n" , 100 , "number of ABI check iterations with varied inputs" )
2026-07-26 16:13:11 +02:00
profile := fs . Bool ( "profile" , false , "list basic-block structure per function" )
2026-07-30 12:55:54 +02:00
groundTruth := fs . Bool ( "ground-truth" , false , "compare machine code byte-for-byte against go tool asm" )
2026-07-31 17:09:50 +02:00
fuzz := fs . Bool ( "fuzz" , false , "differential fuzz: JIT both gasm and go-tool-asm versions, compare outputs" )
fuzzN := fs . Int ( "n" , 1000 , "number of fuzz iterations per function" )
2026-08-24 21:01:32 +02:00
fuzzOne := fs . String ( "fuzz-one" , "" , "" ) // hidden: fuzz a single function (subprocess mode)
sweepOne := fs . String ( "sweep-one" , "" , "" ) // hidden: smoke/abi a single function (subprocess mode)
2026-08-05 20:46:39 +02:00
call := fs . String ( "call" , "" , "call a single function with -buf instead of the sweeps" )
bufSpec := fs . String ( "buf" , "" , "buffer spec for -call: name:size:pattern[,name:size:pattern] (zero, ones, seq, or hex)" )
2026-08-29 14:03:45 +02:00
scalarSpec := fs . String ( "args" , "" , "scalar args for -call: name=value[,name=value] (decimal or 0x hex)" )
2026-08-05 20:46:39 +02:00
repeat := fs . Int ( "repeat" , 1 , "number of times to repeat a -call invocation" )
2026-07-22 09:28:11 +02:00
fs . Parse ( args )
if fs . NArg () != 1 {
2026-08-05 20:46:39 +02:00
fmt . Fprintln ( os . Stderr , "usage: gasm verify [-smoke] [-abi] [-fuzz] [-ground-truth] [-profile] [-call] <file.s>" )
2026-07-22 09:28:11 +02:00
return 2
}
path := fs . Arg ( 0 )
2026-08-03 08:51:00 +02:00
targetArch := arch . FromFilename ( path )
2026-08-13 11:24:44 +02:00
switch targetArch {
case arch . AMD64 :
// JIT-based verification below.
case arch . RISCV :
// RISC-V: ground-truth only (no JIT on non-RISC-V hosts).
2026-08-03 08:51:00 +02:00
return cmdVerifyRISCV ( path , * groundTruth , * profile )
2026-08-13 11:24:44 +02:00
case arch . LOONG64 :
// LoongArch: ground-truth only (no JIT on non-LoongArch hosts).
return cmdVerifyLOONG64 ( path , * groundTruth , * profile )
2026-08-20 13:32:52 +02:00
case arch . ARM64 :
// AArch64: ground-truth only (no JIT on non-ARM64 hosts).
return cmdVerifyARM64 ( path , * groundTruth , * profile )
2026-08-13 11:24:44 +02:00
default :
2026-08-21 00:50:31 +02:00
fmt . Fprintln ( os . Stderr , "gasm verify: unsupported architecture" )
2026-08-13 11:24:44 +02:00
return 1
2026-08-03 08:51:00 +02:00
}
2026-07-22 09:28:11 +02:00
k , err := verify . Load ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
defer k . Close ()
names := k . FuncNames ()
fmt . Printf ( "%s: %d functions JIT-loaded\n" , path , len ( names ))
rc := 0
2026-07-30 12:55:54 +02:00
2026-08-05 20:46:39 +02:00
// Single-function call mode: invoke one function with user-supplied buffers.
if * call != "" {
2026-08-29 14:03:45 +02:00
return cmdVerifyCall ( k , path , * call , * bufSpec , * scalarSpec , * repeat )
2026-08-05 20:46:39 +02:00
}
2026-08-01 18:08:10 +02:00
// Subprocess mode: fuzz a single function and exit.
if * fuzzOne != "" {
gt , err := verify . GroundTruth ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
src , err := readSource ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
sigs := verify . ExtractSignatures ( src )
sig , ok := sigs [ * fuzzOne ]
if ! ok {
fmt . Printf ( "%s: no signature\n" , * fuzzOne )
return 0
}
goCode , ok := gt [ * fuzzOne ]
if ! ok {
fmt . Printf ( "%s: not in go tool asm\n" , * fuzzOne )
return 0
}
res := k . FuzzFunc ( * fuzzOne , sig , goCode , * fuzzN , 42 )
fmt . Printf ( "%s\n" , res )
if ! res . OK () {
return 1
}
return 0
}
2026-08-24 21:01:32 +02:00
// Subprocess mode: run the smoke/abi checks for a single function and
// exit with the accumulated status. The parent interprets a clean exit
// as success, a non-zero exit as failure and death-by-signal as a crash.
if * sweepOne != "" {
fl , err := k . Func ( * sweepOne )
if err != nil || ! fl . NoSplit {
fmt . Fprintf ( os . Stderr , "gasm verify: %s: %v\n" , * sweepOne , err )
return 1
}
msgs , failed := runSweepChecks ( k , path , * sweepOne , fl , * smoke , * abi , * abiN )
for _ , m := range msgs {
fmt . Println ( m )
}
if failed {
return 1
}
return 0
}
2026-07-30 12:55:54 +02:00
// Ground-truth comparison: assemble with go tool asm and compare bytes.
if * groundTruth {
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
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
}
}
rc = 1
}
}
fmt . Printf ( "ground truth: %d/%d functions byte-identical\n" , matched , total )
if matched < total {
rc = 1
}
}
2026-07-31 17:09:50 +02:00
// Differential fuzz: JIT both gasm and go-tool-asm, compare outputs.
// Each function runs in a subprocess so a crash (partial functions like
// decoders that fault on malformed input) doesn't kill the whole run.
if * fuzz {
gt , err := verify . GroundTruth ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: fuzz: %v\n" , err )
return 1
}
src , err := readSource ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
sigs := verify . ExtractSignatures ( src )
fuzzed := 0
for _ , name := range names {
sig , ok := sigs [ name ]
if ! ok {
fmt . Printf ( " %s: SKIP (no // func signature)\n" , name )
continue
}
goCode , ok := gt [ name ]
if ! ok {
fmt . Printf ( " %s: SKIP (not in go tool asm output)\n" , name )
continue
}
2026-08-01 18:08:10 +02:00
// Run in a subprocess: if the function crashes on random
// input (partial function), we report it and move on.
res := fuzzInSubprocess ( path , name , * fuzzN )
if res != "" {
fmt . Printf ( " %s\n" , res )
if strings . Contains ( res , "MISMATCH" ) {
rc = 1
}
2026-07-31 17:09:50 +02:00
}
2026-08-01 18:08:10 +02:00
_ = sig
_ = goCode
2026-07-31 17:09:50 +02:00
fuzzed ++
}
fmt . Printf ( "fuzz: %d functions tested, %d iterations each\n" , fuzzed , * fuzzN )
}
2026-08-21 01:10:14 +02:00
// Print function info and profile.
2026-07-22 09:28:11 +02:00
for _ , name := range names {
fl , _ := k . Func ( name )
flags := ""
if fl . NoSplit {
flags = " NOSPLIT"
}
fmt . Printf ( " %s: %d bytes, args=%d, frame=%d%s\n" , name , fl . Size , fl . Args , fl . Frame , flags )
2026-07-26 16:13:11 +02:00
if * profile {
blocks , err := k . Blocks ( name )
if err != nil {
fmt . Printf ( " profile: %v\n" , err )
} else {
fmt . Printf ( " blocks: %d\n" , len ( blocks ))
}
}
2026-08-21 01:10:14 +02:00
}
2026-07-26 16:13:11 +02:00
2026-08-24 21:01:32 +02:00
// 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
// CRASH line instead of killing this process (mirrors fuzzInSubprocess).
2026-08-21 01:10:14 +02:00
if * smoke || * abi {
type checkResult struct {
name string
msg string
fail bool
2026-07-22 09:28:11 +02:00
}
2026-08-21 01:10:14 +02:00
var mu sync . Mutex
var results [] checkResult
sem := make ( chan struct {}, runtime . NumCPU ())
var wg sync . WaitGroup
2026-07-26 16:13:11 +02:00
2026-08-21 01:10:14 +02:00
for _ , name := range names {
fl , _ := k . Func ( name )
if ! fl . NoSplit {
continue
}
wg . Add ( 1 )
2026-08-24 21:01:32 +02:00
go func ( name string ) {
2026-08-21 01:10:14 +02:00
defer wg . Done ()
sem <- struct {}{}
defer func () { <- sem }()
2026-08-24 21:01:32 +02:00
msg , fail := sweepInSubprocess ( path , name , * smoke , * abi , * abiN )
2026-08-21 01:10:14 +02:00
mu . Lock ()
2026-08-24 21:01:32 +02:00
results = append ( results , checkResult { name : name , msg : msg , fail : fail })
2026-08-21 01:10:14 +02:00
mu . Unlock ()
2026-08-24 21:01:32 +02:00
}( name )
2026-08-21 01:10:14 +02:00
}
wg . Wait ()
// Print results in source order.
resultMap := make ( map [ string ] checkResult , len ( results ))
for _ , r := range results {
resultMap [ r . name ] = r
}
for _ , name := range names {
if r , ok := resultMap [ name ]; ok {
fmt . Println ( r . msg )
if r . fail {
2026-08-04 22:26:04 +02:00
rc = 1
}
2026-07-26 16:13:11 +02:00
}
}
2026-07-22 09:28:11 +02:00
}
return rc
}
2026-08-01 18:08:10 +02:00
// fuzzInSubprocess runs the fuzz for a single function in a child process.
// If the child is killed by a signal (e.g. SIGSEGV from a partial function
// faulting on random input), it returns a CRASH report instead of dying.
func fuzzInSubprocess ( path , funcName string , n int ) string {
self , err := os . Executable ()
if err != nil {
return fmt . Sprintf ( "%s: cannot find self: %v" , funcName , err )
}
cmd := exec . Command ( self , "verify" , "--fuzz-one=" + funcName , "-n" , strconv . Itoa ( n ), path )
out , err := cmd . CombinedOutput ()
if err != nil {
// Check if the child was killed by a signal.
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 ())
}
}
// Non-zero exit without a signal: the fuzz reported mismatches.
2026-08-29 15:40:31 +02:00
lines := strings . SplitSeq ( strings . TrimSpace ( string ( out )), "\n" )
for l := range lines {
2026-08-01 18:08:10 +02:00
if strings . Contains ( l , funcName ) {
return strings . TrimSpace ( l )
}
}
return fmt . Sprintf ( "%s: FAIL (exit %v)" , funcName , err )
}
// Success: extract the result line.
2026-08-29 15:40:31 +02:00
lines := strings . SplitSeq ( strings . TrimSpace ( string ( out )), "\n" )
for l := range lines {
2026-08-01 18:08:10 +02:00
if strings . Contains ( l , funcName ) {
return strings . TrimSpace ( l )
}
}
return strings . TrimSpace ( string ( out ))
}
2026-08-05 20:46:39 +02:00
2026-08-24 21:01:32 +02:00
// 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
// 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
}
args := [] string { "verify" , "--sweep-one=" + funcName }
if smoke {
args = append ( args , "-smoke" )
}
if abi {
args = append ( args , "-abi" , "-abi-n" , strconv . Itoa ( abiN ))
}
args = append ( args , path )
cmd := exec . Command ( self , args ... )
out , err := cmd . CombinedOutput ()
if err != nil {
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
}
}
// Non-zero exit without a signal: the checks themselves failed and
// the child already printed the diagnostic lines.
return sweepCheckLines ( out ), true
}
return sweepCheckLines ( out ), false
}
// sweepCheckLines extracts the check-result lines from child output,
// dropping the child's own file/function banners (the parent prints those).
func sweepCheckLines ( out [] byte ) string {
var lines [] string
2026-08-29 15:40:31 +02:00
for l := range strings . SplitSeq ( string ( out ), "\n" ) {
2026-08-24 21:01:32 +02:00
t := strings . TrimSpace ( l )
if strings . HasPrefix ( t , "smoke:" ) || strings . HasPrefix ( t , "abi:" ) {
lines = append ( lines , " " + t )
}
}
return strings . Join ( lines , "\n" )
}
// runSweepChecks performs the in-process smoke and ABI checks for one
// 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
if smoke {
args := make ([] byte , fl . Args )
_ , err := k . CallFunc ( name , args )
if err != nil {
msgs = append ( msgs , fmt . Sprintf ( " smoke: FAIL — %v" , err ))
failed = true
} else {
msgs = append ( msgs , " smoke: OK" )
}
}
if abi {
if src , err := readSource ( path ); err == nil {
result := k . FuzzFuncCheckedByName ( name , src , abiN , int64 ( abiN ))
if result . Mismatches > 0 {
msgs = append ( msgs , fmt . Sprintf ( " abi: %s" , result ))
failed = true
} else {
msgs = append ( msgs , fmt . Sprintf ( " abi: clean (%d varied inputs)" , result . Matches ))
}
} else {
args := make ([] byte , fl . Args )
_ , report , err := k . CallFuncChecked ( name , args )
if err != nil {
msgs = append ( msgs , fmt . Sprintf ( " abi: FAIL — %v" , err ))
failed = true
} else if ! report . OK () {
msgs = append ( msgs , fmt . Sprintf ( " abi: %s" , report ))
failed = true
} else {
msgs = append ( msgs , " abi: clean" )
}
}
}
return msgs , failed
}
2026-08-29 14:03:45 +02:00
// cmdVerifyCall implements `gasm verify --call <func> [--buf spec] [--args spec] [--repeat n]`.
// It invokes a single function with user-supplied buffers and scalar
// arguments and prints the arg block before and after the call, so the user
// can inspect return values and any output written to the buffers.
func cmdVerifyCall ( k * verify . Kernel , path , funcName , bufSpec , scalarSpec string , repeat int ) int {
2026-08-05 20:46:39 +02:00
fl , err := k . Func ( funcName )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
if ! fl . NoSplit {
fmt . Fprintf ( os . Stderr , "gasm verify: %s is not NOSPLIT (frame=%d); --call supports NOSPLIT functions only\n" , funcName , fl . Frame )
return 1
}
// Parse the // func signature to lay out the argument block.
src , err := readSource ( path )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
sig , ok := verify . ExtractFuncSig ( src , funcName )
if ! ok {
fmt . Fprintf ( os . Stderr , "gasm verify: no // func signature found for %s\n" , funcName )
return 1
}
layout := verify . ArgLayout ( sig )
// Allocate the requested buffers (if any) and build the arg block.
specs , err := verify . ParseBufSpec ( bufSpec )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
var pool verify . BufPool
if err := pool . Alloc ( specs ); err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
defer pool . Close ()
args := pool . BuildArgs ( layout , fl . Args )
2026-08-29 14:03:45 +02:00
scalars , err := verify . ParseScalarArgs ( scalarSpec )
if err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
if err := verify . ApplyScalarArgs ( args , layout , scalars ); err != nil {
fmt . Fprintf ( os . Stderr , "gasm verify: %v\n" , err )
return 1
}
2026-08-05 20:46:39 +02:00
fmt . Printf ( "%s: %d bytes, args=%d\n" , funcName , fl . Size , fl . Args )
fmt . Printf ( " signature: func %s(%s) %s\n" , sig . Name , formatParams ( sig . Params ), formatResults ( sig . Results ))
if len ( specs ) > 0 {
fmt . Printf ( " buffers:\n" )
for _ , s := range specs {
fmt . Printf ( " %s: %d bytes, pattern=%s\n" , s . Name , s . Size , s . Pattern )
}
}
2026-08-29 14:03:45 +02:00
if len ( scalars ) > 0 {
names := make ([] string , 0 , len ( scalars ))
for n := range scalars {
names = append ( names , n )
}
sort . Strings ( names )
fmt . Printf ( " scalars:\n" )
for _ , n := range names {
fmt . Printf ( " %s = %d\n" , n , scalars [ n ])
}
}
2026-08-05 20:46:39 +02:00
fmt . Printf ( " args before: %s\n" , hexDump ( args ))
rc := 0
2026-08-29 15:25:15 +02:00
for i := range repeat {
2026-08-05 20:46:39 +02:00
out , err := k . CallFunc ( funcName , args )
if err != nil {
fmt . Printf ( " call %d: FAIL — %v\n" , i + 1 , err )
rc = 1
continue
}
if repeat == 1 {
fmt . Printf ( " args after: %s\n" , hexDump ( out ))
} else if i == repeat - 1 {
fmt . Printf ( " args after %d calls: %s\n" , repeat , hexDump ( out ))
}
fmt . Printf ( " call %d: OK\n" , i + 1 )
}
return rc
}
// formatParams renders a parameter list as "a []byte, b []byte".
func formatParams ( ps [] verify . Param ) string {
var parts [] string
for _ , p := range ps {
if p . Name != "" {
parts = append ( parts , p . Name + " " + p . Typ )
} else {
parts = append ( parts , p . Typ )
}
}
return strings . Join ( parts , ", " )
}
// formatResults renders a result list as "(n int, code int)" or "int".
func formatResults ( rs [] verify . Param ) string {
if len ( rs ) == 0 {
return ""
}
if len ( rs ) == 1 && rs [ 0 ]. Name == "" {
return rs [ 0 ]. Typ
}
return "(" + formatParams ( rs ) + ")"
}
// hexDump returns a one-line hex dump of buf, truncated to 64 bytes.
func hexDump ( buf [] byte ) string {
const max = 64
2026-08-29 15:40:31 +02:00
n := min ( len ( buf ), max )
2026-08-05 20:46:39 +02:00
var sb strings . Builder
2026-08-29 15:25:15 +02:00
for i := range n {
2026-08-05 20:46:39 +02:00
if i > 0 {
sb . WriteByte ( ' ' )
}
fmt . Fprintf ( & sb , "%02x" , buf [ i ])
}
return fmt . Sprintf ( "%s%s (%d bytes)" , sb . String (), truncMark ( len ( buf ), max ), len ( buf ))
}
// truncMark returns "…" when the buffer is longer than max, else "".
func truncMark ( n , max int ) string {
if n > max {
return "…"
}
return ""
}