Tenets

Design principles governing sage's architecture. These guide all new code across every module.

Code organization

Methods live on the types they process. Type-checking, name resolution, and lowering are inherent methods on CST and symbol types. Not standalone functions, not visitor traits.

#![allow(unused)]
fn main() {
TypeCst::check(cx)
PathCst::resolve(cx, ns)
ExprCst::check(bx)
LocalFnSym::sig(db)
LocalStructSym::fields(db)
}

Master modules with submodules. Each concern gets a top-level module subdivided by item kind or sub-concern. Avoid monolithic files. One struct or closely-related cluster per file.

  • cst/ — per-item CST data: fns.rs, structs.rs, ty.rs, paths.rs, generics.rs, expr.rs
  • local_syms/ — per-kind symbol definitions: fns.rs, structs.rs, enums.rs, mods.rs
  • check/infer/ — inference sub-concerns: egraph.rs, skeleton.rs, version.rs, runtime.rs, bound.rs

Shared infra lives in pass-named modules. Contexts, helpers, and traits shared across item kinds live in a module named after the pass:

  • check/sig.rs — Check (signature-lowering context)
  • check/infer_ctx.rs — InferCtx and Scope (body-checking context)
  • check/resolve/ — Resolver, Namespace, and lexical ribs

Item-specific logic imports from these; it does not redefine its own plumbing.

Query design

Single-keyed queries. sym.sig(db), sym.body(db) — the symbol knows its scope, the query derives everything else. No ambient parameters threaded from callers.

