Shared Checking Design
This chapter records mechanisms shared by the signature-checking phase and body-checking phase. Begin with those phase contracts for inputs, outputs, failure modes, incremental boundaries, and evidence; use this page as the deeper reference for their common query and stash design.
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. 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
(cst/, local_syms/, infer/) subdivided by item kind or sub-concern.
Avoid monolithic files. One struct or closely-related cluster per file.
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—Check, the signature-lowering contextcheck::infer_ctx—InferCtxandScope, the body-checking contextresolve—Resolver,Namespace, and lexical ribs
Item-specific logic imports from these; it doesn't 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.
sig is the cross-item boundary. A signature query extracts exactly what
other items need to type-check against this one: generics, parameter types,
return type, field types. It is the minimal public surface.
Associated-item identity is a separate narrow boundary.
LocalTraitSym::items and LocalImplSym::items return stable owner-linked
symbols. A consumer then queries only the selected item's signature; item
enumeration does not check any associated body.
Detail queries are lazy. body(), fields(), and similar queries compute
information that is either not needed from other items or only needed some of
the time. They depend on sig() but are not depended on by other items'
signatures.
Symbols and generic parameters are minted exactly once. An owner sig()
query mints its GenericParam symbols via
cst.generics.check(db, cx, parent) and stores them in a Binder.
LocalTraitSym::items and LocalImplSym::items mint stable owner-linked item
symbols; an associated function's sig() reuses the owner binder before
minting method-level generics. Detail queries such as fields() and body()
open those binders and bring the same parameter symbols and owner Self type
into scope. No query remints an existing identity.
Sequential layering inside a query. body() calls sig() → opens
binder → resolves names → runs inference → elaborates the completed typed
tree. Each step builds on the prior. From outside: one query, one result.
Intermediates (ResolvedBody, method candidates, adjustment recipes, and
partially inferred expressions) are not separately queryable.
crates.sage_ir.checking.symbol_keyed_interfaces
Verification intent: Query traces show a signature request and a body request as distinct symbol-keyed roots, reject ambient caller parameters and callee-body reads, and demonstrate unchanged reuse across an interface-preserving edit.
crates.sage_ir.checking.identity_minting
Verification intent: Cross-query and persistent-edit tests compare
owner, generic-parameter, Self, and associated-item identities across
signatures, field/member details, and bodies, including shadowed equal names.
Completed body output
The result of successful body checking is the elaborated typed
IR, not a typed copy of source syntax. Resolved definitions,
substitutions, borrows, dereferences, and coercions are materialized in the
tree. A method resolver may describe an autoref or autoderef while selecting a
candidate, but body elaboration consumes that description before returning
CheckedBody.
Checking one body may query callee signatures, associated items, impl headers, and trait-solver results. It does not query callee bodies. This is both a layering rule and an incremental dependency requirement.
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 dst: Stash (output types or resolved exprs).
At the end, cx.finish(root) wraps dst into Stashed<T>.
Purpose-specific contexts. Check lowers signatures and their parameter
environments into a target stash. InferCtx owns the body target stash plus
inference, obligations, diagnostics, and cooperative tasks. Both treat the
source CST stash as 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. Deterministic
allocation means same CST + same scope = same fingerprint = no downstream
re-execution.
crates.sage_ir.checking.owned_products
Verification intent: Structural traversal detects any source-stash or tree-sitter pointer in checked output, equal recomputation yields equal fingerprints, and a persistent edit test shows an equal recomputed product stops downstream reexecution.
Trait obligations
Trait proof and type-producing normalization operations are handled by the
check::solve subsystem under
the trait-solver semantic contract. The
body checker keeps an obligation registry rather
than treating a conditional solver answer as final: substitutions are applied,
residual goals remain registered, and every residual must either be discharged
or produce a diagnostic before body checking finishes.
The Salsa query boundary canonicalizes the caller's local inference state. A canonical variable records whether it represents a rigid caller parameter or a bindable inference variable, together with its kind and relative current universe ceiling. The query records the current universe relative to a caller-retained absolute base, so closed queries and nested binders can reopen response existentials safely. The query also includes the local crate and parameter environment, so local impl discovery and memoization do not depend on ambient state.
Proof-local equality changes are transactional. A short-lived operation runs in a child egraph version and collapses that child into its direct parent only after the whole operation succeeds. A failed operation discards the child. Concurrent impl candidates own isolated proof contexts and match within a local child version; they produce canonical responses and are dropped rather than being merged into a requester. Within each egraph, inference-variable IDs are globally unique and carry an owning version, so branch-local types cannot be reinterpreted outside their owning ancestry.
The body environment contains opened function predicates and the deduplicated
defining predicates of referenced local traits. Generic function uses and
struct/enum construction or explicit type uses submit their instantiated
parameter environments. Once trait method selection is complete, associated
projections in its instantiated signature are replaced by caller inference
variables and registered as input-only normalization operations; the caller's
expected type is related only after the solver output is imported. Obligations
retain source provenance, proof obligations deduplicate after canonicalization,
retry only after relevant inference wakes, and receive a mandatory terminal
pass after inference fallback. CheckedBody creation asserts that the
obligation registry, runtime, wake queue, and root egraph have no live work.
crates.sage_ir.inference.terminal_obligations
Verification intent: Tests cover obligations solved immediately, woken after inference, deduplicated after canonicalization, rejected at the terminal pass, and exhausted by a resource limit; successful completion asserts no live obligations, tasks, wakes, or root branches.
The implemented external-inherent slice opens owner and method type generics
in one synchronous child egraph version. Receiver and argument compatibility
must all succeed before the child collapses into the body root. Its instantiated
parameter environment is accumulated in StagedObligationBatch and published
only after commit, so a rejected call cannot leak generic equalities, wakeups,
or obligations. This transaction does not cross an await point. The broader
method algorithm will extend the same boundary to result compatibility,
autoderef, conditional candidate responses, and local inherent methods.
crates.sage_ir.inference.atomic_checking
Verification intent: Unit tests reject a candidate after partial generic matching and observe the parent unchanged, accept the same shape and observe exactly one staged obligation publication, and exercise isolated sibling candidates without shared inference identities.
Deferred lifetime and borrow semantics
Lifetime syntax remains in the CST, but checking currently maps every
explicit, elided, universal, existential, external, and synthesized lifetime
directly to Lifetime::Dummy. No lifetime inference variables are introduced.
The only lifetime relation is Outlives(Dummy, Dummy), which succeeds.
References and dereferences remain ordinary typed operations. Sage does not currently validate liveness, uniqueness, overlap, or any other borrow property. This deliberate soundness hole avoids committing to a separate region-inference subsystem before Sage's unified type-and-lifetime inference design is settled.
Resolution model
Ribs first, module-scope fallback. Ribs capture lexically-scoped bindings
(generics, locals, Self). If the first path segment isn't found in ribs,
fall through to resolver.resolve_segments() for module-level names.
CST representation
Stash-allocated, relative-spanned, AllocStashData. Per-item stashes
with content-addressed (hash-consed) allocation. CST type aliases follow the
pattern type FnCst<'db> = Stashed<Ptr<FnCstData<'db>>>. The CST captures
all syntactic detail needed for later phases. No back-pointers to tree-sitter
nodes.
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.