Developers / Codebase development standards
Good compiler work should be easy to read, easy to test and hard to misinterpret. These references cover implementation style, test design and the validation gates.
All of these must be followed by all contributors to meet the criteria for an acceptable PR.
This document owns implementation style for new and refactored compiler code.
Test strategy and test construction guide.
Final technical gates and completion checks guide.
Priorities:
panic!, todo! or user-data-driven .unwrap().CompilerDiagnostic for normal user-facing source, config, import, syntax, type, rule, borrow and deferred-feature diagnostics.CompilerError only for internal compiler, filesystem, backend, dev-server and tooling infrastructure failures.DiagnosticPayload facts and stable diagnostic codes, not pre-rendered prose.SourceLocation in every user-facing diagnostic and preserve source labels wherever they add useful context.TypeIds and context enums and render through DiagnosticRenderContext, not cloned DataType values or formatted type names.DiagnosticBag for stage-local accumulation and CompilerMessages only at build/render boundaries.For test assertion policy, see testing.mtf.
i and j.build_ast, generate_hir and emit_wasm.function_signature over sig, expression over expr, data_type over ty and a specific context name over ctx or env.format! and printingUse the saying library macro say!() for user-facing stdout that may need colour styling. Use variables directly in format! strings whenever possible.
Use inline imports only when they are one level deep and improve local reasoning. Import long paths at the top, including types used once in signatures.
// ACCEPTABLE: terse and locally clear.
let type_id = builtin_type_ids::BOOL
// BAD: import this long path at the top.
let type_id = crate::compiler_frontend::datatypes::ids::builtin_type_ids::BOOLAvoid aliasing unless it clearly improves readability.
mod.rs should be the module entry point and structural map. It exposes the public surface, shows the module flow and points to the files that contain the implementation.mod.rs focused on orchestration, re-exports and documentation rather than core implementation.mod.rs first. Its documentation should explain structure and data flow and state what the module owns, important exclusions and why the pieces are arranged that way..unwrap() unless it is blatantly safe and tied to an internal invariant..to_owned() and .clone() whenever possible, prefer borrowing when possible and find patterns that avoid copies.For test-file placement, see testing.mtf.
Compiler code should optimise for fast review. Dense code is harder to scan and easier to extend badly.
Prefer code that reads as a sequence of named steps and makes data flow obvious before every detail is understood. The main function for a module or pass should also read as a sequence of named steps.
Use named intermediate values, short single-purpose functions, explicit multi-step control flow, narrow helpers, context/input structs, enums for meaningful states, comments as landmarks and visible spacing. Avoid dense expression chains, clever iterator nesting, large inline matches and logic compressed into one expression. Brevity is less important than clarity.
Use vertical spacing to show structure.
Required:
Prefer this:
let source_file = header.source_file.clone();
let visibility = binding_environment.visibility_for(&source_file)?;
// Resolve type aliases before constants because constant folding may depend on alias-expanded types.
self.resolve_type_aliases(&headers, visibility, string_table)?;
self.resolve_constant_headers(&headers, visibility, string_table)?;Comments must explain non-local intent, not restate the code. Write for programmers reading the codebase for the first time so they can understand local code without first reading distant owners. Stay comment-positive: keep the comments that carry ownership, invariants and failure context, and drop the ones that only narrate syntax.
File-level docs are the primary place for subsystem role, ownership, exclusions and detailed WHAT/WHY context. A reader should learn the architecture from the file header and mod.rs map, then use item comments only where local code is not enough.
Detailed item comments belong on complex functions, stage joins, ordering constraints, invariants, unusual diagnostics and non-obvious data flow. Tiny accessors, constructors, fields, forwarding methods and obvious private helpers need no comment, or at most one short sentence.
WHAT/WHY headings are optional landmarks, not a required template. Use them when they make a non-local contract easier to find. Do not add them to every item.
Keep comments aligned with the current design. Migration history belongs in Git. Do not justify obsolete adapters, compatibility wrappers or deleted owners in comments.
Test-only implementation belongs with tests. Production files must not grow test-only constructors, mutators, semantic variants or convenience lookups just to support fixtures.
Be grammatical and readable. Do not narrate syntax, repeat a descriptive name or paraphrase a clear signature.
// Register same-file declarations before dependencies so aliases cannot shadow local names.
for declaration in same_file_declarations {
visible_names.register_same_file_declaration(declaration)?;
}
// Prelude names are registered as reserved names first, then inserted only if not shadowed.
for symbol in external_package_registry.prelude_symbols() {
visible_names.reserve_prelude_symbol(symbol)?;
}Stage separation is a dependency rule, not only a naming preference. Production code must respect these directions.
compiler_frontend must not depend on build_system, project builders or the project tool's configuration containers to perform local semantic compilation. It receives compiler-owned option and input values instead.build_system/create_project_modules must not assemble interface binding, declaration ordering, AST, public-interface or borrow stages. It builds one compiler input, calls the compiler's module compilation service and handles the returned outcome.docs/compiler-design-overview.md or docs/build-system-design.md. Do not introduce a local exception in a comment.Prefer narrowed Rust visibility for these rules wherever the module tree can express them. Use a repository source check only for dependency directions Rust visibility cannot encode.
Tests may target stage-local APIs from their owning module. Later build, link and backend code may still consume completed HIR, function IDs, link facts and borrow results where the architecture documents that handoff. The rule bans external orchestration of the stages, not legitimate consumption of their final data.
When moving code across compiler stages, do not copy the old module shape unchanged. Use the move to correct ownership, names, APIs, comments and data flow.
Moved code must:
Do not preserve a bad API shape because it already exists.
Order types in a file from higher-level abstractions to lower-level supporting types.
Functions should usually stay under ~200 lines. Longer functions are fine when they still represent one coherent operation, such as a compiler transformation, state machine or tightly coupled sequential process.
Split functions when they mix unrelated responsibilities, are hard to test or no longer match their name.
Large matches should be grouped by meaning.
Use blank lines and short comments to make each group obvious. If a match grows too large, extract branch handling into named helpers.
Prefer:
match resolution {
// Constants create dependency edges.
ConstantReference::SourceConstant { path, source_file } => {
add_constant_dependency(header, path, source_file)?;
}
// Type names are valid to resolve but do not create value dependency edges.
ConstantReference::SourceTypeAlias { .. } => {}
// These are structurally invalid in constant initializers.
ConstantReference::SourceNonConstant { path }
| ConstantReference::ExternalNonConstant { .. } => {
errors.push(non_constant_reference_error(path, location.clone()));
}
// Unknown names may produce better diagnostics during AST expression parsing.
ConstantReference::Unknown => {}
}Avoid long ungrouped matches where every branch looks equally important.
Don't use advanced Rust features just to make code shorter.
Avoid:
Option / Result combinator pipelines for validation logicPrefer named types and straightforward flow:
pub(crate) struct DependencyResolution {
pub(crate) target: ResolvedDependencyTarget,
pub(crate) local_name: StringId,
pub(crate) location: SourceLocation,
}Instead of:
Result<(ResolvedDependencyTarget, StringId, SourceLocation), CompilerError>A few more lines are fine when the result is easier to inspect and harder to misuse.
Keep macro usage minimal. Use small declarative macros only where they clearly reduce repetition and avoid creating procedural macros.
clippy and the default Rust formatter.allow(dead_code) only for clearly identified planned work or test-only code.Use section banners only for real phase boundaries in long functions, large files or complex orchestration such as parsing, lowering, template folding, dependency resolution, diagnostic rendering or build flow.
Use exactly this format:
// ------------------------
// Resolve template slots
// ------------------------The banner has three lines. The title starts with // and uses title case or a clear imperative name. The top and bottom start with // and their dashes extend equally beyond the title on both sides. Do not use banners around small helpers or to hide a file that should be split.
The diagnostic lane rules above apply to every local error boundary.
CompilerDiagnostic constructors in compiler_diagnostic.rs. Do not add CompilerError variants for syntax, type, rule, import, borrow or config failures.return_compiler_error! only for internal compiler bugs or broken invariants.Result boundary carrying CompilerDiagnostic or CompilerError triggers result_large_err, box the payload inside that boundary enum or use a stage-local boxed diagnostic result alias.DiagnosticBag and CompilerMessages owning plain CompilerDiagnostic values at accumulation and render/build boundaries.Option<CompilerDiagnostic> over Result<(), CompilerDiagnostic> for predicate-only validators that report one diagnostic or continue.Result<_, CompilerDiagnostic>.PathBufs.// Typed diagnostic constructor.
return Err(CompilerDiagnostic::invalid_assignment_target(
InvalidAssignmentTargetReason::ImmutableVariable,
variable_name,
location,
));
// Internal bug path.
return_compiler_error!(
"Unsupported AST node type: {:?}",
node_type; {
CompilationStage => "AST Processing",
PrimarySuggestion => "This is a compiler bug - please report it"
}
);This section covers codebase doc comments and small requested documentation edits. For substantial documentation work, follow the task's dedicated writing guidance and the surrounding source.
@page.moth owns website title, introduction, navigation, composition and layout.overview.mtf owns compact routing and contract reference and must not be imported by @page.moth..mtf files own detailed design content, remain useful when read directly and may be imported by website pages. Pages should make subjects easy to explore while compact references reach the contract quickly.For small edits:
' apostrophes and contractions. Reserve hard must and must not wording for deliberate emphasis.This document owns test selection, test structure, assertions, fixture design and test pruning for compiler work.
It does not define the final validation command. For completion gates, read docs/src/developer-docs/style-guide/validation.mtf.
The primary goal is end-to-end language and artifact correctness. Prefer real usage patterns and complete Moth snippets over narrow implementation-shaped tests.
Heading paths use > to name nested sections. Read the selected heading through the next heading of the same or higher level, including nested subsections unless the route narrows further. Read this document in full for test infrastructure, suite policy, broad fixture cleanup or audits.
Task | Read |
|---|---|
Choosing the test owner, level or location |
|
Unit or subsystem-invariant coverage |
|
User-visible language or project behaviour |
|
Runtime output or reactivity behaviour |
|
Threads, shared state, timing or repeated execution | |
Diagnostics or warnings |
|
Backend structure, target parity, Wasm validation or goldens |
|
Manifest policy, suite inventory, filtering or audit behaviour |
|
Adding or changing a Cargo feature, or a feature- or platform-gated test | |
Asserting on source text, or adding an architecture ban | |
Fixtures, temporary paths or test support | |
Removing duplicate or obsolete coverage |
|
Final coverage review | |
Integration tests written in Moth are the default for user-visible language behaviour. They exercise the real compiler path and usually provide the strongest regression coverage.
portable_path_text normalisation helper instead of raw or native path strings. Keep Path or PathBuf values for filesystem operations, converting to portable text only at assertion or display boundaries so tests remain robust across platforms.Behavior | Owner |
|---|---|
Pure data or invariant behavior | Focused unit test near the owning subsystem |
Compiler-stage invariant | Stage-local unit test naming the invariant |
Stage-boundary orchestration | Minimal pipeline/build smoke test |
User-facing language behavior | Integration case under |
Backend artifact behavior | Backend-specific artifact assertions or contractual goldens |
Side-table or hidden compiler fact | Focused unit test if external output cannot expose it |
Cross-backend semantic parity | One integration input with backend-specific assertions |
Each behavior should have one primary owner. Secondary coverage is justified only when it protects a distinct boundary, target, diagnostic lane or internal invariant.
src/compiler_frontend/hir/tests/src/compiler_tests/tests/cases/Retain unit tests for:
Prefer integration tests for user-visible behavior. Once a subsystem is stable, prune superseded unit tests. Rewrite obsolete tests rather than preserving old API shapes. A unit test should name the invariant it protects. Do not use unit coverage as a substitute for an end-to-end case when user behavior changed.
moth tests runs the integration test runner.input/ directory.io.line(...) unless console behavior is itself under test.Every successful HTML and HTML-Wasm backend run applies its universal backend baseline. The baseline protects harness and target invariants. It is not the case's authored semantic contract.
Use success_contract = "acceptance_only" only when that backend has no stronger case-specific semantic, artifact, golden, absence or expected-warning assertion.
[backends.html]
mode = "success"
warnings = "forbid"
success_contract = "acceptance_only"Acceptance-only intent does not disable the baseline. The audit records both baseline_applied = true and acceptance_only = true, with backend_baseline and acceptance_only in assertion_kinds.
Do not combine acceptance-only intent with artifact assertions, golden mode, rendered-output assertions, artifact-absence assertions or an authored expected-warning contract. A whole case whose backends are all acceptance-only uses role = "smoke". A mixed-backend case may use a boundary or backend role when another backend owns a stronger contract.
A successful backend with only the universal baseline is invalid. Typed fixture loading rejects it before policy evaluation, listing, audit, or execution.
For new type-system syntax, include:
Canonical fixture shape:
tests/cases/
├── manifest.toml
└── case_name/
├── input/
│ ├── main.moth
│ └── helper.moth
├── expect.toml
└── golden/
├── html/
└── html_wasm/manifest.toml is the authoritative case order. Every case declares a unique canonical id, unique fixture path and at least one non-empty tag.tests/cases/, including through symlinks.role. Supported roles are primary, boundary, backend, adversarial and smoke.contract. A whole-case acceptance-only smoke may remain contractless because it has no case-specific semantic owner. Do not invent a contract for it.primary case requires a contract, and one contract may have at most one primary case.Manifest entry shape:
[[case]]
id = "borrow_conflict_resolved_by_reordering"
path = "borrow_conflict_resolved_by_reordering"
tags = ["integration", "borrows"]
contract = "language.bindings.alias_final_use_allows_mutable_rebinding"
role = "primary"Use the canonical case ID and retained metadata for local iteration:
cargo run --quiet -- tests --case arithmetic_operator_precedence --backend html
cargo run --quiet -- tests --tag borrows --tag diagnostics --backend html
cargo run --quiet -- tests --contract <contract-id>
cargo run --quiet -- tests --list --tag borrows--case and --contract are exact matches.--tag filters compose as logical AND.--list accepts selection filters and reports canonical ID, selected backend blocks, tags, contract and role without compiling cases.Use the audit mode to validate and inventory the complete canonical suite without compiling cases:
cargo run --quiet -- tests --auditAudit writes its JSON inventory before returning hard-policy failure. Normal listing and execution use the same policy evaluator and reject hard findings before compiling a case. Missing roles, missing contracts on non-smoke cases, primary cases without contracts and duplicate primary owners are hard findings. Contractless whole-case acceptance-only smoke cases are valid.
Audit cannot be combined with filters or --list. It writes the reporting-owned JSON inventory to target/test-reports/integration_suite_inventory.json, including canonical metadata, backend expectation facts, hard-policy violations and primary-less contract-family advisories. The report is review input only; it never deletes, merges or rewrites fixtures.
The schema 8 inventory reports acceptance-only, rendered-output, artifact, golden, absence and expected-warning backend counts, plus the zero baseline-only invariant. It reports exact, ordered and exact-once runtime contracts separately. Per-backend fields keep baseline_applied, acceptance_only, assertion_kinds, golden_present, golden_mode, every rendered-output form, exact warning codes, diagnostic match mode and any contains-mode reason separate so the report matches what ran.
Schema 7 added the weak-contract review fields. smoke_role_cases, warning_ignore_backend_blocks and diagnostic_contains_backend_blocks count the three legal weak contracts, and weak_contract_review_backend_blocks counts blocks carrying at least one of them. Every backend block lists its own weak_contract_reviews. These are review fields, not policy: a smoke case, an ignored warning contract and a contains-mode failure all remain valid. The canonical suite currently declares none of them, so any non-zero count is a new weaker contract to review rather than an inherited one to explain.
Schema 8 added report identity. Every report carries run — a run id, the command, the host OS and architecture, the features the binary was built with, the runner thread count and whether the run finished — and the inventory replaced repository_commit with repository_revision, which reports a discovered commit, not_a_repository, or unknown with the reason discovery failed. A failed discovery and a genuine absence are different facts, and a report that names neither its command nor its build cannot be told apart from a stale one.
Reports are written to a sibling temporary file and renamed into place, so a reader never sees a partial file. Before the work begins, each report is replaced by one whose run says `completed: false`; an interrupted run therefore leaves a report that says it did not finish rather than the previous run's passing output.
Hard policy has one suite-policy owner. Missing roles, missing non-smoke contracts, duplicate primary contracts, a primary without a contract and a whole-case acceptance-only fixture without role = "smoke" are hard findings. Normal list and execution modes reject hard findings before selection or compilation. Audit still writes the complete JSON report, then returns failure. Primary-less contract-family advisories remain non-fatal.
Runtime assertions use one Node harness. It records console output and HTML fragment inserts in one event array when each event happens, then joins their text with newlines in that same order. Channel views are derived from the event array. They never reconstruct chronology after execution.
The harness runs only when a backend declares a rendered-output assertion. It executes the emitted page scripts and waits for one documented microtask tick so scheduled reactive updates can flush. Do not make extra scheduler turns or incidental microtask counts contractual.
Choose the narrowest field that owns the behaviour:
rendered_output_exact protects the complete combined output. It is mutually exclusive with the other rendered-output fields. Exact comparison normalizes line endings only and preserves every other whitespace difference.rendered_output_contains_in_order protects the relative order of two or more fragments.rendered_output_contains_exactly_once protects activation, mounting and helper output from duplication.rendered_output_contains and rendered_output_not_contains protect required and forbidden fragments when complete output isn't the contract.[backends.html]
mode = "success"
warnings = "forbid"
rendered_output_contains = ["active-root"]
rendered_output_not_contains = ["imported-root"]
rendered_output_contains_in_order = ["before-loop", "after-loop"]
rendered_output_contains_exactly_once = ["active-root", "mounted"]Use exact output for small deterministic behaviours such as loop or map operation results. Use ordered and exact-once fragments when unrelated output may coexist. Prefer these runtime contracts over generated JavaScript text or a whole-page golden when execution is the real owner.
An exact mismatch is reported escaped, post-normalization, with the byte offset of the first difference, so a whitespace-only difference does not print as two identical lines.
The harness parses the emitted page for a supported script shape and rejects everything else rather than guessing:
type="module" is rejected as unsupported. The harness materializes no emitted glue, provider or runtime module and no import map, so it cannot run the module graph the HTML backend emits when a bundle import preamble is present. Executing it as a classic script would report a failure that did not happensrc, an unknown type, nomodule, async, and a malformed or unterminated tag are each rejected by nameA harness problem is never reported as a case failure. RenderHarnessError names which boundary broke: Artifact, Workspace, Spawn, Timeout, ExitStatus, OutputDecoding, OutputProtocol, ScriptShape or Cleanup. Execution is bounded by a 30-second deadline with kill-and-reap, stdout and stderr are drained on their own threads under a 4 MiB bound, and a stderr decode failure never replaces the boundary that actually failed.
A golden file's extension decides the artifact kind it expects, and a cross-kind match is rejected before content is read. Identical bytes emitted as a different artifact kind do not satisfy a golden. Text goldens require strict UTF-8; invalid UTF-8 is a harness failure, not a rewrite.
A test that waits for a thread to reach a state must observe that state, not guess how long reaching it takes.
Stateful owners — the timing collector, the frontend counter stores, output writes, current-directory guards and the Node harness — only misbehave under a thread schedule or a repeat that one default-parallelism run never produces. That schedule has an owned command:
just stressIt repeats the unit and integration suites at one thread, default parallelism and sixteen threads, and keeps running after a failing lane so the whole matrix is visible.
diagnostic_codes.diagnostic_codes is an exact unordered multiset by default. Repeat a code when the diagnostic must occur more than once.ErrorType.[backends.html]
mode = "failure"
warnings = "forbid"
diagnostic_codes = ["MOTH-RULE-0033", "MOTH-RULE-0033"]Use diagnostic_assertions when a broad code needs a compiler-owned reason or when source remapping is part of the contract. Each table selects one diagnostic by stable code and one-based occurrence. A unique code may omit occurrence. Repeat the code in diagnostic_codes to preserve its exact multiplicity.
[[backends.html.diagnostic_assertions]]
code = "MOTH-RULE-0057"
reason = "invalid_generic_instantiation.recursive_function_instantiation"
path = "input/@page.moth"
line = 3
count = 1
[[backends.html.diagnostic_assertions.secondary_labels]]
occurrence = 1
path = "input/helpers.moth"
line = 2message_contains only when rendered wording or label prose is itself contractual.Use diagnostic_match = "contains" only when intentional independent recovery or cascades make additional diagnostics part of the accepted current behaviour. Every contains-mode block needs a substantive diagnostic_match_reason. The suite audit reports the reason and treats a missing or blank reason as a hard policy finding.
diagnostic_match = "contains"
diagnostic_match_reason = "Independent recovery may emit additional diagnostics from another source unit."Warnings use a separate backend-local contract:
warnings = "forbid" rejects every warning and accepts no warning_codes field.warnings = "ignore" deliberately makes warnings non-contractual and accepts no warning_codes field.warnings = "exact" requires warning_codes as an exact unordered multiset. Repeat duplicate codes to preserve their required count.[backends.html]
mode = "success"
warnings = "exact"
warning_codes = ["MOTH-RULE-0022", "MOTH-RULE-0022"]
rendered_output_contains = ["warning-contract result=one"]Use one expect.toml per case with backend-specific assertion blocks.
entry = "."
[backends.html]
mode = "success"
warnings = "forbid"
artifacts_must_not_exist = ["unexpected.html"]
[backends.html_wasm]
mode = "success"
warnings = "forbid"
[[backends.html_wasm.artifact_assertions]]
path = "page.wasm"
kind = "wasm"
validate_wasm = true
must_export = ["memory", "moth_str_ptr", "moth_str_len", "moth_release"]entry = "." is the one supported current-directory sentinel and selects the case's input/ directory. Every other entry is a relative contained path with no absolute, platform-prefix, parent or current-directory component. Its canonical path must remain inside the owning input/ directory, including through symlinks.golden/<backend>/artifacts_must_not_exist for success cases whose contract requires an exact normalized output path to be absent. Paths reported as NotBuilt are ignored because they were never emitted.golden_mode without at least one discovered file is invalid. When files exist, strict is the default if no mode is authored. When no files exist, audit reports golden_present = false and golden_mode = null.A feature-gated test that no command runs is not a test. Every declared Cargo feature belongs to a lane. Standard lanes run in the feature matrix. Deliberately expensive developer systems may use an opt-in lane with its own named command:
just test-feature-matrixLane | Features | Only this lane covers |
|---|---|---|
| none | the shipped configuration and every |
| | the timing collector, boundary identities and command/build timing tests |
| | AST substage timings and the detailed-only summary shape |
| | counter-only builds, where counters record without a timing collector |
| | collector-backed counters and the counter summary carried by a timing session |
| | the deferred-feature diagnostics for |
| every | the developer stage-dump branches, which no other lane compiles |
| none | the benchmark, profiling and process-runner tests in the |
| | the reference-solver developer gate, owned by |
| | the measured generated differential campaign, owned by |
Lanes are package-scoped, and that is not a stylistic choice. Cargo unifies features across one resolve graph, and xtask depends on moth with features = timers, so cargo test --workspace always builds the compiler with timers enabled. It can never run the default configuration, and --features benchmark_counters on a workspace command is really timers plus counters. Only cargo test -p moth with an explicit feature list configures the crate the way a lane claims.
Feature coverage is checked mechanically, without running a lane:
just feature-lane-checkThe check reads Cargo.toml and every cfg attribute in src and xtask/src, then fails when a declared feature has no lane, when a lane names a feature its package does not declare, or when a cfg names a feature that does not exist. A misspelled feature name in a cfg is a test that can never compile and never reports, which is why the check reads the tree instead of a list in prose. It writes target/test-reports/feature_lane_coverage.json, which names, for every feature, its standard lanes, opt-in lanes and the files whose cfg attributes mention it. That report is a coverage map and states no lane outcome, because the check that writes it runs no lane. Running the standard lanes is just test-feature-matrix, which writes their outcomes to target/test-reports/feature_matrix_results.json. The opt-in boracle and boracle-campaign lanes run only through their named just boracle and just boracle-campaign commands, not through the standard lane set.
Adding a Cargo feature means adding it to a standard or opt-in lane in the same change. The gate fails otherwise.
Platform-gated tests are owned by the platform that has the API:
Gate | Runs on | Owns |
|---|---|---|
| Linux and macOS | symlink identity and retargeting, non-regular nodes, unreadable paths and non-UTF-8 names |
| Windows | extended-length path prefixes, Windows canonicalisation and hard-link inspection |
| all three | the portable policy shared by both families, asserted on each |
| Linux | non-UTF-8 filesystem identity, which macOS rejects at the filesystem boundary |
CI runs every gate on Linux, macOS and Windows, so a platform-gated owner executes wherever its API exists. A platform-specific test may be absent only when the underlying API does not exist there; an equivalent portable policy test should still run everywhere. Windows results are currently non-blocking — validation.mtf owns that status and the conditions that end it.
A test that reads source text and asserts on it proves what the text says, not what the code does. An alias, a reformat or an equivalent reimplementation all pass it.
just source-audit, which walks the tree once and reports typed findings. validation.mtf > Source audit owns it.tempfile::tempdir() for temporary directories and files.dev/ or release/ outputs from test or benchmark projects.Benchmark fixtures are performance evidence, not correctness coverage.
Do not use benchmark coverage as a substitute for unit or integration tests. Negative diagnostics belong in tests/cases, not benchmark fixtures.
Use bench-check to detect broad performance regressions after correctness has already been established by ordinary tests.
Before considering test coverage complete, check:
This document defines the technical gates a completed compiler or documentation slice must pass.
The required final gate depends on whether the slice changes implementation or is strictly documentation-only.
Start every completion decision with Choose the final gate by scope. Heading paths use > to name nested sections. Read the selected heading through the next heading of the same or higher level, including nested subsections unless the route narrows further. Read this document in full when changing validation infrastructure or auditing the complete gate policy.
Task | Read |
|---|---|
Select the required final gate | |
Choose direct compiler or Cargo invocation | |
Complete or review code-bearing work |
|
Complete or review documentation-only work |
|
Add or change a Cargo feature, or a feature-gated test | |
Add or change a broad-source architecture ban | |
Read, change or interpret a CI gate |
|
Pick a targeted iteration command | |
Performance or benchmark validation |
|
Review a failed gate or a validation claim |
|
Use an up-to-date release build of the compiler when one is available. The moth command in PATH is the usual direct form:
moth check docs --terse
moth build docs --releaseIf a suitable release build is not available, run the same operations through Cargo from the repository root:
cargo run --quiet -- check docs --terse
cargo run --quiet -- build docs --releaseSlice | Required final gate |
|---|---|
Code-bearing or mixed change | |
Strictly documentation-only change | |
The justfile is the executable authority for the full code-bearing gate.
A slice is code-bearing if it changes Rust, compiler or build-system sources, libraries, tests, benchmarks, manifests, scripts, configuration or any other non-documentation implementation file.
Before declaring a non-trivial code-bearing slice complete, run:
just validateGate | Current command | Purpose |
|---|---|---|
Clippy | | Native-host linting of every workspace target under the explicit standard feature set, warnings denied |
Feature lane coverage | | Proves every declared Cargo feature has an executing lane, and that no |
Rust unit tests | | Rust unit and subsystem tests across the complete workspace, in the workspace-unified feature configuration |
Compiler integration tests | | Canonical suite policy followed by end-to-end compiler cases |
Documentation check | | Moth docs-source checking inside the full gate |
Benchmark sanity check | | Complete benchmark preflight followed by bounded non-recording measurements |
Complexity budgets | | Fits the growth exponent of every declared scaling series against its declared input sizes and fails when one exceeds its budget |
Source audit | | The one broad-source architecture audit: |
Timer erasure gate | | Builds a no-timer release binary and proves that no timer-only marker, and no name from the schema-owned metric inventory, survives into its bytes |
ci-clippy-native lints the native host only, across every workspace target and the explicit standard feature set. It deliberately excludes the opt-in boracle feature.clippy on Linux, macOS and Windows.xtask packages.xtask depends on moth with features = timers, and Cargo unifies features across one resolve graph, so cargo test --workspace always builds the compiler with timers enabled. The default configuration and each standard feature configuration are covered by just test-feature-matrix, not by just validate.bench-ci preflights every benchmark manifest case once, then measures only the quick CLI and frontend subsets with three iterations.bench-ci does not update local history or tracked benchmark summaries.bench-scaling answers the question history cannot. Every other benchmark mode compares a case against its own recorded past, so it detects change; a cost that has been superlinear since the day it was written never changes and never reports. The scaling lane measures the same metric at several declared input sizes and holds the fitted exponent to a budget. It writes no history either. benchmarks/README.md > Scaling Series owns the authoring rules.The integration command loads the complete canonical suite and applies the shared hard-policy evaluator before selection or compilation. This makes missing ownership, duplicate primaries and other hard suite-policy findings part of the normal validation gate.
The campaign that hardened this suite is owned by one command:
just test-honesty-auditIt writes the canonical inventory to target/test-reports/test_honesty_inventory.json, and it is the single owner of what that file says. The inventory has four parts:
temp_dir helper, a /tmp literal handed to a call that touches the filesystem, a discarded current-directory or environment restoration, a discarded thread join, an ignore attribute with no reason, and a report written straight to its final path. Every hit fails the audit.is_err, contains and any are review categories on purpose: a gate that failed on each of them would be turned off within a week.docs/roadmap/evidence/honesty_ledger.json. Every entry must carry a severity, a status, a description, a disposition and an owning phase; a duplicate code, a missing decision or an open hard finding fails the audit.The tracked durable copy under docs/roadmap/evidence/ is refreshed by the same owner, never by hand:
just test-honesty-evidenceEvery machine-readable report is written to a sibling temporary file and renamed into place, so a reader never observes a partial file and a failed write leaves the previous report intact.
Every report also carries the identity of the run that produced it: an id unique within the process without depending on the wall clock, the command, the OS and architecture, the features the linked compiler was built with, a thread count where the command owns one, and whether the run finished.
Completion state is what separates the reports, and the distinction is real:
test_honesty_inventory.json, source_audit.json and integration_suite_inventory.json are replaced by a completed: false report before their owned work starts. A run interrupted partway leaves a report that says so rather than a previous successful one that looks current.feature_matrix_results.json starts with every standard lane pending and is rewritten as each standard lane resolves, so an interrupted matrix reports the lanes it measured and the lanes it never reached. Opt-in lanes are owned by their named commands and do not appear as unmeasured matrix outcomes.feature_lane_coverage.json is a coverage map, not an outcome table. It answers which lane covers which declared feature, which just feature-lane-check establishes without running a single lane, so it is complete the moment it is written and never states a lane result. Lane outcomes live in feature_matrix_results.json.An architecture ban whose scope is "no file anywhere may contain this" is not a behaviour claim, and a unit test that reads one file's text and asserts on it is not behaviour evidence. Those bans have one owner:
just source-auditIt walks src and xtask/src once, applies every broad-source rule, and reports typed findings with the file each was found in. Only the audit's own implementation files are exempt, because they necessarily contain the fragments the rules search for; their rules are proved against fixture text instead. A source file that cannot be read is a finding, not a file the audit skips.
Add a broad-source ban here rather than as a test. A ban that lives in a test reads one file, calls that evidence, and stops applying the moment the code moves.
just validate runs one standard feature configuration. The standard lanes run all standard configurations:
just test-feature-matrixEight package-scoped standard lanes cover the default configuration, timers, detailed_timers, benchmark_counters, timers with benchmark_counters, the scoped-block feature lane, every show_* feature and the xtask package. testing.mtf > Feature and platform lanes owns the lane table and what each lane uniquely covers.
The boracle feature is an opt-in developer lane. just boracle owns its formatting check, feature-enabled Clippy and smoke test. The boracle-campaign lane, using boracle_campaign, is a separate opt-in developer lane owned by just boracle-campaign. It was measured at roughly 61 seconds against roughly 8 seconds for the default lane, so it cannot belong in the default lane. Both lanes are intentionally excluded from just validate, just test-feature-matrix and ordinary CI gates.
Lanes use cargo test -p <package> rather than --workspace because Cargo unifies features across one resolve graph. xtask requires moth with timers, so no workspace-wide command can run the default configuration.
just feature-lane-check proves the mapping without running a lane, and is part of just validate. The report distinguishes standard and opt-in lane owners. Adding a Cargo feature means adding it to a standard or opt-in lane in the same change; the check fails otherwise.
just validate is the local gate and is deliberately fail-fast: the first failure stops the run.
CI does not work that way. Each validation family is its own job, so one failure never stops another from reporting:
Gate | Command |
|---|---|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
Each gate runs on Linux, macOS and Windows. The workflow result is still failed when any blocking gate fails. The integration, feature-matrix and honesty-audit gates upload target/test-reports/ whether they passed or failed, because a failed run's report is the evidence a reviewer needs most.
unit-tests and feature-matrix overlap on purpose. unit-tests runs the workspace-unified command a developer runs locally; feature-matrix runs the configurations that command cannot reach.
The release workflow keeps the single fail-fast just validate. A tag build only needs to know whether the revision is releasable, and the split gates have already reported on every push to the default branch.
Linux and macOS gates are blocking. Windows gates are non-blocking: a Windows failure is reported but does not fail the workflow or hold back deployment.
Windows becomes blocking when both hold:
windows-latest in one run of the default branchThe change is then removing continue-on-error from the gate job. Until that happens, a Windows result is evidence to act on, not a gate that can be ignored: a Windows-only failure belongs in the ledger like any other.
A platform-specific test may be absent only when the underlying API does not exist on that platform. An equivalent portable policy test should still run everywhere. `testing.mtf > Feature and platform lanes` owns which gate owns which platform behaviour.
A slice is documentation-only only when every modified file is documentation source or generated documentation.
Allowed examples include:
docs/**README.md, CONTRIBUTING.md or AGENTS.mddocs/release/**A documentation-only slice must not change Rust, tests, libraries, build configuration, manifests, scripts, benchmark code or fixtures, compiler configuration or other executable project sources.
For a strictly documentation-only slice, run one documentation release build using a current compiler.
The direct release-binary form is:
moth build docs --releaseIf no suitable release build is available, use cargo run --quiet -- build docs --release instead.
Then:
Do not additionally run just validate, Clippy, unit tests, integration tests or benchmark checks for a documentation-only slice. A Cargo invocation of the compiler is an alternative way to run the same documentation gate, not an extra gate.
If any non-documentation file changed, the documentation-only exception does not apply. Use the full code-bearing gate.
Task | Command |
|---|---|
Unit-test iteration | |
Integration iteration | |
Clippy iteration | |
Feature lane coverage | |
Every standard feature lane | |
Boracle feature lane | |
Boracle campaign lane | |
Broad-source architecture audit | |
Test honesty audit and inventory | |
Durable honesty evidence refresh | |
Docs-source iteration | |
Bounded benchmark sanity | |
Benchmark-only preflight | |
Full CLI benchmark check | |
Full frontend benchmark check | |
Documentation release-build iteration | |
Complete code-bearing gate | |
Passing one targeted command does not imply a code-bearing slice passes the complete gate.
cargo fmt when Rust files changed.just validate does not currently format source automatically.The ship recipe formats before validation, while validate itself does not.
Do not edit files under docs/release/** directly.
The documentation release build is:
moth build docs --releaseA generated diff must result from documentation source changes, not manual HTML edits.
When a code-bearing slice changes stage ownership, frontend boundaries, HIR, diagnostics, types or backend handoff, perform a manual architecture audit in addition to the automated gate.
Audit that:
TypeEnvironment and TypeIdCompilerDiagnosticCompilerErrorThis full architecture audit is not required for a prose-only documentation change.
Review the progress matrix whenever behaviour, support, rejection, backend coverage or test coverage changes.
Update it when current status changed. Do not make a meaningless edit for a pure refactor or prose-only correction.
just validate uses non-recording bench-ci.bench-ci validates every benchmark case before it measures the quick subset. It replaces the full ten-iteration CLI and frontend checks inside the normal validation gate.just bench-check and just bench-frontend-check for deliberate full-suite performance evidence. Both commands remain non-recording.Before declaring a non-trivial code-bearing slice complete, check:
style-guide.mtf.testing.mtf.just validate passes.Before declaring a documentation-only slice complete, check: