コンテンツにスキップ

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.

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

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-core knows 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-nbt is the byte codec and nothing more. Litematica regions, schematic palettes, and Bedrock’s .mcstructure quirks belong in cairn-lang-formats.
  • cairn-lang-redstone reuses core’s sensor and actuator placement but owns its own IR layers (Redstone §14.8).
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.

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.

Terminal window
cargo fmt --all -- --check
cargo clippy --workspace --all-targets -- -D warnings
cargo build --workspace --locked
cargo test --workspace --locked
cargo test -p cairn-lang-core --bench lowering
cargo test -p cairn-lang-redstone --bench place_and_route

The 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:

Terminal window
RUSTFLAGS="-D warnings" cargo build --workspace --locked

CI 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:

Terminal window
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --workspace --no-deps --locked --all-features --document-private-items

Two 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:

Terminal window
cargo bench -p cairn-lang-core -p cairn-lang-redstone --bench lowering --bench place_and_route

cairn-lang-wasm builds with wasm-pack and the website expects the resulting pkg/ at website/src/wasm/:

Terminal window
wasm-pack build crates/cairn-lang-wasm --target web --release
  • 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_docs is 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).
  1. Design. Read the relevant spec chapter and restate the slice you are implementing in plain prose.
  2. Acceptance criteria. Write them as bullets, before any code.
  3. Tests. Translate the ACs into #[test] functions.
  4. Implementation. Make them pass.
  5. 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.

Format support lives in cairn-lang-formats. A new file type needs three things:

  1. A reader from bytes to the block-array IR.
  2. A writer from the block-array IR to bytes.
  3. 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.

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).

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.