Codebase / Codebase development standards

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 that show a change is ready.

Codebase style guide

This document owns implementation style for new and refactored compiler code.

For test strategy and test construction, read docs/src/docs/codebase/style-guide/testing.mtf.

For final technical gates and completion checks, read docs/src/docs/codebase/style-guide/validation.mtf.

Priorities:

  1. Readability
  2. Modularity
  3. Correctness and diagnostics

No user-input panics

Diagnostics

For test assertion policy, see testing.mtf.

Naming

Avoid vague names when they cross more than a few lines or function boundaries:

format! and printing

Use variables directly in format! strings whenever possible.

Imports

Avoid inline imports. They are only acceptable when they are one level deep and help local reasoning.


    // ACCEPTABLE: builtin_type_ids is imported, but kept inline because it's terse and clear.
    let type_id = builtin_type_ids::BOOL

    // BAD: large inline import, even if this is only used once, it's too noisy inline.
    let type_id = crate::compiler_frontend::datatypes::ids::builtin_type_ids::BOOL

    // BAD: inline imports with long paths should be imported at the top, even if only used once.
    fn lookup_constant(
        &self,
    ) -> Option<(
        crate::compiler_frontend::external_packages::ExternalConstantId,
        &crate::compiler_frontend::external_packages::ExternalConstantDef,
    )> {
        ...
    }
    

Avoid aliasing unless it clearly improves readability.

Code style and organisation

For test-file placement, see testing.mtf.

Readable Rust style

Compiler code should optimise for fast human review. Dense code is harder to scan and easier to extend badly.

Prefer code that reads as a sequence of named steps over dense expression chains, clever iterator nesting or large inline matches. The best code in this compiler should make the data flow obvious before the reader understands every detail.

Good compiler code has:

Avoid code that compresses too much logic into one expression. Brevity is less important than clarity.

Vertical spacing

Use vertical spacing to show structure.

Required:

Prefer this:


    let source_file = header.source_file.clone();
    let visibility = import_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)?;
    

Avoid this:


    let source_file = header.source_file.clone();
    let visibility = import_environment.visibility_for(&source_file)?;
    self.resolve_type_aliases(&headers, visibility, string_table)?;
    self.resolve_constant_headers(&headers, visibility, string_table)?;
    

Comments as reading landmarks

Comments are encouraged when they make code faster to understand. They should be terse and focus on wider context rather than re-describing the code. Write comments aimed at programmers reading the codebase for the first time, trying to navigate around and understand how everything flows and is connected together in the wider picture.

Always:

WHAT comments should mostly provide context about what something does from the perspective of its role in the wider module / compiler. They should only help condense complexity or provide wider codebase/non-local context concisely.

Comments should help a reader understand code locally without reading large parts of the codebase first. They should not restate syntax, especially for functions with already descriptive names and signatures.

Be comment-positive, but not noisy.

They should explain:

Use terse comments to mark:

Good comments are grammatical, readable and explain intent:


    // Register same-file declarations before imports 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)?;
    }
    

Refactor moves

When moving code across compiler stages, don't copy the old module shape unchanged. Use the move to correct ownership, names, APIs, comments and data flow.

Moved code must:

Don't preserve a bad API shape just because it already exists.

API breakage

Type ordering

Order types in a file from higher-level abstractions to lower-level supporting types.


    pub struct HirModule { ... }
    pub struct HirBlock { ... }
    pub struct HirNode { ... }
    pub struct HirExpression { ... }
    

Iterators versus loops


    let mut processed_nodes = Vec::new();

    for node in ast_nodes {
        if let Some(ir_node) = convert_to_ir(&node)? {
            if ir_node.is_optimizable() {
                processed_nodes.push(optimize_node(ir_node));
            }
        }
    }
    

Function size

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.

Match readability

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.

Avoid clever Rust

Don't use advanced Rust features just to make code shorter.

Avoid:

Prefer named types and straightforward flow:


    pub(crate) struct ImportResolution {
        pub(crate) target: ResolvedImportTarget,
        pub(crate) local_name: StringId,
        pub(crate) location: SourceLocation,
    }
    

Instead of:


    Result<(ResolvedImportTarget, StringId, SourceLocation), CompilerError>
    

