From 93c47a312ae026b4adb687fdbd8424a1503c2372 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Petr=20Balv=C3=ADn?= Date: Sat, 19 Sep 2026 21:18:43 +0200 Subject: [PATCH] feat(docs): man pages for gasm and every command, guarded against CLI drift Assisted-by: GLM 5.3 Flash --- CHANGELOG.md | 7 ++ README.md | 5 +- cmd/gasm/man_test.go | 157 +++++++++++++++++++++++++++++ docs/CLI.md | 4 + docs/man/gasm-asm.1 | 62 ++++++++++++ docs/man/gasm-audit-instructions.1 | 47 +++++++++ docs/man/gasm-debug.1 | 113 +++++++++++++++++++++ docs/man/gasm-diff.1 | 35 +++++++ docs/man/gasm-dis.1 | 36 +++++++ docs/man/gasm-fmt.1 | 52 ++++++++++ docs/man/gasm-lint.1 | 90 +++++++++++++++++ docs/man/gasm-lsp.1 | 24 +++++ docs/man/gasm-parse.1 | 20 ++++ docs/man/gasm-profile.1 | 16 +++ docs/man/gasm-scaffold.1 | 22 ++++ docs/man/gasm-tokens.1 | 19 ++++ docs/man/gasm-verify.1 | 117 +++++++++++++++++++++ docs/man/gasm.1 | 96 ++++++++++++++++++ justfile | 35 +++++++ 19 files changed, 956 insertions(+), 1 deletion(-) create mode 100644 cmd/gasm/man_test.go create mode 100644 docs/man/gasm-asm.1 create mode 100644 docs/man/gasm-audit-instructions.1 create mode 100644 docs/man/gasm-debug.1 create mode 100644 docs/man/gasm-diff.1 create mode 100644 docs/man/gasm-dis.1 create mode 100644 docs/man/gasm-fmt.1 create mode 100644 docs/man/gasm-lint.1 create mode 100644 docs/man/gasm-lsp.1 create mode 100644 docs/man/gasm-parse.1 create mode 100644 docs/man/gasm-profile.1 create mode 100644 docs/man/gasm-scaffold.1 create mode 100644 docs/man/gasm-tokens.1 create mode 100644 docs/man/gasm-verify.1 create mode 100644 docs/man/gasm.1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8e7c247..6626f2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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 diff --git a/README.md b/README.md index a35eff8..e98161d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/cmd/gasm/man_test.go b/cmd/gasm/man_test.go new file mode 100644 index 0000000..0d397ec --- /dev/null +++ b/cmd/gasm/man_test.go @@ -0,0 +1,157 @@ +// Copyright (c) 2026 Petr BalvĂ­n (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-.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 -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), " ") +} diff --git a/docs/CLI.md b/docs/CLI.md index 9d7b2f5..a425d96 100644 --- a/docs/CLI.md +++ b/docs/CLI.md @@ -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 diff --git a/docs/man/gasm-asm.1 b/docs/man/gasm-asm.1 new file mode 100644 index 0000000..f94d4ea --- /dev/null +++ b/docs/man/gasm-asm.1 @@ -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] +.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) diff --git a/docs/man/gasm-audit-instructions.1 b/docs/man/gasm-audit-instructions.1 new file mode 100644 index 0000000..4fba7a6 --- /dev/null +++ b/docs/man/gasm-audit-instructions.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) diff --git a/docs/man/gasm-debug.1 b/docs/man/gasm-debug.1 new file mode 100644 index 0000000..b143763 --- /dev/null +++ b/docs/man/gasm-debug.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 \-\-func +.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) diff --git a/docs/man/gasm-diff.1 b/docs/man/gasm-diff.1 new file mode 100644 index 0000000..d5cfbf1 --- /dev/null +++ b/docs/man/gasm-diff.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] +.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) diff --git a/docs/man/gasm-dis.1 b/docs/man/gasm-dis.1 new file mode 100644 index 0000000..e1ad652 --- /dev/null +++ b/docs/man/gasm-dis.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] +.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) diff --git a/docs/man/gasm-fmt.1 b/docs/man/gasm-fmt.1 new file mode 100644 index 0000000..f8d5093 --- /dev/null +++ b/docs/man/gasm-fmt.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) diff --git a/docs/man/gasm-lint.1 b/docs/man/gasm-lint.1 new file mode 100644 index 0000000..421a6a1 --- /dev/null +++ b/docs/man/gasm-lint.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 +.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) diff --git a/docs/man/gasm-lsp.1 b/docs/man/gasm-lsp.1 new file mode 100644 index 0000000..7a0a24a --- /dev/null +++ b/docs/man/gasm-lsp.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) diff --git a/docs/man/gasm-parse.1 b/docs/man/gasm-parse.1 new file mode 100644 index 0000000..98bdde5 --- /dev/null +++ b/docs/man/gasm-parse.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 +.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) diff --git a/docs/man/gasm-profile.1 b/docs/man/gasm-profile.1 new file mode 100644 index 0000000..bc06828 --- /dev/null +++ b/docs/man/gasm-profile.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 +.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) diff --git a/docs/man/gasm-scaffold.1 b/docs/man/gasm-scaffold.1 new file mode 100644 index 0000000..235cfac --- /dev/null +++ b/docs/man/gasm-scaffold.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 +.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 (\fIPortable\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) diff --git a/docs/man/gasm-tokens.1 b/docs/man/gasm-tokens.1 new file mode 100644 index 0000000..adffb18 --- /dev/null +++ b/docs/man/gasm-tokens.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 +.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) diff --git a/docs/man/gasm-verify.1 b/docs/man/gasm-verify.1 new file mode 100644 index 0000000..0dbaeee --- /dev/null +++ b/docs/man/gasm-verify.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] +.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) diff --git a/docs/man/gasm.1 b/docs/man/gasm.1 new file mode 100644 index 0000000..1817338 --- /dev/null +++ b/docs/man/gasm.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 . diff --git a/justfile b/justfile index e5736d6..edda209 100644 --- a/justfile +++ b/justfile @@ -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}}