#![allow(unused)]
fn main() {
#[salsa::tracked]
pub fn sig(self, db: &'db dyn crate::Db) -> Stashed<Binder<'db, FnSig<'db>>> { ... }
}

Semantic interfaces are narrow and lazy. sig() is the primary cross-item boundary: it exposes the owner binder, parameters, return type, and predicates needed to use a definition. Language-required field, member, and associated-value interfaces may be separate keyed queries so a consumer does not load facts it did not request. Bodies and checking temporaries are not interfaces.

Interfaces are the cross-item semantic boundary. A symbol's checked signature is its primary interface; field, member, and other language-required interfaces may be separate keyed products. Bodies and checking temporaries are not interfaces. A caller may depend on a callee's interface, but never on its body. See Cross-item dependencies stop at semantic interfaces.

Required verification: Query traces for signatures and callers require only the relevant interface products and forbid callee-body and unrelated-detail reads; interface-preserving edits do not propagate past the corresponding interface boundary.

Generic parameters are minted exactly once. The sig() query mints GenericParam symbols via cst.generics.check(db, cx, parent) and stores them in a Binder. All other queries (fields, body) open that binder and bring the same param symbols into scope via ribs.add_generic_params. No re-minting, no identity confusion.

A declaration has one set of generic-parameter identities. The signature boundary creates the symbols once; every detail query opens the binder and reuses those identities rather than recreating equivalent-looking parameters. See Symbols form the uniform semantic identity family.

Required verification: Identity tests compare generic parameters reached through signatures, fields, predicates, and bodies, including after a warm query and an unrelated edit.

Sequential layering inside a query. body() calls sig() -> opens binder -> resolves names -> runs inference. Each step builds on the prior. From outside: one query, one result. Intermediates are not separately queryable.

Data flow: the two-stash pattern

CST is read-only input; output goes to a fresh stash. The context bridges src: &Stash (the CST) and target_stash: Stash (output types or resolved exprs). At the end, cx.finish(root) wraps target_stash into Stashed<T>.

#![allow(unused)]
fn main() {
pub struct Check<'a, 'db> {
    pub resolver: Resolver<'db>,
    pub src: &'a Stash,
    pub target_stash: Stash,
}
}

Purpose-specific contexts. Check lowers signatures into its target stash. InferCtx owns a body target stash plus inference, obligations, diagnostics, and cooperative tasks. Both keep source CST storage read-only and return self-contained semantic output.

Stashed<T> is the memoization boundary. Salsa compares fingerprints (content hashes of the output stash) for change detection. Equal rooted semantic output has the same fingerprint regardless of allocation history, allowing Salsa to stop downstream propagation after recomputation.

Semantic outputs own their storage and have deterministic identity. Checking reads source storage without mutating it, builds the result in a fresh stash, and returns a self-contained Stashed<T> whose fingerprint reflects semantic content rather than process allocation. See Stash for type-level interning.

Required verification: Storage-isolation tests forbid source mutation or cross-stash pointers, deterministic rebuild tests compare fingerprints, and edit experiments show equal outputs are backdated.

Resolution model

Ribs first, module-scope fallback. Ribs capture lexically-scoped bindings (generics, locals, Self). If the first path segment is not found in ribs, fall through to resolver.resolve_segments() for module-level names.

#![allow(unused)]
fn main() {
if let Some(entry) = cx.resolver.ribs.lookup(first.name, ns) {
    // found in rib — generic param, local, or Self
} else {
    // fall through to module-level resolution
    cx.resolver.resolve_path(stash, path, ns)
}
}

Module-level resolution walks direct expanded module symbols for local modules and queries TcxDb::module_children for external crates.

Lexical scope has priority over module fallback. Resolution consults namespace-aware lexical ribs before traversing local or external module membership, so a nearer binding shadows a module-level name without changing the module index.

Required verification: Resolution tests cover shadowing, namespaces, Self, generics, and locals, plus local and external fallback when no rib entry applies.

Incrementality

Per-item CST stashes isolate parsing from checking. Each item's CST is stored as Stashed<Ptr<FnCstData>>. The CST uses RelativeSpans so that whitespace edits before an item do not change its content hash.

Salsa tracked structs are per-item. LocalFnSym, LocalStructSym — one tracked struct per top-level item. Tracked fields store the CST, absolute span, and other independently changing definition facts. Signature, field/member, and body products are tracked functions keyed by the symbol; they are not identity fields stored eagerly in it.

Body changes do not propagate through unchanged signatures. sig() semantically uses only generics, parameters, return type, and predicates. A coarse CST or module dependency may currently cause it to reexecute, but an equal checked signature is backdated before downstream interface consumers.

Incremental reuse is judged at semantic boundaries. Coarse upstream invalidation may reexecute a query, but an equal symbol, signature, or other public semantic product is backdated before unrelated downstream consumers are invalidated. See Salsa for incremental computation.

Required verification: Persistent-database edit tests distinguish execution from downstream propagation for relevant, interface-preserving, and unrelated edits.

Oracle conformance

Emit a common form, then compare exact text. The rustc and Sage emitters independently translate their native IR into the shared reference schema and serialize it deterministically. The conformance decision is byte-for-byte identity of those serialized outputs.

Adapters perform only the representation changes required by the shared schema. The comparison path never applies paired normalization, erases a difference, strips unsupported content, reorders output, or attempts semantic equivalence. Rich diffs may explain a mismatch after exact comparison fails; they cannot turn it into a pass. See Oracle Test Harness.

Conformance is exact shared-form identity. Sage and rustc each adapt independently to the shared oracle schema; the comparator performs no semantic reconciliation, filtering, or paired normalization. See Oracle test harness.

Required verification: Harness tests demonstrate exact success, expose a one-field mismatch, and fail if either adapter or comparator attempts to erase, reorder, or normalize a semantic difference.

Validation evidence

Semantic evidence begins with Rust source. When a behavior can be expressed by a Rust program, integration and acceptance tests start from a checked-in Cargo project and exercise the production parsing, expansion, metadata, checking, inspection, and transport path that is in scope. Reviewed snapshots record what that execution produced; snapshots are never loaded as semantic inputs to manufacture the result under test.

Small constructed values remain appropriate for unit-testing representation code, and narrowly scoped test doubles may inject failures at operating-system or external-service boundaries. Neither establishes that Sage computed a semantic result correctly. See Semantic evidence starts from source.

Semantic integration evidence starts from source. A semantic anchor is established by running real Rust source through the production layers named by the claim. Reviewed snapshots and traces are outputs of that run, not alternate semantic inputs. Constructed values and test doubles can establish only the isolated boundary they directly exercise.

Required verification: Each semantic integration test identifies its checked-in Rust project, production entry point, observed layers, reviewed snapshots or traces, and any deliberately excluded boundary. Replacing a production semantic layer with precomputed JSON or a scripted provider must make the test ineligible as evidence for that layer.

Naming conventions

  • *Cst — CST nodes (stash-allocated, per-item): TypeCst, ExprCst, PathCst, FnCstData, StructCstData
  • *Sym — symbols (salsa-tracked or enum wrappers): LocalFnSym, Symbol, FnSymbol
  • Ty* — typed-tree nodes: TyExpr, TyStmt, TyPat, TyBody
  • *Sig — signature payloads: FnSig, StructSig, StructFields

Anchor tenets

These are writing guidelines, not additional specification rules. The anchor-content specification defines the machine-checked boundaries; the boundary migration record records their implementation and corpus audit.

Give each anchor one independently testable guarantee. A useful claim identifies the relevant situation or precondition, the required outcome, and any failure behavior. A reviewer should be able to explain what observation would contradict it. Name the behavior being guaranteed rather than its position in the document.

Use as much prose as the guarantee needs. One claim may need several paragraphs, a list of cases, or an example. Blank lines improve readability; they should not determine which qualifications are reviewed. Keep the conditions and exceptions needed to understand a guarantee inside its anchor's content, including qualifications expressed in lists or examples.

Split requirements that can change independently. For example, a paragraph about reviewing all tests can easily accumulate unrelated details about the review file. Prefer three claims:

  • complete_test_set: one review considers the claim and all its associated tests together; different tests may establish different parts of the claim.
  • source_representation: saved source snapshots use arrays of lines so a Git diff exposes changes to the reviewed text.
  • review_scope: the review assesses whether the tests establish the claim; implementation excerpts provide context without becoming approval inputs.

A change to snapshot formatting should not require reconsidering what counts as adequate test coverage. Conversely, do not split a precondition from its outcome merely to make paragraphs shorter: the resulting fragments may no longer make complete claims.

Make the end of a claim intentional. Under the anchor boundary rules, content continues until the next anchor, a section heading, or the end of its document or containing .sage prose block. Use <!-- end anchor --> when explanation following a claim should sit outside its review input without starting another section. Check the extracted content when reviewing an edit; nearby prose can qualify a rule even when its author intended only commentary.

Write for both readers and test authors. Prefer concrete behavior over phrases such as “works correctly.” Examples clarify the contract, while tests make the claimed distinction observable. Keep motivation here or in an RFD; put the guarantee beside the entity responsible for it. An explicit TODO or N/A association makes incomplete coverage visible, but does not make an ambiguous claim easier to verify.

Implementation and test evidence

The generated anchor pages report implementation entries, annotated tests, explicit TODO/N/A gaps, and saved claim/test reviews. These associations do not themselves establish that a test ran.

The earlier coverage snapshot preserves the former capability descriptions, edit experiments, and limitations as historical evidence. Current coverage comes from the generated entries; implementation plans and decision rationale belong in the relevant RFDs.