A few more lines are fine when the result is easier to inspect and harder to misuse.

Macros

Warnings and lints

Section banners

Use section banners only in long functions, large files or complex orchestration code where phase boundaries help a reader jump into the process.

Use exactly this format:


    // ------------------------
    //  Resolve template slots
    // ------------------------
    

Rules:

Good uses:

Returning errors

The diagnostic system has two paths:

Rules for new code

Examples


    // 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"
        }
    );
    

Codebase documentation

Codebase pages and Moth template references have different jobs.

Writing rules:

Style checklist

Before considering an implementation slice structurally complete, check:

Testing standards

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/docs/codebase/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.

Default testing preference

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.

Supporting rules

Test ownership

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 tests/cases/

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.

Test location and module layout


        src/compiler_frontend/hir/tests/
    

        src/compiler_tests/
    

        tests/cases/
    

Unit tests

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.

Integration cases

Successful backend intent

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 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"
    

Focused integration selection and inventory

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 
        cargo run --quiet -- tests --list --tag borrows
    

Use the audit mode to validate and inventory the complete canonical suite without compiling cases:


        cargo run --quiet -- tests --audit
    

Audit 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 6 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.

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 output assertions

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:


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

Diagnostics assertions


        [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 = 2
    

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:


        [backends.html]
        mode = "success"
        warnings = "exact"
        warning_codes = ["MOTH-RULE-0022", "MOTH-RULE-0022"]
        rendered_output_contains = ["warning-contract result=one"]
    

Backend matrices and artifact assertions

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"]
    

        golden//
    

HIR and internal-IR assertions

Fixtures and temporary files

Pruning and duplicate coverage

Benchmarks are not correctness tests

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.

Testing checklist

Before considering test coverage complete, check:

Validation gates

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.

Compiler invocation

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 --release
    

If 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 --release
    

Choose the final gate by scope

Slice

Required final gate

Code-bearing or mixed change

just validate

Strictly documentation-only change

moth build docs --release or the equivalent Cargo invocation

The justfile is the executable authority for the full code-bearing gate.

Code-bearing final 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 validate
    

What just validate runs

Gate

Current command

Purpose

Cross-target Clippy

just ci-clippy

Native, Linux x64 and Windows x64 linting with warnings denied

Rust unit tests

cargo test --workspace --quiet -- --format terse

Rust unit and subsystem tests across the complete workspace

Compiler integration tests

cargo run --quiet -- tests --terse

Canonical suite policy followed by end-to-end compiler cases

Documentation check

cargo run --quiet -- check docs --terse

Moth docs-source checking inside the full gate

Benchmark sanity check

cargo run --package xtask --bin xtask -- bench-ci

Complete benchmark preflight followed by bounded non-recording measurements

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. Run the audit command when the JSON inventory under target/test-reports/ is also required:


    cargo run --quiet -- tests --audit
    

Documentation-only release-build gate

A slice is documentation-only only when every modified file is documentation source or generated documentation.

Allowed examples include:

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 --release
    

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

Fast iteration commands

Task

Command

Unit-test iteration

cargo test --workspace --quiet -- --format terse

Integration iteration

moth tests

Clippy iteration

just ci-clippy

Docs-source iteration

moth check docs --terse

Bounded benchmark sanity

just bench-ci

Benchmark-only preflight

just bench-validate

Full CLI benchmark check

just bench-check

Full frontend benchmark check

just bench-frontend-check

Documentation release-build iteration

moth build docs --release

Complete code-bearing gate

just validate

Passing one targeted command does not imply a code-bearing slice passes the complete gate.

Formatting

The ship recipe formats before validation, while validate itself does not.

Generated documentation

Do not edit files under docs/release/** directly.

The documentation release build is:


    moth build docs --release
    

A generated diff must result from documentation source changes, not manual HTML edits.

Manual architecture audits

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:

This full architecture audit is not required for a prose-only documentation change.

Progress-matrix review

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.

Benchmark safety

Failure handling

Code-bearing completion checklist

Before declaring a non-trivial code-bearing slice complete, check:

Documentation-only completion checklist

Before declaring a documentation-only slice complete, check: