Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 51 additions & 42 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,84 +6,86 @@ Wasm file (with embedded WIT custom section) and generates Go bindings targeting

## Build & Run

Every routine operation is a `just` recipe; `just --list` shows them all.

### Prerequisites

- Rust toolchain (version pinned to what's in in `rust-toolchain.toml`)
- `wasm32-unknown-unknown` and `wasm32-wasip1` targets (configured in
`rust-toolchain.toml`)
- [just](https://just.systems/)
- Rust toolchain (version pinned in `rust-toolchain.toml`).
`just install` installs it with the `wasm32-unknown-unknown` and
`wasm32-wasip1` targets the file configures.
- Go (for running example tests)

The recipes ignore an inherited `RUSTUP_TOOLCHAIN` or `CARGO_TARGET_DIR`, so
they use the toolchain pinned in `rust-toolchain.toml` and the `target/`
directory that `examples/generate.go` reads from.

### Building

```sh
cargo build
just build
```

### Running

Gravity takes a Core Wasm file and produces Go source + an optimized `.wasm`
file:
file. `just run` passes its arguments to gravity:

```sh
cargo run --bin gravity -- --world <world-name> --output <output.go> <input.wasm>
just run --world <world-name> --output <output.go> <input.wasm>
```

## Testing

`just check` runs the same steps as `.github/workflows/ci.yml`, in the same
order: format check, build, unit tests, the Go example tests and the CLI
snapshot tests. Each step is also its own recipe, described below.

### Unit Tests

Run all unit tests (does NOT include snapshot/CLI tests):

```sh
cargo test
just test
```

### Snapshot / CLI Tests

The CLI tests use `trycmd` for snapshot testing and require the example Wasm
files to be built first. They are excluded from the default `cargo test` run.
The CLI tests use `trycmd` for snapshot testing and read the example Wasm
files, so `just test-cli` builds every example first. They are excluded from
`just test`.

```sh
# 1. Build example Wasm files
cargo build -p example-basic --target wasm32-unknown-unknown --release
cargo build -p example-iface-method-returns-string --target wasm32-unknown-unknown --release
cargo build -p example-instructions --target wasm32-unknown-unknown --release
cargo build -p example-regressions --target wasm32-unknown-unknown --release

# 2. Run CLI snapshot tests
cargo test --test cli
just test-cli
```

To update snapshot expectations when output changes intentionally:
To update snapshot expectations when output changes intentionally, then read
the diff before committing it:

```sh
TRYCMD=overwrite cargo test --test cli
just update-snapshots
```

Snapshot files live in `cmd/gravity/tests/cmd/` (`.toml` for config, `.stdout`
and `.stderr` for expected output).

**Important**: The `trycmd` dependency has the `filesystem` feature disabled.
This means `TRYCMD=overwrite` can only update _existing_ `.stdout`/`.stderr`
files — it cannot create new ones. When adding a new snapshot test, you must
manually create the `.stdout` and `.stderr` files first (e.g. by redirecting
gravity's output), then `TRYCMD=overwrite` can keep them in sync afterward.
This means `just update-snapshots` can only update _existing_
`.stdout`/`.stderr` files — it cannot create new ones. `just new-snapshot
<name>` creates all three files for a new example from gravity's current
output; after that, `just update-snapshots` keeps them in sync.

### Example Go Tests

The examples include Go test files that verify the generated bindings work
end-to-end with wazero. After building the example Wasm files and running
Gravity to regenerate bindings:
end-to-end with wazero. `examples/generate.go` has `//go:generate` directives
that build each example's Wasm and run Gravity on it. `just generate` runs
them, and `just test-go` runs them and then the Go tests:

```sh
cd examples
go generate ./...
go test ./...
just test-go
```

Or use `go generate` from the repo root (the `examples/generate.go` file has
`//go:generate` directives that build Wasm and run Gravity).

## Project Structure

```text
Expand Down Expand Up @@ -120,13 +122,18 @@ examples/
iface-method-returns-string/ # Interface method returning a string
instructions/ # Tests various canonical ABI instructions
regressions/ # Regression tests for import codegen edge cases

justfile # Recipes for building, testing and the CI gate
```

**Note on generated files**: `examples/.gitignore` ignores all `*/*.go` files
(except `*/*_test.go`) and all `*/*.wasm` files. This means the generated Go
bindings and Wasm binaries are not committed — only the test files, WIT
definitions, and Rust source are tracked. You must run `go generate ./...` from
the `examples/` directory before the Go tests will compile.
definitions, and Rust source are tracked. The Go tests compile only after
`just generate` has run, which `just test-go` does first. After switching to a
branch without an example, its directory holds only these generated files, and
the `examples/*` workspace glob then fails every cargo command with "failed to
load manifest"; `just clean-examples` removes them.

## Architecture

Expand Down Expand Up @@ -200,12 +207,12 @@ Bump every example's pins with them.
## Style & Conventions

- Use conventional commits: `feat:`, `fix:`, `docs:`, `test:`, `refactor:`, etc.
- Format code before committing with `cargo fmt`
- Lint with `cargo clippy`
- Format code before committing with `just format`
- Lint with `just clippy` (CI does not run clippy)
- When adding new instruction handlers in `func.rs`, add corresponding entries
in the `instructions` example and update snapshot tests
- When changing codegen output, update snapshot `.stdout` files (or use
`TRYCMD=overwrite`)
- When changing codegen output, update snapshot `.stdout` files with
`just update-snapshots`

## Adding a New Example

Expand All @@ -216,9 +223,11 @@ Bump every example's pins with them.
3. Add `//go:generate` directives to `examples/generate.go` (one for
`cargo build`, one for `cargo run --bin gravity`)
4. Write `examples/<name>/<name>_test.go` (this file IS committed)
5. Create snapshot test files in `cmd/gravity/tests/cmd/`:
5. Create the snapshot test files with `just new-snapshot <name>`, or
`just new-snapshot <name> <world>` when the world is not named for the
example. It builds the example and writes three files to
`cmd/gravity/tests/cmd/`:
- `<name>.toml` — trycmd config (`bin.name = "gravity"`, `args = "..."`)
- `<name>.stdout` — capture with
`cargo run --bin gravity -- --world <name> <wasm-path> > <stdout-path>`
- `<name>.stderr` — typically empty (`touch <stderr-path>`)
6. Verify: `cargo test && cargo test --test cli && (cd examples && go generate ./... && go test ./...)`
- `<name>.stdout` — gravity's output for the example
- `<name>.stderr` — empty
6. Verify: `just check`
158 changes: 158 additions & 0 deletions justfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
# Gravity command runner.
# Run `just --list` to see available recipes, or `just <recipe>` to run one.
#
# `just check` runs the same steps as .github/workflows/ci.yml, in the same
# order. Change both together.

# show available recipes (default when no recipe is given)
[private]
default:
@just --list

# rust-toolchain.toml pins the toolchain CI uses; an inherited override would
# replace it.
unexport RUSTUP_TOOLCHAIN

# examples/generate.go builds into and reads from ../target, so a relocated
# target directory makes `go generate` fail with "unable to read file".
unexport CARGO_TARGET_DIR

# styled header: bold cyan ✦, bold white label
h := BOLD + CYAN + "✦" + WHITE + " "
n := NORMAL

# ─── Root commands ───────────────────────────────────────────────────────────────

# run the full CI gate: format check, build, unit tests, Go example tests, CLI
# snapshot tests
[doc('run the full CI gate')]
[group('ci')]
check: lint build test test-go test-cli

# install the toolchain, components and targets pinned in rust-toolchain.toml
[group('setup')]
install:
@echo '{{ h }}install: rustup toolchain{{ n }}'
@rustup toolchain install --no-self-update

# auto-format Rust code and this justfile
[group('format')]
format:
@echo '{{ h }}format: cargo fmt{{ n }}'
@cargo fmt --all
@echo '{{ h }}format: justfile{{ n }}'
@just --fmt

# check formatting of Rust code and this justfile
[group('ci')]
[group('lint')]
lint:
@echo '{{ h }}lint: cargo fmt{{ n }}'
@cargo fmt --all --check
@echo '{{ h }}lint: justfile{{ n }}'
@just --fmt --check

# lint with clippy. Not part of `check`, because CI does not run clippy.
[doc('lint with clippy (not part of the CI gate)')]
[group('lint')]
clippy:
@echo '{{ h }}lint: cargo clippy{{ n }}'
@cargo clippy --workspace --all-targets --locked

# ─── Build and test ──────────────────────────────────────────────────────────────

# build the workspace
[group('build')]
[group('ci')]
build:
@echo '{{ h }}build: cargo build{{ n }}'
@cargo build --locked

# run the Rust unit tests. The CLI snapshot tests are excluded; see `test-cli`.
[doc('run the Rust unit tests')]
[group('ci')]
[group('test')]
test:
@echo '{{ h }}test: cargo test{{ n }}'
@cargo test --locked

# run gravity with the given arguments, e.g.
# `just run --world basic --output basic.go basic.wasm`
[doc('run gravity with the given arguments')]
[group('build')]
[positional-arguments]
run *args:
@cargo run --quiet --locked --bin gravity -- "$@"

# build the example wasm and generate their Go bindings
[group('build')]
generate:
@echo '{{ h }}generate: example wasm and Go bindings{{ n }}'
@go generate ./...

Comment thread
arcjet-rei marked this conversation as resolved.
# run the Go example tests against freshly generated bindings
[group('ci')]
[group('test')]
test-go: generate
@echo '{{ h }}test: go test{{ n }}'
@go test ./...

Comment thread
arcjet-rei marked this conversation as resolved.
# run the trycmd CLI snapshot tests. They read the example wasm that
# `generate` builds.
[doc('run the trycmd CLI snapshot tests')]
[group('ci')]
[group('test')]
test-cli: generate
@echo '{{ h }}test: cargo test --test cli{{ n }}'
@cargo test --locked --test cli

# rewrite the CLI snapshot .stdout and .stderr files from current output. Read
# the diff before committing it. trycmd cannot create a snapshot file, so a new
# test's files must exist first; see AGENTS.md.
[doc('rewrite existing CLI snapshot files from current output')]
[group('test')]
update-snapshots: generate
@echo '{{ h }}test: cargo test --test cli (overwrite){{ n }}'
@TRYCMD=overwrite cargo test --locked --test cli

# create the CLI snapshot files for the example in examples/<name>: a .toml
# that runs gravity on the example's wasm, the .stdout gravity prints for it,
# and an empty .stderr. trycmd cannot create these files, only update them.
# `world` defaults to the example's name. Existing files are left alone.
[doc('create the CLI snapshot files for a new example')]
[group('test')]
[script('bash')]
new-snapshot name world=name:
set -euo pipefail
name={{ quote(name) }}
world={{ quote(world) }}
base="cmd/gravity/tests/cmd/$name"
for ext in toml stdout stderr; do
if [[ -e "$base.$ext" ]]; then
echo "{{ style("error") }}error:{{ n }} $base.$ext already exists" >&2
exit 1
fi
done
wasm="target/wasm32-unknown-unknown/release/example_${name//-/_}.wasm"
echo "{{ h }}build: example-$name (wasm){{ n }}"
cargo build --locked -p "example-$name" --target wasm32-unknown-unknown --release
echo "{{ h }}snapshot: $name{{ n }}"
# Capture first, so a failing gravity run leaves no partial snapshot behind.
stdout="$(mktemp)"
trap 'rm -f "$stdout"' EXIT
cargo run --quiet --locked --bin gravity -- --world "$world" "$wasm" > "$stdout"
# trycmd runs gravity from cmd/gravity, so the .toml's path is relative to it.
printf 'bin.name = "gravity"\nargs = "--world %s ../../%s"\n' "$world" "$wasm" > "$base.toml"
cp "$stdout" "$base.stdout"
: > "$base.stderr"

# ─── Cleanup ─────────────────────────────────────────────────────────────────────

# remove gitignored example output. After switching to a branch without an
# example, its directory holds only generated files, and the examples/*
# workspace glob then fails every cargo command with "failed to load manifest".
[doc('remove gitignored example output (generated Go and wasm)')]
[group('setup')]
clean-examples:
@echo '{{ h }}clean: examples{{ n }}'
@git clean -f -X -- examples/
Loading