Contributing
Brief operational guide for working on formawasm. Strict clippy, typed errors, tests return Result, no panics in production paths.
Where things live
| File | Purpose |
|---|---|
README.md | Project pitch — short, user-facing. |
book.toml + docs/ | This book. Built with mdbook build. |
CHANGELOG.md | Phase-by-phase history. The "Roadmap" section captures what's left. |
Cargo.toml | Single source of lint levels ([lints.*]). |
clippy.toml | Behavioral clippy thresholds and acronym list. |
deny.toml | License + advisory gates (run via make deny). |
Makefile | Local check shortcuts (make check runs the full suite). |
.github/workflows/ci.yml | Same gates as make, in CI. |
Code style
Comments
- Short and to the point — one sentence is usually enough.
- Explain why a decision was made or what architectural constraint it satisfies.
- Never explain what the code already says (rename a variable instead).
- Module-level
//!comments: purpose of the module and its relationship to the rest of the system. One short paragraph max. - Struct/enum doc comments: one line stating what it represents and its role.
- Method/function doc comments: only when the signature does not make the intent obvious, or when there is a non-obvious invariant the caller must respect.
Errors
- Always typed. Use
thiserrorfor crate-level error enums. - Never
unwrap()/expect()/panic!()outside of tests (clippy enforces this). - Tests return
Result<(), TestError>whereTestError = Box<dyn std::error::Error + Send + Sync>. Use?to propagate; never bareassert!/panic!— returnErr(...)instead.
Suppressions
#[allow]is forbidden by lint. Use#[expect(reason = "...")]with a real explanation when you must override a lint.
Async
- Only where genuinely needed. Don't make a function
asyncspeculatively. - Never hold a lock across
.await. Clippy enforcesawait_holding_lock.
Preferred crates
thiserror(v2) — all error handling.strum(withderive) — enum ↔ string conversions.
Microcommit cadence
One commit per microcommit. Before each commit verify:
make check # fmt + clippy + doc + test
Subject line: <scope>: <verb-phrase> (e.g. lower: emit i32 add via wasm-encoder). Body explains why, not what.
The full local suite — including cargo deny and the wasm-opt feature path — runs as:
make ci
That mirrors the GitHub Actions workflow.
Quality gates
The strict lint configuration in Cargo.toml::[lints.*] is the canonical source. Highlights:
- No silent failures:
unwrap_used,expect_used,panic,todo,unimplemented,unreachable,exitare alldeny. Use typedResulteverywhere. - No print macros:
print_stdout,print_stderr,dbg_macroaredeny. Usetracingfor diagnostics. The CLI binary is the one exception, gated by an#[expect(reason = "...")]at the binary's top. - No silent overflow / wrap / truncation / sign loss:
arithmetic_side_effects,integer_division,modulo_arithmetic,cast_possible_truncation,cast_possible_wrap,cast_sign_loss,cast_precision_lossare alldeny. Use checked arithmetic or document the invariant via#[expect(reason = "...")]. - No
await_holding_lock— async work that mixes locks and.awaitis a deadlock waiting to happen. - Match exhaustiveness:
wildcard_enum_match_armisdeny. Every enum match lists every variant explicitly.
The strict-clippy + typed-error setup mirrors the reference Rust setup at ~/projects/smid/smid-ws0.
CI
.github/workflows/ci.yml runs the same gates as make ci:
cargo fmt --checkcargo clippy --all-targets -- -D warningscargo doc --no-depswithRUSTDOCFLAGS="-D warnings"(catches dead intra-doc links and private-item leaks)cargo testcargo deny check(license + advisory gates)cargo test --features wasm-opt(parallel job; the optional post-pass can't bit-rot)
A failing CI gate blocks merge. Don't bypass with --no-verify — fix the underlying issue.
Working in the book
The book's source lives in docs/; build it with:
mdbook build # output goes to ./book/
mdbook serve # local dev server with live reload
./book/ is .gitignored — only the docs/ source is committed.
When you change the book, update docs/SUMMARY.md if you've added or removed pages. mdBook only emits pages listed in SUMMARY.md, so an unlinked file silently disappears from the rendered output.