docs: sync README, CHANGELOG and docs with the current state

This commit is contained in:
2026-08-30 10:44:18 +02:00
parent 6c1c8d9d96
commit 56f8babbce
7 changed files with 407 additions and 263 deletions
+68 -2
View File
@@ -15,6 +15,14 @@ Unreleased changes on the `development` branch.
and loong64 in addition to amd64. Each architecture has its own ptrace
register access, disassembler (`golang.org/x/arch`), register display,
and stop-info handler. The REPL is fully arch-neutral.
- **Headless debugging.** `gasm debug --script` runs REPL commands from a
file (or stdin) and exits; `--timeout` kills the debuggee when a run
hangs, with the watchdog armed before the ptrace attach. `--cover` runs
to completion with a breakpoint on every label and reports which blocks
executed: label-level coverage for headless test runs.
- **Conditional breakpoints.** `break <label> if <reg> <op> <val>` now also
compares two registers (`break loop if RAX > RBX`), not only a register
against an immediate.
- **JIT execution trampolines.** `verify.Call` now works on all four
architectures via hand-written assembly trampolines
(`trampoline_{arm64,riscv64,loong64}.s`) that save the Go stack, switch
@@ -22,21 +30,67 @@ Unreleased changes on the `development` branch.
- **Hardware watchpoints on all architectures.** arm64 uses DBGWVR/DBGWCR
via `PTRACE_SETREGSET` with `NT_ARM_HW_BREAK`; riscv64 and loong64 use
`PTRACE_POKEUSER` to access trigger/debug registers.
- **`gasm verify --args`.** Scalar arguments (`name=value`, decimal or
`0x` hex) can now be supplied to a `--call` invocation alongside `--buf`
buffers, closing the gap where only buffers could be supplied.
- **`gasm audit-instructions`.** Black-box diff of the amd64 encoder
against the installed `go tool asm`: superset encodings (gasm-only,
shippable via `gasm asm --format goobj`), known-but-unencodable names
(the backlog) and go-only names (feature gaps).
- **`gasm scaffold differential`.** Prints a differential test skeleton
for every `// func` signature in a kernel file: random seed states, the
kernel call and a portable reference (`<name>Portable`), compared
byte-for-byte.
- **LSP: find references** (`textDocument/references`).
- **LSP: rename symbol** (`textDocument/rename`).
- **LSP: document formatting** (`textDocument/formatting`) using the
`format` package for canonical gofmt-style output.
- **LSP: inlay hints** (`textDocument/inlayHint`) — frame size hints after
- **LSP: inlay hints** (`textDocument/inlayHint`): frame size hints after
the TEXT directive's argument area.
- **LSP: workspace symbol search** (`workspace/symbol`): substring search
over the TEXT functions and GLOBL/DATA symbols of every open document.
- **LSP: code actions.** Quick fixes for `missing-ret` (insert the RET)
and `unused-label` (remove the label) diagnostics.
- **LSP: signature help** (`textDocument/signatureHelp`): the callee's
`// func` signature while the cursor is on a `CALL`.
- **LSP: document highlights**: every reference to the function or label
under the cursor is highlighted.
- **Lint: `unused-label` rule.** Flags labels that are defined but never
referenced by any jump (Hint severity).
- **Lint: `invalid-textflag` rule.** Flags TEXT/GLOBL flags not in the
known set from `textflag.h` (Warning severity).
- **Lint: `stack-imbalance` rule.** Tracks SP changes and flags if the
net delta at RET does not match the declared frame size.
- **Lint: `register-width-mismatch` rule.** Flags amd64 operands whose
register width does not match the width the mnemonic suffix prescribes
(for example a 32-bit register in a `MOVQ`).
- **Lint: `abi0-register-args` rule.** Flags kernels whose `// func`
parameters are never read from the FP frame (for example arguments read
from registers instead), which pass every test today and break on a
toolchain upgrade. Shipped with the Go assembler's operand-width model.
- **Lint: `nonportable-register-name` rule.** Flags the `RAX`/`EAX`-style
register aliases gasm accepts but `go tool asm` rejects, so files using
them only link through the gasm GOOBJ path.
- **Lint: `unencodable-instruction` rule.** Flags mnemonics the
architecture table knows but the encoder cannot yet emit, at edit time
instead of at assembly time.
- **DWARF5 debug sections in ELF output.** All four ELF emitters now emit
`.debug_abbrev`, `.debug_info`, `.debug_line`, and `.debug_line_str`
sections, enabling `addr2line` and GDB/LLDB source-level debugging.
sections, enabling `addr2line` and GDB/LLDB source-level debugging, and
the amd64 emitter adds a `.debug_frame` CFI section for stack unwinding.
- **amd64: legacy SSE and conversion coverage.** The encoder now handles
the legacy (non-VEX) SSE packed binaries and immediate shuffles, the
legacy SSE integer instructions and `BSWAP`, the scalar and packed
double conversions (`CVTSS2SD`, `CVTSD2SS`, `CVTPS2PD`, `CVTPD2PS`) and
the prefetch hints.
- **amd64: `VPCMP` and the full opmask set.** The EVEX compare with an
opmask destination and the remaining opmask-register instructions are
encoded, byte for byte against the Go assembler.
### Changed
- **Parallel verify sweeps.** The `-smoke` and `-abi` per-function checks
now run in parallel instead of sequentially.
### Fixed
@@ -48,6 +102,18 @@ Unreleased changes on the `development` branch.
`AssembleFileLOONG64`, and `AssembleFileARM64` now mark external
relocations and populate `img.Externals`, enabling cross-package symbol
resolution in GOOBJ output.
- **Subprocess-isolated smoke and ABI sweeps.** A function that faults
during the `-smoke` or `-abi` sweep is reported without killing the
parent; each sweep runs in a child process.
- **`BSF`, `BSR` and `POPCNT` encodings.** Corrected to the bytes
`go tool asm` emits.
- **`MOVQ` immediates.** Compressed to the toolchain's imm32 forms.
- **Frame adjustments of 128 to 255 bytes.** Now use the imm8 `ADDQ`
stack adjustment.
- **amd64 encoding parity.** A broad pass aligned the remaining encoder
outputs and operand strictness with `go tool asm`.
- **`gasm scaffold` parameter names.** Shared parameter names and
two-sided seed sets in the generated skeleton are now correct.
- **asm help text.** Updated to list arm64 as a supported architecture.
## [0.31.1] — 2026-08-20
+77 -71
View File
@@ -1,101 +1,107 @@
# Contributing to gasm-devkit
## Prerequisites
Thanks for contributing to gasm-devkit.
- Go 1.27 or later (`toolchain go1.27.0`)
- `just` command runner
- A Linux host on amd64, arm64, riscv64 or loong64
## Development setup
## Development Setup
Requirements: Go 1.27 or later, the [just](https://github.com/casey/just)
command runner, and a Linux host on amd64, arm64, riscv64 or loong64.
```sh
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
cd gasm-devkit
just install # download module dependencies
just build # go vet + gofmt check
just test # full test suite with race detector
just test # full suite, race detector, 80 % coverage gate
```
## Commands
## Workflow
Every just recipe:
1. Branch from `development`; never commit directly to `main` (`main` is
release-only: merge from `development`, then tag).
2. Commit with [Conventional Commits](https://www.conventionalcommits.org/):
`type(scope): description`: subject line only, imperative mood,
lowercase after the colon, no trailing dot. Allowed types: `feat`,
`fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`,
`build`, `revert`. The only line after the subject is the trailer:
`Assisted-by: <model-name>`. No `Co-Authored-By`, no `Signed-off-by`,
no other trailers.
3. Record every user-visible change in `CHANGELOG.md` under
`## [development]` (categories: Added, Changed, Fixed, Removed,
Security).
4. Add or update tests; coverage must stay **at or above 80 %** (hard
gate, enforced by CI).
5. Update the documentation when behaviour, flags or the public surface
change.
6. Open a pull request against `development`.
| Recipe | What it does |
|--------|-------------|
| `just` | List all recipes |
| `just install` | `go mod download` |
| `just build` | `go vet ./...` + `gofmt -l .` check — zero errors required |
| `just test` | `go test -race -count=1 -coverprofile=coverage.out ./...` + 80 % coverage gate |
| `just fmt` | `gofmt -w .` |
| `just run -- lint file.s` | Run the CLI with `go run` (args after `--`) |
| `just install-bin` | Install `gasm` into `$GOBIN` with the release version stamped |
| `just gen` | Regenerate `arch/*_gen.go` instruction tables from the Go toolchain |
| `just uninstall` | Remove build artefacts (`coverage.out`, `gasm`, `*.test`) |
Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`;
CI builds and publishes the binaries for all four architectures.
## Running a Single Test
## Code style
`gofmt` and `go vet` via `just fmt` / `just build`; both must pass with
zero output; `go fix -diff ./...` must report nothing on touched packages.
- Standard library only in production code; `golang.org/x/arch` is used
in tests only (round-trip decoding) and is never linked into the `gasm`
binary.
- No cgo, no C, no external toolchains at runtime.
- Explicit `if err != nil`; errors wrapped with
`fmt.Errorf("context: %w", err)`; no panics outside `main`.
- The parser, lexer and formatter are hand-written; the `arch` instruction
tables are generated only via `_gen/gen.go` (`just gen`), never edited.
## Running a single test
```sh
go test -run TestVexGroundTruth ./asm/
go test -run TestDifferentialLZ4Fuzz ./verify/
go test -run TestGroundTruthBasic ./verify/
go test -run TestGOObjectLinkAndRun ./asm/
go test -run TestFuzzWideCopy ./verify/
```
## Testing the Debugger
The interactive debugger (`gasm debug`) requires a compiled binary on
`$PATH`; `go run` does not work for the traced child process. Install
first with `just install-bin`.
The interactive debugger (`gasm debug`) requires a compiled binary —
`go run` does not work for the child process. Install first:
## CI (Gitea Actions)
```sh
just install-bin
gasm debug --func add testdata/verify/basic_amd64.s
```
Workflows live in `.gitea/workflows/` and run on self-hosted runners:
## Code Style
| Workflow | Trigger | What it does |
|----------|---------|--------------|
| Test | push / PR to `development` | gofmt check, `go vet`, `go test -race`, 80 % coverage gate |
| Release | tag `v*` | cross-compiles binaries for linux/{amd64,arm64,riscv64,loong64} and publishes the Gitea release |
See [AGENTS.md](AGENTS.md) for the full style guide. Key points:
The Definition of Done (`just build` + `just test` + `just fmt`) must
still pass locally before pushing.
- `gofmt` — zero diff.
- `go vet` — zero warnings.
- Standard library only in production code; `golang.org/x/arch` in tests.
- No cgo, no C, no JavaScript.
- Hand-written Plan 9 assembly; tables generated only via `_gen/gen.go`.
## AI Contribution Policy
## Branches and Releases
AI tools are welcome as productivity aids. What matters is that
contributions remain understandable, reviewable, and genuinely useful.
- `development` is the working branch.
- `main` is release-only: `git merge --ff-only development`, then `git tag vX.Y.Z`.
- Conventional Commits: `feat(asm): add EVEX gather and scatter`.
- Every commit ends with `Assisted-by: <model-name>`.
- **Disclose AI use.** If you used AI to draft or generate any part of a
commit, issue, pull request, or code review, say so clearly.
- **Commit messages:** end every commit with exactly one trailer:
`Assisted-by: <model-name>` (e.g. `Assisted-by: GLM 5.3`).
- **Pull requests and issues:** attribute AI assistance in one trailing
line, e.g. `_Assisted-by: GLM 5.3_`. Do not paste it into the PR
description as a section.
- **Take responsibility.** You remain accountable for the accuracy,
completeness, and intent of everything you submit.
- **Review before marking ready.** Read AI-generated diffs carefully, run
them locally, and add or update tests where appropriate.
- **Preferred models.** Prefer open-weight models with transparent
training data: **GLM**, **DeepSeek**, and **MiMo**.
## CI
CI runs on every push to `development` and on pull requests:
- **Test** (`test.yml`) — `gofmt` check, `go vet`, `go test -race` and the
80 % coverage gate.
- **Release** (`release.yml`) — cross-compiles release binaries for
linux/{amd64,arm64,riscv64,loong64} on version tags and publishes them.
The Definition of Done (`just build` + `just test` + `just fmt`) must still
pass locally before pushing.
## AI-Assisted Contributions
AI agents may assist with code, documentation, tests, and review. All
AI-assisted changes must:
- Include the trailer `Assisted-by: <model-name>` in the commit message
(e.g. `Assisted-by: DeepSeek V4 Pro`).
- Follow the [AGENTS.md](AGENTS.md) rules.
- Pass the Definition of Done before committing.
Attribute agent authorship in issues and pull requests on one trailing
line:
```
_Assisted-by: Qwen 3.8 Max_
```
## Questions
## Reporting bugs
Open an issue at
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues).
[sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrbalvin/gasm-devkit/issues)
with the version (`gasm --version`), OS and architecture, the exact
command, the full output, and the expected versus actual behaviour.
**Security issues:** email **opensource@petrbalvin.org** instead of opening
a public issue.
+119 -111
View File
@@ -1,149 +1,157 @@
# gasm-devkit
Developer tooling for **GAsm** — Go's built-in Plan 9 assembler.
Developer tooling for **GAsm**, Go's built-in Plan 9 assembler.
Go ships an assembler but no tooling for it. There is no syntax highlighting,
Go ships an assembler but no tooling for it: there is no syntax highlighting,
no autocomplete, no linter, no static analyser, no formatter, no standalone
assembler and no debugger for `.s` files. Developers write assembly blind,
validate it by benchmark, and debug it by print statement.
validate it by benchmark, and debug it by print statement. gasm-devkit is the
missing toolkit: a single, self-contained binary, `gasm`, that brings proper
developer tooling to Plan 9 assembly on amd64, arm64, riscv64 and loong64.
gasm-devkit is the missing toolkit. It is a single, self-contained binary —
`gasm` — that brings proper developer tooling to Plan 9 assembly:
## Features
```
gasm tokens dump the lexical token stream
gasm parse parse and report syntax errors
gasm fmt canonicalise formatting (gofmt for assembly)
gasm lint static checks
gasm lsp language server (completion, hover, symbols, diagnostics, highlighting)
gasm asm standalone assembler
gasm verify dynamic analysis & verification
gasm debug source-level debugger
gasm diff compare machine code of two .s files
gasm profile show basic-block structure of functions
```
- **Front end.** A hand-written lexer and an error-tolerant parser produce a
typed AST with source positions; `gasm tokens` and `gasm parse` expose them
directly.
- **Formatter.** `gasm fmt` canonicalises indentation, operand spacing,
per-function mnemonic alignment and blank-line layout: `gofmt` for assembly,
operating recursively on directories the way `go fmt` does.
- **Linter.** `gasm lint` runs 17 conservative static checks, among them
`undefined-label`, `abi-argsize` (declared frame vs the `// func` signature),
`register-clobber` (Go ABI register liveness over the control-flow graph),
`stack-imbalance`, `abi0-register-args` and `unencodable-instruction`.
- **Standalone assembler.** `gasm asm` encodes all four architectures without
the Go toolchain and writes raw images, linkable ELF objects (with DWARF5
debug sections) or the Go toolchain's own GOOBJ format, which `go build`
consumes in place of the toolchain's output.
- **Dynamic verification.** `gasm verify` JIT-loads assembled functions into
executable memory: smoke calls, ABI checks (sentinel registers, red-zone
canary), differential fuzzing against the `go tool asm` build, and
byte-for-byte ground-truth comparison of the machine code.
- **Debugger.** `gasm debug` is a source-level ptrace debugger with
breakpoints (optionally conditional), hardware watchpoints, register and
memory inspection, and headless script runs with label-level coverage.
- **Language server.** `gasm lsp` serves completion, hover, document symbols,
diagnostics, semantic-token highlighting, go-to-definition, find references,
rename, formatting, inlay hints, code actions, signature help, document
highlights and workspace symbol search over stdio.
- **Comparators and audits.** `gasm diff` compares the machine code of two
assembly files byte-for-byte, `gasm profile` shows basic-block structure,
`gasm audit-instructions` diffs the encoder against the installed toolchain,
and `gasm scaffold` generates a differential test skeleton for a kernel.
- **Complete instruction coverage.** The instruction tables are generated
from the Go toolchain's own assembler source, so the toolkit recognises
every mnemonic the real assembler accepts; `just gen` refreshes them.
## Architecture support
### Architecture support
gasm-devkit targets every architecture Go's assembler speaks. The instruction
tables are **generated from the Go toolchain's own assembler source**
(`cmd/internal/obj/<arch>`), so gasm-devkit recognises *every* mnemonic the
real assembler accepts — not a hand-maintained subset that drifts and rots.
| Architecture | GOARCH | File suffix | Instructions recognised |
|--------------|-------------|----------------|------------------------------------|
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
| Architecture | GOARCH | File suffix | Instructions recognised |
|--------------|-------------|--------------|---------------------------------------------|
| AMD64 | `amd64` | `_amd64.s` | 1600 + common opcodes + traditional aliases |
| ARM64 | `arm64` | `_arm64.s` | 538 + common opcodes |
| RISC-V | `riscv64` | `_riscv64.s` | 961 + common opcodes |
| LoongArch | `loong64` | `_loong64.s` | 799 + common opcodes |
"Common opcodes" are the instructions shared by every architecture (`RET`,
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, …). AMD64 additionally
`JMP`, `NOP`, `CALL`, `TEXT`, `FUNCDATA`, `PCDATA`, ...). AMD64 additionally
carries the traditional conditional-jump spellings (`JZ`, `JNZ`, `JA`, `JC`,
…) that the assembler accepts as aliases. Regenerating the tables is one
command — `just gen` — and requires only a Go installation; the committed
output has no runtime dependency on the toolchain.
...) that the assembler accepts as aliases. Regenerating the tables is one
command (`just gen`) and requires only a Go installation; the committed output
has no runtime dependency on the toolchain.
## Supported Platforms
## Install
The toolkit runs on Linux. All four Linux architectures are supported as
hosts — amd64, arm64, riscv64 and loong64 — and the release matrix
cross-compiles the same four targets.
Prebuilt binaries for linux/amd64, linux/arm64, linux/riscv64 and
linux/loong64 are on the
[releases page](https://sourcedock.dev/petrbalvin/gasm-devkit/releases).
From source (Go 1.27 or later):
**FreeBSD support is planned for a future release.**
```sh
go install sourcedock.dev/petrbalvin/gasm-devkit/cmd/gasm@latest
```
## Principles
Or from a repository checkout, with the development version stamped:
- **Pure Go and GAsm only.** No C, no cgo, no external toolchains, no native
binaries, no JavaScript runtimes. The parser is hand-written; there is no
parser generator.
- **Self-contained.** The toolkit's production code depends only on the
standard library; one binary, no runtime data files. The single module
dependency, `golang.org/x/arch`, is used **only in tests** to validate the
instruction encoder by round-trip decoding — it is never linked into the
`gasm` binary.
- **Linux-only.** Runs natively on amd64, arm64, riscv64 and loong64 Linux
hosts; the release matrix cross-compiles the same four targets. Latest
stable Go only.
- **No vendor lock-in.** The integration surface is the Language Server
Protocol and a command-line interface — both open standards. No cloud
service, no proprietary API, no dependence on any one editor's internals.
- **Complete and verifiable.** Instruction coverage is generated from the
assembler's own source and regenerated on demand, so it cannot silently fall
behind the toolchain.
## Components
| Package | Purpose |
|---------|---------|
| `token` | Lexical token kinds and source positions. |
| `lexer` | Hand-written scanner for Plan 9 assembly. |
| `ast` | The abstract syntax tree. |
| `parser` | Line-oriented, error-tolerant parser producing the AST. |
| `arch` | amd64, arm64, riscv64 and loong64 register files and instruction tables. |
| `lint` | Conservative static checks (13 rules including unused-label, invalid-textflag, stack-imbalance). |
| `s` | A canonical formatter — `gofmt` for assembly. |
| `asm` | The standalone assembler: all four architecture encoders, linker, object-file emitters (ELF with DWARF5, GOOBJ). |
| `verify` | JIT execution substrate for dynamic analysis, combined ABI+fuzz differential testing. Assembly trampolines for all four architectures. |
| `debug` | Interactive ptrace debugger for all four architectures: single-stepping, breakpoints, hardware watchpoints, register and memory inspection. |
| `lsp` | Language Server Protocol server: completion, hover, symbols, diagnostics, semantic tokens, find references, rename, formatting, inlay hints. |
| `cmd/gasm` | The `gasm` binary tying it all together. |
| `_gen` | The generator that rebuilds the instruction tables from the Go toolchain. |
See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md) for the design rationale and
data flow, and [`docs/DECISIONS.md`](docs/DECISIONS.md) for design decisions
deliberately postponed (with the analysis needed to pick them up again).
```sh
just install-bin
```
## Quick start
```sh
just install # download dependencies (there are none)
just build # go vet + gofmt check — zero errors, zero warnings
just test # full suite, race detector, 80 % coverage gate
just fmt # gofmt the tree
just gen # regenerate the instruction tables from the Go toolchain
cat > hello_amd64.s <<'EOF'
#include "textflag.h"
// func add(a, b int) int
TEXT ·add(SB), NOSPLIT, $0-24
MOVQ a+0(FP), AX
ADDQ b+8(FP), AX
MOVQ AX, ret+16(FP)
RET
EOF
gasm lint hello_amd64.s # static checks
gasm asm -o hello.bin hello_amd64.s # assemble to a raw image
gasm verify --call add --args a=2,b=3 hello_amd64.s # JIT-call it with arguments
```
Install the binary and use it:
## Usage
```sh
just install-bin # installs gasm into $GOBIN
gasm --help # overview of commands and flags
gasm tokens kernel_amd64.s # dump the token stream
gasm parse kernel_amd64.s # parse, report syntax errors
gasm fmt -w kernel_amd64.s # canonicalise in place
gasm fmt # reformat every .s below here, like go fmt
gasm lint *.s # static checks
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
gasm verify kernel_amd64.s # JIT-load and report functions
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
gasm verify --call decodeBlockAVX2 --buf src:64:hex...,dst:256:zero k.s
gasm verify --call sumTo --args n=57,base=0x1f k.s # scalar arguments
gasm debug --func name k.s # interactive debugger
gasm fmt # reformat every .s below here, like go fmt
gasm fmt -w kernel_amd64.s # canonicalise one file in place
gasm lint *.s # static checks
gasm asm --format elf -o k.o k.s # assemble to a linkable ELF object
gasm asm --format goobj -p pkg/path -o k.o k.s # Go object, consumed by go build
gasm verify --ground-truth k.s # byte-for-byte vs go tool asm
gasm verify --fuzz k.s # differential fuzz vs the go tool asm build
gasm debug --func name k.s # interactive debugger
gasm debug --func name --script cmds.txt --timeout 30s k.s # headless run
gasm debug --func name --cover k.s # which labels did execution reach?
gasm audit-instructions # encoder vs go tool asm name diff
gasm scaffold differential k.s # generate a differential test skeleton
gasm diff a.s b.s # compare machine code byte-for-byte
gasm debug --func name --cover k.s # which labels did execution reach?
gasm diff a.s b.s # compare machine code byte-for-byte
gasm diff --map wideCopyAVX2=wideCopyAVX512 avx2.s avx512.s
gasm profile k.s # show basic-block structure
gasm profile k.s # show basic-block structure
gasm audit-instructions # encoder vs go tool asm name diff
gasm scaffold differential k.s # generate a differential test skeleton
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for the full development workflow,
[docs/CLI.md](docs/CLI.md) for the command reference, and
[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for setup and recipes.
Run `gasm --help` for the command overview and `gasm <command> -h` for a
command's flags. [docs/CLI.md](docs/CLI.md) is the full reference.
## Editor integration
### Editor integration
`gasm lsp` speaks the Language Server Protocol over standard input/output, so
any LSP-capable editor can use it — point your editor's LSP client at the
any LSP-capable editor can use it: point your editor's LSP client at the
binary and associate it with `.s` files. Syntax highlighting is delivered as
**LSP semantic tokens**, so no editor-specific grammar is required. The server
LSP semantic tokens, so no editor-specific grammar is required. The server
infers the target architecture from the file-name suffix
(`_amd64.s` / `_arm64.s` / `_riscv64.s` / `_loong64.s`).
## License
## Development
```sh
just install # download module dependencies
just build # go vet + gofmt check, zero errors and zero warnings
just test # full suite, race detector, 80 % coverage gate
just fmt # gofmt the tree
just gen # regenerate the instruction tables from the Go toolchain
```
See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and
[docs/DEVELOPMENT.md](docs/DEVELOPMENT.md) for setup details and every
recipe.
## Documentation
- [docs/ARCHITECTURE.md](docs/ARCHITECTURE.md): components and data flow
- [docs/CLI.md](docs/CLI.md): full command reference
- [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md): development setup and recipes
- [docs/DECISIONS.md](docs/DECISIONS.md): deferred design decisions
- [CHANGELOG.md](CHANGELOG.md): release history
## Licence
BSD-3-Clause — see [LICENSE](LICENSE).
BSD-3-Clause — see [LICENSE](LICENSE).
Copyright © 2026 [Petr Balvín](https://petrbalvin.org)
+89 -60
View File
@@ -7,7 +7,7 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
## Design goals
1. **A real AST, not a grammar hack.** The linter, analyser, assembler and
language server all need to *reason* about assembly — not just colour it.
language server all need to *reason* about assembly, not just colour it.
So the centre of the toolkit is a hand-written lexer and a parser that
produce a typed AST with source positions on every node.
2. **Architecture as data, not code.** Per-architecture differences (amd64,
@@ -26,7 +26,7 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
graph TD
SRC["source .s"] --> LEX["lexer<br/>token stream"]
LEX --> PAR["parser<br/>AST + diagnostics"]
LEX --> FMT["s<br/>re-space tokens"]
LEX --> FMT["format<br/>re-space tokens"]
PAR --> LINT["lint<br/>static checks"]
PAR --> LSP["lsp server"]
LEX --> LSP
@@ -69,8 +69,8 @@ instruction, comment, preprocessor) and dispatches. A malformed line is
reported and skipped; it never aborts the file.
Operands are parsed into a faithful, flat representation. The amd64
addressing modes — `reg`, `$imm`, `(base)`, `off(base)`, `(base)(index*scale)`,
`name+off(FP)`, `name<>(SB)` — are all captured structurally, and the original
addressing modes (`reg`, `$imm`, `(base)`, `off(base)`, `(base)(index*scale)`,
`name+off(FP)`, `name<>(SB)`) are all captured structurally, and the original
token text is retained for fidelity.
A deliberate boundary: the AST records **syntax only**. Whether a bare
@@ -80,8 +80,8 @@ arch-agnostic and its output deterministic.
### `arch`
Register files are generated programmatically (the regular `R8`–`R15`,
`X0`–`X15`, `Y0`–`Y15`, `Z0`–`Z31`, `K0`–`K7` ranges) plus the irregularly
Register files are generated programmatically (the regular `R8`-`R15`,
`X0`-`X15`, `Y0`-`Y15`, `Z0`-`Z31`, `K0`-`K7` ranges) plus the irregularly
named registers listed explicitly. Instruction names are **generated from the
Go toolchain's own assembler source** (`cmd/internal/obj/<arch>/anames.go`,
plus the common opcodes and the per-architecture front-end aliases such as the
@@ -95,11 +95,13 @@ fixed-arity instructions (`RET`, `NOP`, `JMP`, `CALL`) carry counts at all.
### `lint`
Rules are conservative by design — silence beats a false positive. The rules
Rules are conservative by design: silence beats a false positive. The rules
are `unknown-instruction`, `operand-count`, `undefined-label`,
`duplicate-label`, `missing-ret`, `missing-textflag-include`, `abi-argsize`,
`unreachable-code`, `register-clobber`, `funcdata-pcdata`, `unused-label`,
`invalid-textflag` and `stack-imbalance`. Every diagnostic carries a stable
`invalid-textflag`, `stack-imbalance`, `register-width-mismatch`,
`abi0-register-args`, `nonportable-register-name` and
`unencodable-instruction`. Every diagnostic carries a stable
code so callers can disable rules individually, and arch-specific rules switch
off entirely when the target architecture cannot be inferred from the file name.
@@ -107,7 +109,7 @@ Two things keep the rules honest on real-world code:
- **Pseudo-ops and macros are not instructions.** `unknown-instruction` knows
the assembler pseudo-ops (`BYTE`, `WORD`, `FUNCDATA`, `PCDATA`, …) and
recognises macro invocations — an in-file `#define` name, or any identifier
recognises macro invocations: an in-file `#define` name, or any identifier
containing an underscore (no Plan 9 mnemonic ever does).
- **Macro-heavy files get the label/RET heuristics turned off.** Without a
preprocessor, labels a macro defines are invisible, so `undefined-label` and
@@ -128,27 +130,27 @@ Two deeper analyses sit on top of the AST:
parameters), and checks the total against the argument size declared in the
`TEXT` directive. It only runs for stack-argument functions (a non-zero
declared arg area that is actually addressed through `FP`), and aborts
silently on a type whose size it cannot determine — so it never guesses.
silently on a type whose size it cannot determine, so it never guesses.
- **`unreachable-code`.** Code after a `RET` and before the next label is
dead. The check is suppressed for any function whose reachability cannot be
decided statically: those using PC-relative jumps (`JMP 2(PC)`),
register-indirect branches (`JALR`/`JR`/`JIRL`/`BR`/`BLR`), or living in a
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator —
file with `#ifdef` conditionals. `UNDEF` is deliberately not a terminator:
code after it is occasionally intentional metadata.
- **`register-clobber` (register liveness).** The linter builds the function's
control-flow graph (basic blocks split at labels and after branches, with
fall-through and jump-target edges), computes a conservative per-instruction
register def/use, and runs the standard backward liveness iteration to a fixed
point. On top of that it flags writes to the registers the **Go ABI** fixes
across calls that are never saved and restored — calibrated from
across calls that are never saved and restored, calibrated from
`cmd/compile/abi-internal.md`, *not* the platform ABI: Go's stack-based ABI0
has no System V style callee-saved registers (amd64 `BX`, `R12`–`R15` and
has no System V style callee-saved registers (amd64 `BX`, `R12`-`R15` and
the like are caller-saved or permanent scratch, and hand-written kernels may
clobber them freely). The audited set is the frame pointer and the
the frame pointer, the goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
goroutine pointer per architecture (amd64 `BP`/`R14`, arm64 `R18`/`R28`/
`R29`, riscv64 `X27`, loong64 `R22`); the goroutine pointer is reported only
when the function can reach the runtime — it is not `NOSPLIT` or makes a
call — since the ABI0 transition machinery restores it on those paths, and
when the function can reach the runtime (it is not `NOSPLIT` or makes a
call), since the ABI0 transition machinery restores it on those paths, and
NOSPLIT call-free leaves may use it (the runtime's own assembly does). It
runs only on macro-free files, where
no opaque macro can perform the save/restore.
@@ -157,16 +159,16 @@ Two deeper analyses sit on top of the AST:
reference) and a literal index is range-checked; a named index constant such
as `$PCDATA_StackMapIndex` is accepted without a range check.
### `s`
### `format`
The formatter works on the **token stream, not the AST**, so it preserves
every line — comments and blanks included. It normalises indentation, operand
every line, comments and blanks included. It normalises indentation, operand
spacing, per-function mnemonic alignment and blank-line layout: a new block
(a label, `TEXT` or `GLOBL`) is preceded by exactly one blank line (comments
leading a block stay with it), runs of blanks collapse to one, and a `RET`
terminates the body so the next function's doc comment stays at column 0. It
is idempotent and its output always round-trips through the parser. With a
directory argument — or none — it reformats every `.s` file below it in
directory argument, or none, it reformats every `.s` file below it in
place and lists the files changed, the way `go fmt` does (`.` and `_`
directories are skipped).
@@ -176,13 +178,20 @@ The server speaks JSON-RPC 2.0 with `Content-Length` framing over any
`io.Reader`/`io.Writer` (normally stdin/stdout). It maintains an in-memory
document store, republishes diagnostics on every change, and provides:
- **completion** — instructions, registers, pseudo-registers, textflag macros
- **completion**: instructions, registers, pseudo-registers, textflag macros
and local labels;
- **hover** — instruction summaries and register descriptions from `arch`;
- **document symbols** — `TEXT` functions with their labels, plus `GLOBL`/`DATA`;
- **semantic tokens** — syntax highlighting delivered as LSP semantic tokens,
- **hover**: instruction summaries and register descriptions from `arch`;
- **document symbols**: `TEXT` functions with their labels, plus `GLOBL`/`DATA`;
- **semantic tokens**: syntax highlighting delivered as LSP semantic tokens,
classified with the lexer plus `arch` (instructions, registers by class,
pseudo-registers, labels, immediates, comments, directives, textflag macros).
pseudo-registers, labels, immediates, comments, directives, textflag macros);
- **navigation**: go-to-definition from a label reference to its definition,
find references, document highlights of every use of the symbol under the
cursor, rename, and workspace symbol search over the open documents;
- **assists**: document formatting through the `format` package, inlay hints
(the frame size after the TEXT argument area), signature help (the callee's
`// func` signature while the cursor is on a `CALL`), and code actions
offering quick fixes for the `missing-ret` and `unused-label` diagnostics.
Semantic tokens are the key to editor-agnostic highlighting: the editor renders
them from the standard LSP legend, so no editor-specific grammar is needed.
@@ -192,7 +201,7 @@ them from the standard LSP legend, so no editor-specific grammar is needed.
The standalone assembler (Phase 2). Its core is an amd64 instruction encoder:
a REX/ModR-M/SIB/displacement/immediate engine plus the scalar instruction set,
with the Plan 9 operand order (source first) mapped onto the x86 encoding.
Every encoding is validated by decoding it again with `golang.org/x/arch` — the
Every encoding is validated by decoding it again with `golang.org/x/arch`, the
one module dependency, used in tests only and never linked into the binary.
A **RISC-V encoder** (Phase 5, RV64IMAFDC + RVC compression) encodes the full
@@ -232,12 +241,12 @@ fixed point, and jump-to-jump chains are folded (a conditional jump to a label
whose only instruction is an unconditional jump is redirected to the ultimate
target) exactly as the Go toolchain's linker does before it encodes branches.
The `FP`/`SP` pseudo-
registers are translated onto the hardware stack pointer — `x+N(FP)` becomes
registers are translated onto the hardware stack pointer: `x+N(FP)` becomes
`(N+8)(SP)` for a zero-frame function and `(N+frame+16)(SP)` once a frame
pointer is set up, with the matching Go prologue/epilogue generated — so the
pointer is set up, with the matching Go prologue/epilogue generated, so the
output is byte-identical to the Go assembler for these cases. SIMD is handled
by a VEX (AVX/AVX2) encoder — the two- and three-byte VEX prefixes with XMM/YMM
registers — across eight operand forms: the three-operand NDS form, the
by a VEX (AVX/AVX2) encoder (the two- and three-byte VEX prefixes with XMM/YMM
registers) across eight operand forms: the three-operand NDS form, the
two-operand reg/rm form, the immediate-shift form (plus the variable-count
shifts, which share the NDS shape with the count in an XMM register or
memory), the immediate shuffle form (`VPSHUFD`, `VPERMQ`), the
@@ -245,24 +254,24 @@ three-operand-plus-immediate form (`VSHUFPD`,
`VPERM2I128`, `VINSERTI128`), the lane-extract form (`VEXTRACTI128`,
`VEXTRACTF128`, where the YMM source occupies the reg field and the XMM or
memory destination r/m), the direction-sensitive moves (`VMOVDQU`, `VMOVUPD`,
`VMOVD`, `VMOVQ`, `VMOVSD`), the floating-point and FMA arithmetic — the
`VMOVD`, `VMOVQ`, `VMOVSD`), the floating-point and FMA arithmetic: the
packed double operations (`VADDPD`/`VSUBPD`/`VMULPD`/`VDIVPD`/`VMINPD`/
`VMAXPD`), the unpacks (`VUNPCKHPD`/`VUNPCKLPD`), the scalar SD and SS
operations, `VMOVDDUP`, `VXORPD`, the width-changing conversions
(`VCVTDQ2PS`, `VCVTPS2PD`, `VCVTDQ2PD`, and the `VCVTPD2DQX`/`Y` and
`VCVTTPD2DQX`/`Y` spellings, whose length follows the wider source) and
`VFMADD231PD` — and the no-operand `VZEROUPPER`, together with `VPERMD` and
`VFMADD231PD`, and the no-operand `VZEROUPPER`, together with `VPERMD` and
the scalar families (`CMOVcc`, `SETcc`, `LZCNT`/`TZCNT`, the extending moves,
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix — the four-byte prefix with
5-bit register fields (Z0–Z31, X/Y 16–31, with the mod=11 quirk that carries
rm[4] in X̄), opmask registers (K0–K7 as operands, mask destinations and
explicit merging/zeroing masks — written the way Go writes them, as a K
`CVTSx2SD`, `IMUL3`) and the EVEX (AVX-512) prefix, the four-byte prefix with
5-bit register fields (Z0-Z31, X/Y 16-31, with the mod=11 quirk that carries
rm[4] in X̄), opmask registers (K0-K7 as operands, mask destinations and
explicit merging/zeroing masks, written the way Go writes them, as a K
operand among the operands plus a `.Z` mnemonic suffix), and the compressed
disp8×N displacement, whose multiplier follows the memory operand's size —
disp8×N displacement, whose multiplier follows the memory operand's size,
covering every instruction the go-flac and go-lz4 AVX2/AVX-512 kernels use,
plus the common AVX-512 F/BW integer set, the floating-point and conversion
set (the packed double and single arithmetic, the scalar SD/SS forms —
whose EVEX encodings serve masked and zeroing use — `VMOVDDUP`, the
set (the packed double and single arithmetic, the scalar SD/SS forms
(whose EVEX encodings serve masked and zeroing use), `VMOVDDUP`, the
replicating moves, and the width-changing conversions, including the
`VCVTPD2DQ`/`VCVTTPD2DQ` family whose length follows the wider source
operand), and the wider AVX-512 set: ternary logic, lane shuffles, inserts
@@ -272,21 +281,21 @@ expand/compress family, the broadcasts, the opmask-register instructions
moves and the remaining extending/narrowing moves, the floating-point
helper and conversion tail (VRCP14*, VRSQRT14*, VGETEXP*, VGETMANT*,
VSCALEF*, VRNDSCALE*, VREDUCE*, VFIXUPIMM*, VRANGE*, VFPCLASS* with an
opmask destination, and the VCVT* conversions — signed, unsigned and
opmask destination, and the VCVT* conversions, signed, unsigned and
truncating, including the length-suffixed X/Y spellings and the
mask/vector conversions VPMOVM2*/VPMOV*2M, and the scalar conversions
between vector and general-purpose registers (VCVT{,T}S{D,S}2SI{,Q} and
the unsigned forms, VCVTSI2*/VCVTUSI2*), and gather/scatter with VSIB addressing — both the
the unsigned forms, VCVTSI2*/VCVTUSI2*), and gather/scatter with VSIB addressing, both the
VEX spelling with a vector mask register and the EVEX spelling with an
explicit K mask, where the EVEX length follows the VSIB index register,
not the data register. The EVEX mnemonic
suffixes — rounding modes (.RN_SAE/.RD_SAE/.RU_SAE/.RZ_SAE),
suppress-all-exceptions (.SAE) and memory broadcast (.BCST) — set the EVEX
suffixes (rounding modes (.RN_SAE/.RD_SAE/.RU_SAE/.RZ_SAE),
suppress-all-exceptions (.SAE) and memory broadcast (.BCST)) set the EVEX
b bit and the L'L rounding-control field (broadcast keeps the vector length
and scales disp8 by the element size), and combine with the .Z zeroing
suffix. Every encoding is validated two ways: by
round-trip decoding through `golang.org/x/arch`, and byte-for-byte against
the machine code the real Go assembler emits — a comparison that holds for
the machine code the real Go assembler emits, a comparison that holds for
whole functions: all 27 functions of both kernels assemble to exactly the Go
toolchain's bytes, the lone exception being the displacements of the
static-constant loads, which the Go linker fills at link time.
@@ -300,14 +309,17 @@ the bytes are self-consistent at any base address. References to symbols no
object-file emitters turn the whole image into a linkable object: the ELF
writer (`gasm asm --format elf`) lays the code and data out as `.text`/`.data`
sections, exports a symbol per
`TEXT` and `GLOBL` (the `<>` ones local, the rest global) and emit one
PC-relative relocation per static-symbol reference — undefined external
symbols included, so the output links with the system toolchain. The GOOBJ
`TEXT` and `GLOBL` (the `<>` ones local, the rest global), emits one
PC-relative relocation per static-symbol reference, undefined external
symbols included, and appends the DWARF5 debug sections
(`.debug_abbrev`, `.debug_info`, `.debug_line`, `.debug_line_str`, and a
`.debug_frame` CFI section on amd64) so `addr2line` and GDB/LLDB can debug
the output. The GOOBJ
emitter (`gasm asm --format goobj`) writes the format the Go linker consumes
directly: the functions as non-package symbols (the way `cmd/asm` records
assembly symbols), the `GLOBL` data, one `FuncInfo` per function and the
pc-value tables — `pcsp` built from the prologue and epilogue stack
boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables — so a
pc-value tables (`pcsp` built from the prologue and epilogue stack
boundaries, plus flat `pcfile`, `pcline` and `pcinline` tables), so a
gasm-assembled object drops into a `go build` in place of the toolchain's.
The object preamble (the version-and-experiment header the linker compares
verbatim) is captured from the installed `go tool asm`, so the output is
@@ -315,18 +327,21 @@ always consistent with the toolchain that links it. RISC-V and LoongArch
GOOBJ emission share this emitter: the loong64 marker with
R_LOONG64_ADDR_HI/LO relocation types, and the riscv64 marker with a single
R_RISCV_PCREL_ITYPE/STYPE relocation per AUIPC pair (plus `R_RISCV_JAL` for
`CALL sym(SB)`) — the model `cmd/asm`
writes, not the ELF HI20/LO12 pair — and both link into a real `go build` for
`CALL sym(SB)`), the model `cmd/asm`
writes, not the ELF HI20/LO12 pair, and both link into a real `go build` for
their `GOARCH`. Per function, the emitter also writes the two DWARF
symbols the linker's DWARF pass reads verbatim — the subprogram DIE
symbols the linker's DWARF pass reads verbatim: the subprogram DIE
(`SDWARFFCN`) and the `.debug_line` state-machine program (`SDWARFLINES`),
both built the way `cmd/asm` builds them (the DIE carries the
R_DWTXTADDR_U4 address reference; the line program one row per source-line
change, in the same special-opcode encoding) — and the pc-value deltas are
change, in the same special-opcode encoding), and the pc-value deltas are
in the architecture's MinLC units, as the runtime's `pcvalue` expects.
External cross-package references remain future work (the amd64 and RISC-V
paths resolve them; LoongArch does not yet); the rest of Phase 2 is those
and the remaining EVEX forms.
Cross-package external references resolve on all four architectures: when
`img.Externals` is non-empty, the GOOBJ emitter locates the referenced
package's `.a` archive via `go list -json -export`, reads the GOOBJ symbol
definitions the linker reads, and wires the resolved package and symbol
indices into the emission, so a gasm-assembled object links against the
compiled packages it references.
### `verify`
@@ -343,6 +358,9 @@ stack pointer in a package global and jumps to the target), and recovers
control when the function RETs into `leaveJIT` (which restores the Go stack
and returns). A 64-byte pad below the return address accommodates the
ABIInternal wrapper that the Go runtime interposes on assembly functions.
Every supported architecture carries its own hand-written trampoline pair
(`trampoline_amd64.s`, `trampoline_arm64.s`, `trampoline_riscv64.s`,
`trampoline_loong64.s`), so `Call` works wherever the toolkit runs.
`Load` / `LoadSource` / `LoadAST` parse, assemble and map a `.s` file in one
step, returning a `Kernel` whose `CallFunc` method marshals the argument block
@@ -351,16 +369,20 @@ assembler’s `Image.Bytes()` provides the code-and-data concatenation.
The `gasm verify` CLI subcommand exposes this: it loads a file, reports the
available functions and (with `-smoke`) calls each NOSPLIT function with zeroed
arguments to confirm the trampoline round-trips. `gasm verify --fuzz` combines
arguments to confirm the trampoline round-trips. The `-smoke` and `-abi`
sweeps run in parallel and each inside a child process, so a function that
faults is reported without ending the sweep. `gasm verify --fuzz` combines
ABI checks (sentinel registers, canary, stack bounds) with differential fuzz
testing, comparing the JIT-assembled kernel against the portable Go reference
bit-for-bit while verifying the ABI contract on every iteration. When a fuzz
iteration crashes or mismatches, `FuzzResult.CrashInput` stores the exact input
for reproducibility. `gasm verify --call <func> --buf name:size:pattern`
invokes a single function with user-supplied buffers (patterns: zero, ones,
seq, or hex), printing the ABI0 argument block before and after the call —
seq, or hex), printing the ABI0 argument block before and after the call,
useful for partial functions (e.g. decoders) that crash on random input but
should succeed on valid data. `gasm verify --ground-truth` compares the
should succeed on valid data; `--args name=value,...` supplies scalar
arguments (decimal or `0x` hex) alongside the buffers. `gasm verify
--ground-truth` compares the
assembled machine code byte-for-byte against `go tool asm` (relocation sites
masked), reporting any encoding drift.
@@ -375,8 +397,15 @@ The child pins its goroutine to the OS thread with `runtime.LockOSThread`
so the traced thread is the one executing JIT code. The REPL provides
single-step, register inspection (GPR + YMM/XMM via `PTRACE_GETFPREGS`),
label resolution, named buffer allocation with pattern filling
(`--buf name:size:pattern` — zero, ones, seq, or hex), and breakpoint
management.
(`--buf name:size:pattern`: zero, ones, seq, or hex), and breakpoint
management. Breakpoints accept conditions
(`break <label> if <reg> <op> <val>`, including register-against-register
comparisons), and hardware watchpoints work on all four architectures.
For non-interactive use, `--script` runs REPL commands from a file (or
stdin) and exits, `--timeout` kills the debuggee when a run hangs (the
watchdog is armed before the ptrace attach, so a sandboxed debuggee cannot
block it), and `--cover` runs to completion with a breakpoint on every
label and reports which blocks executed.
## Extension points
+35 -8
View File
@@ -48,7 +48,9 @@ an error-severity diagnostic is found.
Rules: `unknown-instruction`, `operand-count`, `undefined-label`,
`duplicate-label`, `missing-ret`, `missing-textflag-include`,
`abi-argsize`, `unreachable-code`, `register-clobber`,
`funcdata-pcdata`.
`funcdata-pcdata`, `unused-label`, `invalid-textflag`,
`stack-imbalance`, `register-width-mismatch`, `abi0-register-args`,
`nonportable-register-name` and `unencodable-instruction`.
## `gasm asm [--format raw|elf|goobj] [-p pkg] [-o out] <file>`
@@ -75,6 +77,7 @@ Assemble FILE, map it into executable memory, and run dynamic checks.
| `--smoke` | Call each NOSPLIT function with zeroed args |
| `--call <func>` | Invoke a single function with `--buf` instead of the sweeps |
| `--buf <spec>` | Buffer spec for `--call`: `name:size:pattern[,name:size:pattern]` |
| `--args <spec>` | Scalar args for `--call`: `name=value[,name=value]` (decimal or `0x` hex) |
| `--repeat <n>` | Number of times to repeat a `--call` invocation (default: 1) |
The `--fuzz` mode runs each function in a subprocess; a partial function
@@ -85,18 +88,23 @@ partial functions with valid data instead.
The `--call` mode parses the `// func` signature, allocates the requested
buffers (`zero`, `ones`, `seq`, or a hex blob), builds the ABI0 argument
block with buffer pointers/lengths/capacities at the matching parameter
offsets, and prints the arg block before and after the call — showing
return values and any output written to the buffers.
offsets, and prints the arg block before and after the call, showing
return values and any output written to the buffers. Scalar parameters
are supplied with `--args` (decimal, or `0x` hex) at their ABI0 offsets.
## `gasm debug --func <name> [--buf spec] <file.s>`
## `gasm debug [--func <name>] [--buf spec] [--script file] <file.s>`
Interactive debugger for JIT-assembled functions (amd64, arm64, riscv64, loong64). Requires a
compiled binary on `$PATH` (not `go run`).
Interactive debugger for JIT-assembled functions (amd64, arm64, riscv64,
loong64). Requires a compiled binary on `$PATH` (not `go run`).
| Flag | Description |
|------|-------------|
| `--func` | Function to debug (required) |
| `--buf` | Buffer spec: `name:size:pattern[,name:size:pattern]` |
| `--args <file>` | File containing the ABI0 argument block |
| `--script <file>` | Run REPL commands from a file (one per line) and exit; `-` reads stdin |
| `--timeout <dur>` | Kill the debuggee after this duration (e.g. `30s`); for headless `--script` runs |
| `--cover` | Run to completion with breakpoints on every label and report which blocks executed |
REPL commands:
@@ -146,6 +154,23 @@ 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.
## `gasm audit-instructions`
Compare the gasm amd64 encoder against the installed `go tool asm` and
print the diff: superset encodings (gasm-only spellings, shippable via
`gasm asm --format goobj`), known-but-unencodable names (the encoder
backlog) and go-only names (feature gaps). The Go side is probed
black-box with a battery of operand shapes per mnemonic, so the audit
tracks whatever toolchain `go env GOROOT` provides.
## `gasm scaffold differential <file.s>`
Print a differential test skeleton for every `// func` signature in
FILE. The generated test seeds random states, drives the kernel and a
portable reference (`<name>Portable`), and compares outputs
byte-for-byte. Write the reference bodies, place the file in the
kernel's package, and run it in CI.
## `gasm lsp`
Run the language server over standard input/output (JSON-RPC 2.0 with
@@ -154,5 +179,7 @@ associate it with `.s` files. The target architecture is inferred from
the file-name suffix (`_amd64.s`, `_arm64.s`, `_riscv64.s`,
`_loong64.s`).
Provides: completion, hover, document symbols, diagnostics, and
semantic-token highlighting.
Provides: completion, hover, document symbols, diagnostics, semantic
tokens, go-to-definition, find references, rename, document formatting,
inlay hints, code actions, signature help, document highlights, and
workspace symbol search.
+2 -2
View File
@@ -14,7 +14,7 @@ why, the options on the table, and the trigger that should reopen it.
would have required either `golang.org/x/tools` or an in-house parser), the
resolver reads the **GOOBJ data directly** from the target package's `.a`
archive. The `.a` file contains a `_go_.o` member whose GOOBJ s is the
same one gasm writes — the parser reuses the same layout (`blkSymdef`,
same one gasm writes; the parser reuses the same layout (`blkSymdef`,
`blkNonpkgdef`, the string table), so no new dependency was needed.
**How it works.**
@@ -23,7 +23,7 @@ same one gasm writes — the parser reuses the same layout (`blkSymdef`,
2. `extractGOOBJ` reads the ar archive, finds the `_go_.o` member, skips
the `"go object …\n!\n"` preamble and parses the GOOBJ header.
3. `goobjFile.symbols()` walks `blkSymdef` and `blkNonpkgdef` in definition
order — the same order the linker uses — to build the symbol → index
order (the same order the linker uses) to build the symbol-to-index
mapping.
4. `resolveExternalSymbols` wires the resolved `{PkgIdx, SymIdx}` into the
GOOBJ emission.
+17 -9
View File
@@ -5,7 +5,7 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
## Prerequisites
- **Go** 1.27+ with `toolchain go1.27.0`
- **just** — the command runner; every task below is a just recipe
- **just**, the command runner; every task below is a just recipe
- No external dependencies beyond the Go toolchain
## Quick Start
@@ -14,12 +14,17 @@ Repository: [sourcedock.dev/petrbalvin/gasm-devkit](https://sourcedock.dev/petrb
git clone https://sourcedock.dev/petrbalvin/gasm-devkit.git
cd gasm-devkit
just install # go mod download
just build # go vet + gofmt — must pass with zero output
just build # go vet + gofmt, must pass with zero output
just test # full suite, race detector, 80 % coverage gate
```
## Just Recipes
### `just install`
`go mod download`. The only module dependency, `golang.org/x/arch`, is
used in tests only.
### `just build`
Runs `go vet ./...` and checks `gofmt -l .` produces no output. This is
@@ -28,10 +33,13 @@ the minimum bar before any commit.
### `just test`
```sh
go test -race -count=1 -coverprofile=coverage.out ./...
go test -race -count=1 ./...
```
Plus an `awk` gate that fails if total coverage is below 80 %.
Plus a coverage run over the ten analysable packages (arch, asm, ast,
format, lexer, lint, lsp, parser, token, verify; `debug` and `cmd/gasm`
need hardware or are CLI glue) and an `awk` gate that fails if total
coverage is below 80 %.
### `just fmt`
@@ -61,7 +69,7 @@ embedded via `-ldflags "-X main.version=..."`.
Regenerates the architecture instruction tables in `arch/` by parsing
the Go toolchain's own assembler source
(`$GOROOT/src/cmd/internal/obj/<arch>/anames.go`). Requires a Go
installation. Output is committed — no runtime dependency on the
installation. Output is committed, with no runtime dependency on the
toolchain.
### `just uninstall`
@@ -72,15 +80,15 @@ Removes `coverage.out`, the `gasm` binary, and `*.test` artefacts.
```sh
go test -run TestVexGroundTruth ./asm/
go test -run TestDifferentialLZ4Fuzz ./verify/
go test -run TestFLACDecorrelate ./verify/
go test -run TestGroundTruthBasic ./verify/
go test -run TestGOObjectLinkAndRun ./asm/
go test -run TestFuzzWideCopy ./verify/
```
## Debugger Note
`gasm debug` spawns a child process from the binary on `$PATH`. It does
not work with `go run` — install first:
not work with `go run`; install first:
```sh
just install-bin
@@ -97,7 +105,7 @@ ast/ Abstract syntax tree
parser/ Line-oriented parser
arch/ Register and instruction tables (generated)
lint/ Static analysis rules
s/ Canonical formatter
format/ Canonical formatter
lsp/ Language Server Protocol server
asm/ Standalone assembler, encoder, object emitters
verify/ JIT execution, differential testing, ABI checks