# Development Guide Repository: [sourcedock.dev/petrbalvin/gasm-sdk](https://sourcedock.dev/petrbalvin/gasm-sdk) ## Prerequisites - **Go** 1.27.1, the exact version the `go` directive in `go.mod` declares - **just**, the command runner; every task below is a just recipe - **A C compiler** (`gcc`): `just race` runs the suite under the race detector, which needs cgo - **Perl**: the `test`, `fmt-check`, `install-man` and `uninstall-man` recipes are Perl programs - **`gzip`**: `install-man` compresses the man pages with it - A Linux host on amd64, arm64, riscv64 or loong64: `gasm debug` needs ptrace and the JIT checks of `gasm verify` need executable memory - **`golang.org/x/arch`**, the one module dependency, which the Go toolchain fetches; nothing else sits outside the standard library ## Setup ```sh git clone https://sourcedock.dev/petrbalvin/gasm-sdk.git cd gasm-sdk just build # compile bin/gasm, zero errors and zero warnings just gates # build, fmt-check, vet, test, race: the definition of done ``` ## Recipes Every recipe in the `justfile`, and what it does. | Recipe | What it does | |---|---| | `default` (bare `just`) | prints the recipe list (`@just --list`) | | `just build` | compiles `bin/gasm` with `CGO_ENABLED=0`, `-trimpath` and `-buildvcs=true`, symbols stripped; zero errors and zero warnings | | `just test` | the test gate: the full suite with `-count=1` and `-timeout 0` under the 4 GiB memory fence, the coverage profile and the 80 % floor, then the CLI and debugger tests outside the profile | | `just race` | the same suite under the race detector, under its own 16 GiB fence; the expensive one, so it runs once, inside `gates` | | `just unit [packages] [run]` | fast, cached, scoped run for iterating, under the fence: no race and no coverage, so an unchanged package reports instantly | | `just fuzz [fuzztime]` | time-boxed fuzz of one target, under the fence; the package is required, because `go test -fuzz` refuses more than one | | `just link-parity` | the opt-in cmd/link GOOBJ parity gate; it costs minutes no pipeline can afford | | `just bench [packages]` | benchmarks (`-benchmem -count=5`); deliberately unfenced, on an idle machine only | | `just fmt` | formats the tree in place with `gofmt` | | `just fmt-check` | zero diff; prints nothing when everything is formatted, which is the shape the CI step wants | | `just vet` | both static gates: `go vet` and `go fix -diff` | | `just gates` | `build`, `fmt-check`, `vet`, `test` and `race`, in that order: the definition of done | | `just clean` | removes the build artefacts, `bin/` and `coverage.out` | | `just install` | builds, then copies the binary into `bindir` (`~/.local/bin`) | | `just uninstall` | removes the installed binary from `bindir` | | `just install-man` | installs the man pages under `docs/man` into `~/.local/share/man/man1` (`MANDIR` overrides), gzip-compressed; not a gate | | `just uninstall-man` | removes the installed man pages | | `just run` | runs the CLI with `go run -buildvcs=true`; the recipe takes no arguments, so flags go through the package instead | | `just dev` | the same as `run`; the project has no watcher to add | | `just gen` | regenerates the `arch` instruction tables from the Go toolchain source; not a gate | ### `just test` ```sh go test -count=1 -timeout 0 -coverprofile=coverage.out \ ./arch/... ./asm/... ./ast/... ./disasm/... ./format/... ./lexer/... \ ./lint/... ./lsp/... ./parser/... ./token/... ./verify/... ``` The suite runs over the logic packages (`-count=1`, so no cached pass counts): arch, asm, ast, disasm, format, lexer, lint, lsp, parser, token, verify. `debug` traces a live process and `cmd/gasm` is thin CLI glue, so both sit outside the profile sweep, and a thin `cmd/` in it would drag the coverage total under the floor. Their tests still run, in a second invocation without a profile: ```sh go test -count=1 -timeout 0 ./cmd/... ./debug/... ``` That covers the CLI's exit codes and the guard that compares the manual pages with the binary's own help, and the debugger's units. The floor fails if the total is below 80 %. Both invocations run under a 4 GiB cgroup fence with swap off, so a runaway test dies as a failed run instead of eating the machine, and no timeout is set: `-timeout 0` disables the ten-minute default `go test` would otherwise impose. The push pipeline runs the same two commands with `-short`, the suite workflow and the local gate run them in full, so the number is the same everywhere. ### `just run` ```sh just run go run -buildvcs=true ./cmd/gasm lint kernel_amd64.s go run -buildvcs=true ./cmd/gasm verify --ground-truth kernel_amd64.s ``` The flag on `go run` is there because it does not stamp the build otherwise, which `--version` would then report as `(devel)`. ### `just gen` Regenerates the architecture instruction tables in `arch/` by parsing the Go toolchain's own assembler source (`$GOROOT/src/cmd/internal/obj//anames.go`). Requires a Go installation. Output is committed, with no runtime dependency on the toolchain. ## Running a single test ```sh go test -run TestVexGroundTruth ./asm/ go test -run TestGroundTruthBasic ./verify/ go test -run TestGOObjectLinkAndRun ./asm/ go test -run TestFuzzWideCopy ./verify/ ``` Add `-v` for the sub-test names, and `-race` when the change touches concurrency. `-count=1` defeats the test cache when a result looks stale. ## Coverage ```sh just test go tool cover -func=coverage.out ``` The `total:` line is the number that matters, and it stays at 80 percent or more. ## Debugging the build ```sh go build -gcflags='-m' ./... # inlining decisions go build -gcflags='-S' ./... # what the compiler generated go tool asm -S kernel_amd64.s # how the toolchain's assembler encodes a kernel gasm dis kernel_amd64.s # what gasm makes of the same kernel gasm tokens kernel_amd64.s # the token stream gasm profile kernel_amd64.s # the basic blocks of each function ``` `gasm verify --ground-truth` is the differential check that ties the two together: it compares gasm's bytes with `go tool asm`'s, with the relocation sites masked, so an encoding drift shows up as a byte difference rather than a crash later. ## Continuous integration Workflows live in `.gitea/workflows/` and run on the project's own runners: | Workflow | Trigger | What it does | |---|---|---| | Test | push or pull request to `development` | the format check, `go vet`, the short layer of the suite with the coverage profile and the 80 % floor, inside the two-minute budget | | Suite | dispatched by hand | the complete gate set minus race: the build, the format check, `go vet`, `go fix -diff`, the full suite and the coverage floor | | FreeBSD build | dispatched by hand | the three FreeBSD compile gates: `GOOS=freebsd` for amd64, arm64 and riscv64 | | Release | a `v*` tag | the matrix build, the version smoke test, and the release with its assets; no gate runs at the tag | The pipelines are written by hand rather than through `just`, but they enforce the same gates: the push path carries the affordable subset and the heavy tests skip under `testing.Short`, so the dispatched suite is the moment to prove the tree end to end. Race never runs in CI, on a push or a tag: it roughly doubles the time and the memory on a shared runner, and the local `just gates` races the tree on the machine at the keyboard before the tag is cut. ## Releases Releases are cut by merging `development` into `main` and tagging `vX.Y.Z`, which triggers the release workflow: it builds the portable Linux targets (amd64, arm64, loong64, riscv64), takes the notes from the matching `CHANGELOG.md` section and uploads the assets. No gate runs at the tag, so `just gates` must be green on the tree before the tag is cut. `SECURITY.md` carries the supported-versions table, naming the version being released; the release commit updates it by hand. The version is never injected. `gasm --version` prints what the toolchain recorded in the build information: the tag on a tagged checkout, a pseudo-version naming the commit below one, `+dirty` on a dirty tree, and `(devel)` outside version control. There is no `-ldflags "-X"` anywhere and no version constant in the source.