feat(docs): man pages for gasm and every command, guarded against CLI drift
Test / test (push) Successful in 2m4s
Test / test (push) Successful in 2m4s
Assisted-by: GLM 5.3 Flash
This commit is contained in:
@@ -39,6 +39,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- **Oracle parity as its own CI step.** The push pipeline already ran
|
||||
the live go-tool-asm comparison inside the suite; a dedicated step
|
||||
now names that gate when it fails.
|
||||
- **Man pages.** docs/man carries gasm(1) and one page per command,
|
||||
written in roff: synopsis, description, every flag with its default,
|
||||
exit status, worked examples and cross-references.
|
||||
`just install-man` compresses them into ~/.local/share/man (MANDIR
|
||||
overrides) and `just uninstall-man` removes them. A test builds the
|
||||
binary and compares every command's `-h` output with its page, so the
|
||||
pages cannot drift from the CLI.
|
||||
|
||||
### Changed
|
||||
|
||||
|
||||
@@ -241,8 +241,11 @@ recipe.
|
||||
|
||||
## Documentation
|
||||
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/CLI.md](docs/CLI.md): full command reference
|
||||
- man pages: `just install-man` installs gasm(1) and one page per command
|
||||
into ~/.local/share/man (MANDIR overrides); `just uninstall-man` removes
|
||||
them
|
||||
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
|
||||
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
|
||||
- [CHANGELOG.md](CHANGELOG.md): release history
|
||||
|
||||
|
||||
@@ -0,0 +1,157 @@
|
||||
// Copyright (c) 2026 Petr Balvín <opensource@petrbalvin.org> (https://petrbalvin.org)
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestManPagesTrackTheCLI builds the binary once, then compares every
|
||||
// command's live `-h` output with its docs/man/gasm-<command>.1 page: the
|
||||
// flag sets must agree both ways, and the page's SYNOPSIS line must carry
|
||||
// the command's usage line. A flag or a usage change that skips the man
|
||||
// page fails here, so the pages cannot drift from the binary.
|
||||
func TestManPagesTrackTheCLI(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("builds the gasm binary")
|
||||
}
|
||||
bin := filepath.Join(t.TempDir(), "gasm")
|
||||
if out, err := exec.Command("go", "build", "-o", bin, ".").CombinedOutput(); err != nil {
|
||||
t.Fatalf("build gasm: %v\n%s", err, out)
|
||||
}
|
||||
|
||||
for _, cmd := range []string{
|
||||
"tokens", "parse", "fmt", "lint", "asm", "dis", "verify",
|
||||
"debug", "diff", "profile", "audit-instructions", "scaffold", "lsp",
|
||||
} {
|
||||
t.Run(cmd, func(t *testing.T) {
|
||||
raw, err := os.ReadFile(filepath.Join("..", "..", "docs", "man", "gasm-"+cmd+".1"))
|
||||
if err != nil {
|
||||
t.Fatalf("read man page: %v", err)
|
||||
}
|
||||
page := string(raw)
|
||||
|
||||
out, _ := exec.Command(bin, cmd, "-h").CombinedOutput()
|
||||
help := string(out)
|
||||
|
||||
binFlags := helpFlags(help)
|
||||
pageFlags := roffFlags(page)
|
||||
for f := range binFlags {
|
||||
if !pageFlags[f] {
|
||||
t.Errorf("flag -%s is in the binary's help but missing from the man page", f)
|
||||
}
|
||||
}
|
||||
for f := range pageFlags {
|
||||
if !binFlags[f] {
|
||||
t.Errorf("flag -%s is in the man page but the binary does not accept it", f)
|
||||
}
|
||||
}
|
||||
|
||||
want := helpUsage(help)
|
||||
got := roffSynopsis(page)
|
||||
if want != "" && got != want {
|
||||
t.Errorf("SYNOPSIS drift:\n page: %s\nbinary: %s", got, want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// helpFlags extracts the flag names from a `gasm <cmd> -h` output.
|
||||
func helpFlags(help string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
inFlags := false
|
||||
for line := range strings.SplitSeq(help, "\n") {
|
||||
if strings.TrimRight(line, " \t") == "Flags:" {
|
||||
inFlags = true
|
||||
continue
|
||||
}
|
||||
if !inFlags {
|
||||
continue
|
||||
}
|
||||
if !strings.HasPrefix(line, " -") {
|
||||
continue
|
||||
}
|
||||
token := strings.FieldsFunc(strings.TrimLeft(line, " "), func(r rune) bool {
|
||||
return r == ' ' || r == '\t'
|
||||
})
|
||||
if len(token) == 0 {
|
||||
continue
|
||||
}
|
||||
m[strings.TrimLeft(token[0], "-")] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var roffEscape = regexp.MustCompile(`\\f[BIRP]`)
|
||||
|
||||
// roffFlags extracts the flag names from a man page's OPTIONS section.
|
||||
func roffFlags(page string) map[string]bool {
|
||||
m := map[string]bool{}
|
||||
inOptions := false
|
||||
for line := range strings.SplitSeq(page, "\n") {
|
||||
if strings.HasPrefix(line, ".SH ") {
|
||||
inOptions = strings.HasPrefix(line, ".SH OPTIONS")
|
||||
continue
|
||||
}
|
||||
if !inOptions {
|
||||
continue
|
||||
}
|
||||
// Flag entries are written as either `.B \-flag` or `\fB\-flag`.
|
||||
var body string
|
||||
switch {
|
||||
case strings.HasPrefix(line, `.B \-`):
|
||||
body = line[3:]
|
||||
case strings.HasPrefix(line, `\fB\-`):
|
||||
body = line[1:]
|
||||
default:
|
||||
continue
|
||||
}
|
||||
name := roffEscape.ReplaceAllString(body, "")
|
||||
name = strings.ReplaceAll(name, `\-`, "-")
|
||||
name = strings.TrimSpace(name)
|
||||
if i := strings.IndexAny(name, " \t"); i >= 0 {
|
||||
name = name[:i]
|
||||
}
|
||||
m[strings.TrimLeft(name, "-")] = true
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// helpUsage returns the command's usage line without the "Usage: " prefix.
|
||||
func helpUsage(help string) string {
|
||||
for line := range strings.SplitSeq(help, "\n") {
|
||||
if strings.HasPrefix(line, "Usage: ") {
|
||||
return normaliseUsage(line[len("Usage: "):])
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// roffSynopsis returns the page's SYNOPSIS usage line, unescaped.
|
||||
func roffSynopsis(page string) string {
|
||||
inSyn := false
|
||||
for line := range strings.SplitSeq(page, "\n") {
|
||||
if strings.HasPrefix(line, ".SH ") {
|
||||
inSyn = strings.HasPrefix(line, ".SH SYNOPSIS")
|
||||
continue
|
||||
}
|
||||
if !inSyn || !strings.HasPrefix(line, ".B ") {
|
||||
continue
|
||||
}
|
||||
return normaliseUsage(strings.ReplaceAll(line[3:], `\-`, "-"))
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// normaliseUsage flattens whitespace and drops the roff font escapes so that
|
||||
// the binary's usage line and the page's SYNOPSIS line compare equal.
|
||||
func normaliseUsage(s string) string {
|
||||
s = roffEscape.ReplaceAllString(s, "")
|
||||
return strings.Join(strings.Fields(s), " ")
|
||||
}
|
||||
@@ -3,6 +3,10 @@
|
||||
The reference below is taken from the program's own `--help`. If the two disagree, the
|
||||
program is right and this file is a defect.
|
||||
|
||||
The same reference is installed as man pages: `just install-man` puts gasm(1) and one
|
||||
page per command into ~/.local/share/man (`MANDIR` overrides), and a test compares each
|
||||
page against the binary so the two cannot drift apart.
|
||||
|
||||
## Synopsis
|
||||
|
||||
```sh
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
.TH GASM-ASM 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-asm \- assemble Plan 9 assembly without the Go toolchain
|
||||
.SH SYNOPSIS
|
||||
.B gasm asm [\-\-format raw|elf|goobj] [\-p pkg] [\-GOARCH arch] [\-o out] <file>
|
||||
.SH DESCRIPTION
|
||||
Assemble FILE without the Go toolchain: every TEXT function is encoded
|
||||
to machine code and printed as a hex dump. Supported architectures:
|
||||
amd64 (including VEX/AVX2 and EVEX/AVX-512), arm64 (AArch64 integer,
|
||||
FP, conditional select, CRC32 and MOV pseudo), riscv64 (RV64IMAFDC and
|
||||
RVC) and loong64 (LoongArch base ISA).
|
||||
.PP
|
||||
With
|
||||
.B \-o
|
||||
the output is written to a file instead. The
|
||||
.B \-\-format
|
||||
flag selects what is written:
|
||||
.B raw
|
||||
(the default) concatenates the functions and the data section into one
|
||||
self-consistent image;
|
||||
.B elf
|
||||
emits a relocatable object (.text/.data sections, a symbol table and
|
||||
one PC32 relocation per static-symbol reference) that links with the
|
||||
system toolchain;
|
||||
.B goobj
|
||||
emits the Go toolchain's own object format, which cmd/link consumes
|
||||
directly (it requires
|
||||
.BR \-p ,
|
||||
the package path, and the installed Go toolchain).
|
||||
.PP
|
||||
Framed functions receive the stack-split guard and the trailing
|
||||
morestack block, byte-identical to the toolchain's output, so split
|
||||
functions link too.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-format \fIraw|elf|goobj\fR
|
||||
Output format; the default is raw.
|
||||
.TP
|
||||
.B \-p \fIpkg\fR
|
||||
Package path for --format goobj, qualifying the exported symbols.
|
||||
.TP
|
||||
.B \-GOARCH \fIarch\fR
|
||||
Target architecture: amd64, arm64, riscv64 or loong64; overrides the
|
||||
file-name suffix, which is how the suffix-less majority of GOROOT's
|
||||
files (cpu_x86.s, stub.s, ...) become assemblable.
|
||||
.TP
|
||||
.B \-o \fIfile\fR
|
||||
Write the output to this file instead of a hex dump on stdout.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success, 1 when parsing or assembly fails, and 2 on a usage
|
||||
error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm asm \-o hello.bin hello_amd64.s raw image
|
||||
gasm asm \-\-format elf \-o k.o k.s linkable ELF object
|
||||
gasm asm \-\-format goobj \-p pkg/path \-o k.o k.s Go object for go build
|
||||
gasm asm \-GOARCH amd64 cpu_x86.s arch override
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-dis (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,47 @@
|
||||
.TH GASM-AUDIT-INSTRUCTIONS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-audit-instructions \- diff the encoder against the Go toolchain, or measure a corpus
|
||||
.SH SYNOPSIS
|
||||
.B gasm audit\-instructions [\-\-corpus [\fIdir\fR]] [amd64|arm64|riscv64|loong64]
|
||||
.SH DESCRIPTION
|
||||
Compare the gasm encoder for the given architecture (default amd64)
|
||||
against
|
||||
.B go tool asm
|
||||
and print the diff: superset encodings (gasm-only, shippable via
|
||||
.BR "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 bare
|
||||
mnemonics, so the audit tracks whatever toolchain
|
||||
.B go env GOROOT
|
||||
provides.
|
||||
.PP
|
||||
With
|
||||
.BR \-\-corpus ,
|
||||
the audit changes shape: it assembles every
|
||||
.I .s
|
||||
file under the given directory (default GOROOT/src) with the gasm
|
||||
encoder only, no toolchain probing. A file whose name carries a
|
||||
recognisable _arch suffix is attempted for that architecture; a file
|
||||
without one is attempted for all four, exactly as a GOARCH build would
|
||||
compile it. The report gives the headline number (files that assemble
|
||||
for every target architecture), the per-architecture pass rates and the
|
||||
most common failure reasons, which drive the encodability backlog by
|
||||
frequency rather than by table order. A run over GOROOT takes under a
|
||||
second.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-\-corpus [\fIdir\fR]
|
||||
Assemble a corpus of .s files and report pass rates and failure
|
||||
reasons.
|
||||
.SH EXIT STATUS
|
||||
The mnemonic-diff mode reports through its output and exits 0; a failed
|
||||
probe or an unknown architecture exits non-zero.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm audit\-instructions amd64
|
||||
gasm audit\-instructions \-\-corpus
|
||||
gasm audit\-instructions \-\-corpus "$(go env GOROOT)/src/crypto"
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,113 @@
|
||||
.TH GASM-DEBUG 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-debug \- interactive source-level debugger for JIT-assembled functions
|
||||
.SH SYNOPSIS
|
||||
.B gasm debug <file.s> \-\-func <name>
|
||||
.SH DESCRIPTION
|
||||
Interactive debugger for JIT-assembled functions. Launches the function
|
||||
in a traced subprocess (ptrace), then provides a REPL for
|
||||
single-stepping, breakpoints, register and memory inspection.
|
||||
.PP
|
||||
With
|
||||
.B \-\-script
|
||||
the REPL commands run from a file and the session ends: the headless
|
||||
mode CI and scripts use.
|
||||
.B \-\-cover
|
||||
runs to completion with a breakpoint on every instruction and reports
|
||||
which executed and how often, the label-level coverage view.
|
||||
.SH REPL COMMANDS
|
||||
.TP
|
||||
.B break \fIlabel|addr\fR [\fBif \fIreg op val\fR]
|
||||
Set a breakpoint, optionally conditional on a register comparison
|
||||
(register against register or immediate).
|
||||
.TP
|
||||
.B delete \fIlabel|addr\fR
|
||||
Remove a breakpoint.
|
||||
.TP
|
||||
.B info break
|
||||
List all breakpoints.
|
||||
.TP
|
||||
.BR step " [" n ], " s
|
||||
Single-step n instructions; the default is 1.
|
||||
.TP
|
||||
.BR next ", " n
|
||||
Step over a CALL.
|
||||
.TP
|
||||
.BR finish ", " fin
|
||||
Run until the function returns.
|
||||
.TP
|
||||
.BR continue ", " c
|
||||
Run until a breakpoint, watchpoint or exit.
|
||||
.TP
|
||||
.BR disas " [" n ], " u
|
||||
Disassemble n instructions at PC.
|
||||
.TP
|
||||
.B regs
|
||||
Print general-purpose and vector registers.
|
||||
.TP
|
||||
.B where
|
||||
Show the source line and nearest label at PC.
|
||||
.TP
|
||||
.B stack
|
||||
Show the stack near RSP (return address and ABI0 args).
|
||||
.TP
|
||||
.BR bt ", " backtrace
|
||||
Backtrace: current frame plus return address.
|
||||
.TP
|
||||
.B x [\fIaddr\fR] [\fIlen\fR]
|
||||
Hex-dump memory; the defaults are the current PC and 64 bytes.
|
||||
.TP
|
||||
.B w \fIaddr val...\fR
|
||||
Write bytes to memory.
|
||||
.TP
|
||||
.B set \fIreg value\fR
|
||||
Set a register.
|
||||
.TP
|
||||
.B watch \fIaddr\fR [\fBr|w\fR] [\fIsize\fR]
|
||||
Set a hardware watchpoint; writes are watched by default.
|
||||
.TP
|
||||
.B unwatch [\fIslot\fR]
|
||||
Clear one watchpoint, or all without an argument.
|
||||
.TP
|
||||
.BR labels ", " l
|
||||
List function labels and offsets.
|
||||
.TP
|
||||
.BR help ", " h ", " ?
|
||||
Show command help.
|
||||
.TP
|
||||
.BR quit ", " q
|
||||
Kill the debuggee and exit.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-args \fIfile\fR
|
||||
File containing the ABI0 argument block.
|
||||
.TP
|
||||
.B \-buf \fIspec\fR
|
||||
Buffer specification: name:size:pattern[,name:size:pattern...] where
|
||||
pattern is zero, ones, seq, or hex.
|
||||
.TP
|
||||
.B \-cover
|
||||
Run to completion with a breakpoint on every instruction and report
|
||||
which executed and how often.
|
||||
.TP
|
||||
.B \-func \fIname\fR
|
||||
Function to debug.
|
||||
.TP
|
||||
.B \-script \fIfile\fR
|
||||
Run REPL commands from a file (one per line) and exit; - reads stdin.
|
||||
.TP
|
||||
.B \-timeout \fIduration\fR
|
||||
Kill the debuggee after this duration (e.g. 30s); for headless --script
|
||||
runs.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when the scripted session completes and 1 when the debuggee
|
||||
crashes or a check fails; the debugger is Linux-only.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm debug \-\-func name k.s
|
||||
gasm debug \-\-func name \-\-script cmds.txt \-\-timeout 30s k.s
|
||||
gasm debug \-\-func name \-\-cover k.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,35 @@
|
||||
.TH GASM-DIFF 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-diff \- compare the machine code of two assembly files
|
||||
.SH SYNOPSIS
|
||||
.B gasm diff [\-GOARCH arch] <file1.s> <file2.s>
|
||||
.SH DESCRIPTION
|
||||
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.
|
||||
.PP
|
||||
Functions are paired by exact name unless
|
||||
.B \-\-map
|
||||
says otherwise, so
|
||||
.B \-\-map wideCopyAVX2=wideCopyAVX512
|
||||
pairs two variants regardless of suffix.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-GOARCH \fIarch\fR
|
||||
Target architecture for both files: amd64, arm64, riscv64 or loong64;
|
||||
overrides the file-name suffixes.
|
||||
.TP
|
||||
.B \-\-map \fIspec\fR
|
||||
Comma-separated old=new pairs to match functions with different names.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when every paired function is identical and 1 when anything
|
||||
differs; a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm diff hello_amd64.s hello_amd64.s
|
||||
gasm diff \-\-map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,36 @@
|
||||
.TH GASM-DIS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-dis \- disassemble machine code to instruction text
|
||||
.SH SYNOPSIS
|
||||
.B gasm dis [\-a arch] <file>
|
||||
.SH DESCRIPTION
|
||||
Disassemble machine code to instruction text, decoded through
|
||||
golang.org/x/arch.
|
||||
.PP
|
||||
With a
|
||||
.I .s
|
||||
file, the file is assembled first and the listing follows the real
|
||||
layout: one block per TEXT function, local labels printed at their
|
||||
offsets. The architecture comes from the file-name suffix, or from
|
||||
.BR \-a .
|
||||
.PP
|
||||
With any other file, or
|
||||
.B \-
|
||||
for standard input, the bytes are disassembled linearly and
|
||||
.B \-a
|
||||
selects the architecture (amd64, arm64, riscv64 or loong64).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-a \fIarch\fR
|
||||
Architecture for raw input: amd64, arm64, riscv64 or loong64.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success, 1 when assembly or decoding fails, and 2 on a usage
|
||||
error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm dis k.s assemble, then list each function
|
||||
gasm dis \-a amd64 \- < dump.bin disassemble raw bytes from stdin
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,52 @@
|
||||
.TH GASM-FMT 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-fmt \- canonicalise the formatting of Plan 9 assembly sources
|
||||
.SH SYNOPSIS
|
||||
.B gasm fmt [\-w|\-l|\-d] [path...]
|
||||
.SH DESCRIPTION
|
||||
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.
|
||||
.PP
|
||||
With no paths, or a directory path, every
|
||||
.I .s
|
||||
file below it is reformatted in place and the changed files are listed,
|
||||
the way
|
||||
.B go fmt
|
||||
does;
|
||||
.B .
|
||||
and
|
||||
.B _
|
||||
directories are skipped. Explicit file paths print to stdout unless
|
||||
.B \-w
|
||||
is given.
|
||||
.PP
|
||||
.B \-l
|
||||
and
|
||||
.B \-d
|
||||
rewrite nothing:
|
||||
.B \-l
|
||||
prints the paths whose formatting differs from gasm's (empty output
|
||||
means everything is formatted, which is what a CI check wants),
|
||||
.B \-d
|
||||
prints the diffs. They are mutually exclusive.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-d
|
||||
Print diffs instead of rewriting files.
|
||||
.TP
|
||||
.B \-l
|
||||
List files whose formatting differs from gasm's.
|
||||
.TP
|
||||
.B \-w
|
||||
Write the result to the source file.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm fmt reformat every .s below here
|
||||
gasm fmt \-w kernel_amd64.s canonicalise one file in place
|
||||
gasm fmt \-l *.s list files whose formatting differs
|
||||
gasm fmt \-d kernel_amd64.s print a unified diff instead
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1)
|
||||
@@ -0,0 +1,90 @@
|
||||
.TH GASM-LINT 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-lint \- run the static checks over assembly files
|
||||
.SH SYNOPSIS
|
||||
.B gasm lint <file...>
|
||||
.SH DESCRIPTION
|
||||
Run the static checks over the given files and print diagnostics as
|
||||
\fIfile:line:col: severity: message [code]\fR. The exit status is
|
||||
non-zero when an error-severity diagnostic is found; warnings (e.g. the
|
||||
register-clobber audit) do not affect it.
|
||||
.PP
|
||||
The checks are conservative: they report what can be proven wrong and
|
||||
stay quiet otherwise, so a clean lint run is meaningful without
|
||||
suppression lists.
|
||||
.SH RULES
|
||||
.TP
|
||||
.B unknown-instruction
|
||||
The mnemonic is not in the architecture's instruction table.
|
||||
.TP
|
||||
.B operand-count
|
||||
The operand count disagrees with the instruction's declared arity.
|
||||
.TP
|
||||
.B undefined-label
|
||||
A jump target names no label in the function.
|
||||
.TP
|
||||
.B duplicate-label
|
||||
Two labels in one function share a name.
|
||||
.TP
|
||||
.B missing-ret
|
||||
The function can fall off its end without a terminator.
|
||||
.TP
|
||||
.B missing-textflag-include
|
||||
TEXT flags are used without including textflag.h.
|
||||
.TP
|
||||
.B abi-argsize
|
||||
The declared frame or argument size disagrees with the
|
||||
.B //\ function
|
||||
signature.
|
||||
.TP
|
||||
.B unreachable-code
|
||||
Code after RET and before the next label is dead; suppressed for
|
||||
functions with PC-relative or register-indirect control flow.
|
||||
.TP
|
||||
.B register-clobber
|
||||
A register the Go ABI fixes across calls is written without save and
|
||||
restore, computed by liveness over the control-flow graph.
|
||||
.TP
|
||||
.B funcdata-pcdata
|
||||
FUNCDATA and PCDATA indices are malformed.
|
||||
.TP
|
||||
.B unused-label
|
||||
A label no jump reaches.
|
||||
.TP
|
||||
.B invalid-textflag
|
||||
A TEXT flag combination the toolchain rejects.
|
||||
.TP
|
||||
.B stack-imbalance
|
||||
The function does not restore the stack pointer on every path.
|
||||
.TP
|
||||
.B register-width-mismatch
|
||||
An operand register has the wrong width for the instruction.
|
||||
.TP
|
||||
.B abi0-register-args
|
||||
A call passes arguments in registers where ABI0 expects the stack
|
||||
frame.
|
||||
.TP
|
||||
.B nonportable-register-name
|
||||
A register spelling that does not exist on the target architecture.
|
||||
.TP
|
||||
.B unencodable-instruction
|
||||
The mnemonic is known to the table but the encoder cannot assemble it
|
||||
yet (amd64).
|
||||
.TP
|
||||
.B reserved-register-write
|
||||
A write to the register the runtime reserves (arm64 R18).
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-disable \fIcodes\fR
|
||||
Comma-separated rule codes to disable.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when no error-severity diagnostic is found, 1 otherwise, and 2
|
||||
on a usage error.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm lint kernel_amd64.s
|
||||
gasm lint \-disable register-clobber,unused-label *.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1)
|
||||
@@ -0,0 +1,24 @@
|
||||
.TH GASM-LSP 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-lsp \- run the Plan 9 assembly language server
|
||||
.SH SYNOPSIS
|
||||
.B gasm lsp
|
||||
.SH DESCRIPTION
|
||||
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
|
||||
.I .s
|
||||
files; the target architecture is inferred from the file suffix
|
||||
(_amd64.s, _arm64.s, _riscv64.s, _loong64.s).
|
||||
.PP
|
||||
Provides completion, hover, document symbols, push and pull
|
||||
diagnostics, semantic-token highlighting, go-to-definition, find
|
||||
references, rename, formatting, inlay hints, code actions, signature
|
||||
help, document highlights, workspace symbol search, include document
|
||||
links and folding ranges; definition, references and rename work across
|
||||
every open document. Syntax highlighting is delivered as LSP semantic
|
||||
tokens, so no editor-specific grammar is required.
|
||||
.SH EXIT STATUS
|
||||
Runs until the client closes the session; exits 0 on a clean shutdown.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1)
|
||||
@@ -0,0 +1,20 @@
|
||||
.TH GASM-PARSE 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-parse \- parse an assembly file and report syntax errors
|
||||
.SH SYNOPSIS
|
||||
.B gasm parse <file>
|
||||
.SH DESCRIPTION
|
||||
Parse FILE and report syntax errors on stderr. The parser is
|
||||
error-tolerant and line-oriented: a malformed line becomes a diagnostic
|
||||
and parsing continues, so one run reports every syntax error in the
|
||||
file rather than the first.
|
||||
.PP
|
||||
On success, print how many declarations and TEXT functions the file
|
||||
contains. FILE may be
|
||||
.B \-
|
||||
to read standard input.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when the file parses without errors and 1 otherwise.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-tokens (1)
|
||||
@@ -0,0 +1,16 @@
|
||||
.TH GASM-PROFILE 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-profile \- show the basic-block structure of functions
|
||||
.SH SYNOPSIS
|
||||
.B gasm profile <file.s>
|
||||
.SH DESCRIPTION
|
||||
Show the basic-block structure of functions in an assembly file: each
|
||||
function's labels, their offsets, and the block boundaries. This is
|
||||
the static structure; for runtime execution counts, use
|
||||
.BR "gasm verify \-fuzz" ,
|
||||
which exercises the code paths.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success and 1 when the file cannot be assembled.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,22 @@
|
||||
.TH GASM-SCAFFOLD 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-scaffold \- generate a differential test skeleton for a kernel file
|
||||
.SH SYNOPSIS
|
||||
.B gasm scaffold differential <file.s>
|
||||
.SH DESCRIPTION
|
||||
Print a differential test skeleton for every
|
||||
.B //\ func
|
||||
signature in FILE. The test seeds random states, drives the kernel and
|
||||
a portable reference (\fI<name>Portable\fR), and compares outputs
|
||||
byte-for-byte. Write the reference bodies, place the file in the
|
||||
kernel's package, and run it in CI.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when the skeleton is written to stdout and 1 when the file
|
||||
cannot be parsed; a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm scaffold differential kernel_amd64.s > kernel_differential_test.go
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-verify (1)
|
||||
@@ -0,0 +1,19 @@
|
||||
.TH GASM-TOKENS 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-tokens \- print the lexical token stream of an assembly file
|
||||
.SH SYNOPSIS
|
||||
.B gasm tokens <file>
|
||||
.SH DESCRIPTION
|
||||
Print the lexical token stream of FILE: position, token kind and text,
|
||||
one token per line. FILE may be
|
||||
.B \-
|
||||
to read standard input.
|
||||
.PP
|
||||
This is the front end's raw view, for when the assembler's own
|
||||
diagnostic is not enough: a mis-scanned operand or a swallowed comment
|
||||
shows up here as the tokens the parser actually received.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success and 1 when the file cannot be read.
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-parse (1)
|
||||
@@ -0,0 +1,117 @@
|
||||
.TH GASM-VERIFY 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm-verify \- JIT-assemble a file and run dynamic checks against it
|
||||
.SH SYNOPSIS
|
||||
.B gasm verify [\-smoke] [\-abi] [\-fuzz] [\-ground\-truth] [\-profile] [\-call] <file.s>
|
||||
.SH DESCRIPTION
|
||||
Assemble FILE, 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.
|
||||
.PP
|
||||
With
|
||||
.BR \-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.
|
||||
.PP
|
||||
With
|
||||
.BR \-abi ,
|
||||
each function is called with sentinel values in the registers the Go
|
||||
ABI fixes across calls (the frame pointer and the goroutine pointer)
|
||||
plus a canary below SP; violations are reported. JIT-based checks run
|
||||
when the host matches the file's architecture (all but loong64, which
|
||||
is ground-truth only for now).
|
||||
.PP
|
||||
With
|
||||
.BR \-fuzz ,
|
||||
each function with a
|
||||
.B //\ 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).
|
||||
.PP
|
||||
With
|
||||
.BR \-ground\-truth ,
|
||||
the assembled machine code is compared byte-for-byte against
|
||||
.B go tool asm
|
||||
(relocation sites masked), reporting any encoding drift.
|
||||
.PP
|
||||
With
|
||||
.BR \-profile ,
|
||||
the static basic-block structure is listed for each function.
|
||||
.PP
|
||||
With
|
||||
.BR \-call ,
|
||||
a single function is invoked with user-supplied buffers
|
||||
.RB ( \-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.
|
||||
.PP
|
||||
With
|
||||
.B \-save\-corpus
|
||||
(and
|
||||
.BR \-fuzz ),
|
||||
every input that crashes or mismatches is written to the directory as
|
||||
replayable JSON.
|
||||
.B \-replay
|
||||
re-runs saved entries against the kernel, one child process per entry,
|
||||
so an input that crashed the original run crashes only the child: the
|
||||
report says whether each entry reproduces.
|
||||
.SH OPTIONS
|
||||
.TP
|
||||
.B \-abi
|
||||
Run ABI-checking calls (sentinel registers and red zone).
|
||||
.TP
|
||||
.B \-abi\-n \fIn\fR
|
||||
Number of ABI check iterations with varied inputs; the default is 100.
|
||||
.TP
|
||||
.B \-args \fIspec\fR
|
||||
Scalar args for -call: name=value[,name=value] (decimal or 0x hex).
|
||||
.TP
|
||||
.B \-buf \fIspec\fR
|
||||
Buffer spec for -call: name:size:pattern[,name:size:pattern] where
|
||||
pattern is zero, ones, seq, or hex.
|
||||
.TP
|
||||
.B \-call \fIname\fR
|
||||
Call a single function with -buf instead of the sweeps.
|
||||
.TP
|
||||
.B \-fuzz
|
||||
Differential fuzz: JIT both the gasm and the go-tool-asm versions and
|
||||
compare outputs.
|
||||
.TP
|
||||
.B \-ground\-truth
|
||||
Compare machine code byte-for-byte against go tool asm.
|
||||
.TP
|
||||
.B \-n \fIn\fR
|
||||
Number of fuzz iterations per function; the default is 1000.
|
||||
.TP
|
||||
.B \-profile
|
||||
List basic-block structure per function.
|
||||
.TP
|
||||
.B \-repeat \fIn\fR
|
||||
Number of times to repeat a -call invocation; the default is 1.
|
||||
.TP
|
||||
.B \-replay \fIdir\fR
|
||||
Replay saved corpus entries (JSON files in this directory) against the
|
||||
kernel.
|
||||
.TP
|
||||
.B \-save\-corpus \fIdir\fR
|
||||
With -fuzz: write each failing input to this directory as replayable
|
||||
JSON.
|
||||
.TP
|
||||
.B \-smoke
|
||||
Call each NOSPLIT function with zeroed args.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 when every requested check passes and 1 when any check fails;
|
||||
a file that cannot be assembled exits 1 and a usage error exits 2.
|
||||
.SH EXAMPLES
|
||||
.nf
|
||||
gasm verify \-\-call add \-\-args a=2,b=3 hello_amd64.s
|
||||
gasm verify \-\-ground\-truth k.s
|
||||
gasm verify \-\-fuzz \-n 500 k.s
|
||||
.fi
|
||||
.SH SEE ALSO
|
||||
.BR gasm (1),
|
||||
.BR gasm\-asm (1),
|
||||
.BR gasm\-debug (1)
|
||||
@@ -0,0 +1,96 @@
|
||||
.TH GASM 1 "2026-09-19" "gasm 0.33.0" "User Commands"
|
||||
.SH NAME
|
||||
gasm \- developer tooling for Go's Plan 9 assembler
|
||||
.SH SYNOPSIS
|
||||
.B gasm
|
||||
.I command
|
||||
.RI [ arguments ]
|
||||
.br
|
||||
.B gasm
|
||||
.BR \-h | \-\-help
|
||||
.br
|
||||
.B gasm
|
||||
.BR \-V | \-\-version
|
||||
.SH DESCRIPTION
|
||||
.B gasm
|
||||
bundles a lexer, parser, formatter, linter, standalone assembler and
|
||||
language server for Plan 9 assembly into one self-contained binary. It
|
||||
serves two purposes: it brings developer tooling to the
|
||||
.I .s
|
||||
files of Go programs, and it assembles Plan 9 assembly without the Go
|
||||
toolchain at all, to raw images, linkable ELF objects with DWARF5 debug
|
||||
sections, or the Go toolchain's own GOOBJ format, which
|
||||
.B go build
|
||||
consumes directly.
|
||||
.PP
|
||||
Four architectures are covered: amd64 (including VEX/AVX2 and
|
||||
EVEX/AVX-512), arm64, riscv64 (RV64IMAFDC and RVC) and loong64. The
|
||||
target architecture is inferred from the file-name suffix
|
||||
(\fI_amd64.s\fR, \fI_arm64.s\fR, \fI_riscv64.s\fR, \fI_loong64.s\fR) or
|
||||
named explicitly with \fB\-GOARCH\fR where the commands accept it.
|
||||
.SH COMMANDS
|
||||
.TP
|
||||
.B gasm\-tokens(1)
|
||||
Print the lexical token stream.
|
||||
.TP
|
||||
.B gasm\-parse(1)
|
||||
Parse a file and report syntax errors.
|
||||
.TP
|
||||
.B gasm\-fmt(1)
|
||||
Canonicalise formatting: gofmt for assembly.
|
||||
.TP
|
||||
.B gasm\-lint(1)
|
||||
Run the static checks.
|
||||
.TP
|
||||
.B gasm\-asm(1)
|
||||
Assemble \fI.s\fR files to machine code, raw images, ELF objects or GOOBJ.
|
||||
.TP
|
||||
.B gasm\-dis(1)
|
||||
Disassemble machine code, raw bytes or an assembled \fI.s\fR file.
|
||||
.TP
|
||||
.B gasm\-verify(1)
|
||||
JIT-assemble and run dynamic checks: smoke calls, ABI checks,
|
||||
differential fuzzing, ground-truth comparison.
|
||||
.TP
|
||||
.B gasm\-debug(1)
|
||||
Interactive source-level debugger.
|
||||
.TP
|
||||
.B gasm\-diff(1)
|
||||
Compare the machine code of two files byte-for-byte.
|
||||
.TP
|
||||
.B gasm\-profile(1)
|
||||
Show the basic-block structure of functions.
|
||||
.TP
|
||||
.B gasm\-audit\-instructions(1)
|
||||
Diff the encoder against the Go toolchain's name table, or measure a
|
||||
corpus of \fI.s\fR files.
|
||||
.TP
|
||||
.B gasm\-scaffold(1)
|
||||
Generate a differential test skeleton for a kernel file.
|
||||
.TP
|
||||
.B gasm\-lsp(1)
|
||||
Run the language server over standard input/output.
|
||||
.TP
|
||||
.B gasm version
|
||||
Print the version, the same as \fB\-\-version\fR.
|
||||
.SH GLOBAL FLAGS
|
||||
.TP
|
||||
.BR \-h ", " \-\-help
|
||||
Show the command overview.
|
||||
.TP
|
||||
.BR \-V ", " \-\-version
|
||||
Print the version the toolchain recorded at build time.
|
||||
.SH EXIT STATUS
|
||||
Exits 0 on success, 1 when a command fails, and 2 on a usage error. An
|
||||
unknown command exits 2.
|
||||
.SH SEE ALSO
|
||||
.BR gasm\-asm (1),
|
||||
.BR gasm\-fmt (1),
|
||||
.BR gasm\-lint (1),
|
||||
.BR gasm\-verify (1),
|
||||
.BR gasm\-debug (1)
|
||||
.PP
|
||||
The full command reference, with worked examples and every flag, is in
|
||||
docs/CLI.md of the repository
|
||||
.UR https://sourcedock.dev/petrbalvin/gasm-devkit
|
||||
.UE .
|
||||
@@ -13,6 +13,10 @@ packages := "./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/..
|
||||
|
||||
bindir := env_var_or_default("BINDIR", env_var("HOME") / ".local" / "bin")
|
||||
|
||||
# Where `install-man` puts the gzip-compressed pages (man1 below it). Exported because
|
||||
# the Perl recipes read it from the environment.
|
||||
export MANDIR := env_var_or_default("MANDIR", env_var("HOME") / ".local" / "share" / "man")
|
||||
|
||||
default:
|
||||
@just --list
|
||||
|
||||
@@ -84,6 +88,37 @@ install: build
|
||||
uninstall:
|
||||
rm -f "{{bindir}}/{{binary}}"
|
||||
|
||||
# Install the man pages under docs/man into mandir/man1, gzip-compressed. Not a gate: a
|
||||
# convenience for the person at the keyboard; man finds them through ~/.local/share/man.
|
||||
install-man:
|
||||
#!/usr/bin/env perl
|
||||
my $out = $ENV{MANDIR} . q{/man1};
|
||||
system(q{mkdir}, q{-p}, $out) == 0 or die qq{mkdir $out: $!\n};
|
||||
for my $p (glob q{docs/man/*.1}) {
|
||||
open(my $g, q{-|}, q{gzip}, q{-c}, $p) or die qq{gzip $p: $!\n};
|
||||
my $content = do { local $/; <$g> };
|
||||
close($g);
|
||||
my $base = $p;
|
||||
$base =~ s{docs/man/}{};
|
||||
open(my $o, q{>}, qq{$out/$base.gz}) or die qq{write $out/$base.gz: $!\n};
|
||||
print {$o} $content;
|
||||
close($o);
|
||||
print qq{$out/$base.gz\n};
|
||||
}
|
||||
|
||||
# Remove the installed man pages.
|
||||
uninstall-man:
|
||||
#!/usr/bin/env perl
|
||||
for my $p (glob q{docs/man/*.1}) {
|
||||
my $base = $p;
|
||||
$base =~ s{docs/man/}{};
|
||||
my $f = $ENV{MANDIR} . q{/man1/} . $base . q{.gz};
|
||||
if (-f $f) {
|
||||
unlink($f) or die qq{unlink $f: $!\n};
|
||||
print qq{removed $f\n};
|
||||
}
|
||||
}
|
||||
|
||||
# Run the program. The flag is there because `go run` does not stamp the build otherwise.
|
||||
run:
|
||||
go run -buildvcs=true {{package}}
|
||||
|
||||
Reference in New Issue
Block a user