Developer Guide
This guide covers the Rust workspace and the day-to-day contributor flow. If you are working on the language spec rather than the compiler, start at the Specification and CONTRIBUTING.md.
Cairn is at the design stage. The spec is the source of truth and the Rust skeleton implements it chapter by chapter. Several crates are still empty.
Workspace layout
Section titled “Workspace layout”| Path | Contents |
|---|---|
Cargo.toml |
Workspace root: shared lints, release profile, MSRV. |
rust-toolchain.toml |
The exact Rust version CI runs, with rustfmt and clippy. |
rustfmt.toml |
Edition 2024, max_width = 100. |
crates/ |
The Rust workspace (below). |
editors/vscode/ |
VS Code extension. |
examples/ |
Worked .crn examples. |
website/ |
This site: Astro + Starlight, including the specification. |
Each crate maps back to the spec chapter it implements:
| Crate | Role | Kind |
|---|---|---|
cairn-lang-core |
Parser, IR, resolver, lint | lib |
cairn-lang-cli |
The cairn binary |
bin |
cairn-lang-nbt |
Java / Bedrock NBT codec | lib |
cairn-lang-formats |
.nbt / .litematic / .schem / .mcstructure |
lib |
cairn-lang-redstone |
Logic synthesis, place-and-route, tick simulation | lib |
cairn-lang-lsp |
Language Server Protocol | lib + bin |
cairn-lang-wasm |
WebAssembly bindings | cdylib + rlib |
cairn-lang-tree-sitter |
Grammar for editor highlighting | grammar |
Dependency rules
Section titled “Dependency rules”cairn-lang-core sits at the root. cairn-lang-cli, cairn-lang-lsp, and cairn-lang-wasm are
leaf integrations that nothing depends on. cairn-lang-formats is the only crate that pulls in
cairn-lang-nbt.
cairn-lang-coreknows nothing about NBT, file formats, redstone simulation, or editor protocols. The block-array IR is the universal pivot (Architecture); everything beyond it lives in a sibling crate.cairn-lang-nbtis the byte codec and nothing more. Litematica regions, schematic palettes, and Bedrock’s.mcstructurequirks belong incairn-lang-formats.cairn-lang-redstonereuses core’s sensor and actuator placement but owns its own IR layers (Redstone §14.8).
Toolchain
Section titled “Toolchain”| Tool | Pinned by | Notes |
|---|---|---|
| Rust | rust-toolchain.toml |
An exact version, not a channel. With rustfmt and clippy. |
| Edition 2024, MSRV | Cargo.toml |
Workspace package metadata, inherited by every crate. |
| Formatting | rustfmt.toml |
max_width = 100, Unix line endings. |
| Lints | [workspace.lints] in Cargo.toml |
unsafe_code = deny, missing_docs = warn, clippy::all + clippy::pedantic. |
The Rust version is exact because CI treats every clippy finding as fatal. On a channel, a Rust
release turns every open branch red on its own — the finding lands on a file the branch never
touched. The pin decides when new lints arrive rather than whether: bumping it is its own pull
request, carrying whatever the new release found. It is not the MSRV; rust-version is the floor a
consumer needs, and raising the pin does not raise it. The number itself lives only in
Cargo.toml: CI’s MSRV job reads it back out with cargo metadata and checks the workspace at
that compiler, so a change reaching for a newly stabilised API goes red there rather than at a
consumer’s.
Every crate inherits these with [lints] workspace = true and writes no [lints.*] table of its
own. Inheritance is opt-in per crate and all-or-nothing: a crate without that line receives none of
the workspace lints, so a lint added to the workspace later would silently skip it.
unsafe_code is denied workspace-wide and lifted inside one module: ffi in the tree-sitter
crate’s Rust binding, which is the only way to reach the generated C parser. The level is deny
rather than forbid because forbid refuses that module’s #![expect(unsafe_code)] too. The
unsafe_code_is_confined test in cairn-lang-core makes up the difference, over every crate
cargo metadata reports as a workspace member: it fails if the workspace level is anything but
deny, if a crate does not inherit the workspace lints, or if an allow, expect, warn or
cfg_attr attribute names unsafe_code outside that module. Its module doc is the full statement
of the policy and of what the test cannot see. If another use case ever needs unsafe, it goes
through a focused PR that lifts the lint on a single module with documented invariants and adds
that file and module to the test’s ALLOWED list, never #[allow] at a call site.
Build, test, lint
Section titled “Build, test, lint”CI runs these on Linux, macOS, and Windows, checks the workspace once more at the declared MSRV, and builds the API docs on Linux (below). Run them before opening a PR.
cargo fmt --all -- --checkcargo clippy --workspace --all-targets -- -D warningscargo build --workspace --lockedcargo test --workspace --lockedcargo test -p cairn-lang-core --bench loweringcargo test -p cairn-lang-redstone --bench place_and_routeThe last two run each bench once as a test, untimed; --workspace does not select bench
targets.
CI sets RUSTFLAGS=-D warnings, so any new warning fails the build. To match it locally:
RUSTFLAGS="-D warnings" cargo build --workspace --lockedCI also builds the API docs on Linux, twice, with rustdoc’s warnings fatal, since clippy does not see a doc link to a private item or to a path that no longer resolves. The first run is the public surface and the only one that flags a public doc linking to a private item; the second also covers the docs only a contributor reads:
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked --all-featuresRUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked --all-features --document-private-itemsTwo benches time the passes a build spends its time in: lowering in cairn-lang-core and
place_and_route in cairn-lang-redstone. They call the passes in-process, since timing the CLI
over an example would mostly time process startup, and they generate their sources, since an
example-sized one goes through a pass faster than the timer can tell a change apart. They take
their settings from the release profile except panic, which Cargo builds unwinding for benches as
it does for tests. CI runs them once as tests, above, but never times them. They exist to measure
a change to [profile.release], opt-level above all, before it is made.
CONTRIBUTING.md walks through
comparing two profiles with them from the first release that includes the benches; until then that
section is only on canary. To run both:
cargo bench -p cairn-lang-core -p cairn-lang-redstone --bench lowering --bench place_and_routecairn-lang-wasm builds with wasm-pack and the website
expects the resulting pkg/ at website/src/wasm/:
wasm-pack build crates/cairn-lang-wasm --target web --releaseConventions for Rust code
Section titled “Conventions for Rust code”- The spec is the source of truth. When spec and implementation disagree, fix the implementation. If the spec is wrong, send a spec PR first.
- No linter-ignore directives.
#[allow(clippy::…)],#[allow(dead_code)], and friends are not allowed. If a lint fires, the design is the bug. - Lift the spec’s terms verbatim.
IntentState,ResolvedState,MatSlot,CanonicalToken,BlockArrayIr. Do not invent parallel vocabulary. missing_docsis a warning everywhere. Every public item gets a///line, and every crate a//!block.- No Minecraft target constants. The
(edition, version)pair is a CLI parameter and must never appear in the language semantics (Compilation Model §4.2). - Errors carry the self-correction triple: what is wrong / valid candidates / suggested fix (Lint).
TDD discipline
Section titled “TDD discipline”- Design. Read the relevant spec chapter and restate the slice you are implementing in plain prose.
- Acceptance criteria. Write them as bullets, before any code.
- Tests. Translate the ACs into
#[test]functions. - Implementation. Make them pass.
- Iterate. Keep tests and implementation in lockstep until green.
The spec is compact enough that an AC list almost always fits in a few lines. There is no value in skipping ahead.
Adding a format backend
Section titled “Adding a format backend”Format support lives in cairn-lang-formats. A new file type needs three things:
- A reader from bytes to the block-array IR.
- A writer from the block-array IR to bytes.
- An
(edition, version)provenance stamp on import (Ecosystem Interop §12.4).
If you find yourself reaching into cairn-lang-core to add format-specific fields, the block-array
IR is leaking format concerns. Discuss before merging.
Adding redstone primitives
Section titled “Adding redstone primitives”The v1 vocabulary is closed (Redstone §14.1):
combinational gates plus latch, pulse, delay, edge_rising, edge_falling, and counter.
Adding to it is a spec change. Open a spec PR with:
- the new primitive’s signal-graph semantics,
- whether it is combinational or sequential,
- the per-edition cell library entry it lowers to,
- the truth-table, latency, and temporal assertions it must satisfy in the headless simulator (Evaluation Framework §13.4).
Where to ask
Section titled “Where to ask”Open an issue against the relevant spec chapter. Implementation-only questions can reference the crate README. Design questions about vocabulary, IR shape, or error message wording belong against the spec.