Architecture
Implementation architecture of Roughly's analysis stack — the hand-written parser, the salsa semantics database, and the language server
This document is the authoritative implementation architecture for Roughly. The Typing Reference is the authoritative user-facing typing contract; this page defines the implementation boundaries that realize it.
Crate graph
Section titled “Crate graph”The crate graph enforces the layering — crate boundaries are the compiler-checked analog of “make illegal states unrepresentable”:
syntax— a hand-written lexer and recursive-descent/Pratt parser emitting rowan green/red trees: lossless (every byte, including trivia, reprints exactly), error-resilient (every parse yields a tree; errors are local to the break), and position-independent (width-only green nodes make an untouched statement’s subtree structurally equal across edits elsewhere in the file).#:annotations are lexed as structured trivia and parsed as first-class nodes with real spans — type syntax is grammar, not comment-scraping. Depends on nothing semantic.semantics— the salsa database and every analysis query: the per-item item tree and HIR, naming, interned types, Hindley–Milner inference, the stub corpus, lints, and the diagnostics edge. Depends onsyntaxand salsa.ide— editor features (hover, navigation, rename, completion, inlay hints, signature help, code actions, symbols) as pure reads ofsemanticsqueries at byte offsets. Editor-protocol concerns live in the server, never here.format— the preserving formatter, onsyntaxonly (compiler-enforced: it can never depend on analysis results).roughly— the product surface: the LSP server and the CLI (check,fmt,server,debug). Owns configuration, position-encoding conversion, diagnostics assembly and publication, and suppression comments.
The *-legacy crates (roughly-legacy, analysis-legacy, engine-legacy)
are the previous stack, frozen in-tree as the cross-implementation oracle and
benchmark baseline for the differential suites until every rewrite gate holds;
no code is shared between the two stacks by design.
The semantics database
Section titled “The semantics database”Analysis is incremental at item granularity on salsa:
SourceFileis a salsa input (text + document kind);ProjectFilesis a singleton input listing the project’s files, package documents first in workspace-relative path order (the order the last-writer-wins symbol index and the CLI agree on).StubSourcesis a set-once singleton carrying the.Rtypescorpus.parse(file)is a per-file query accelerated by a splice cache: the file’s previous text and tree are kept beside the database (shared across storage-handle clones), the current edit is derived as the longest common-prefix/suffix delta, andsyntax::reparsereparses only the touched statement region, sharing the untouched green subtrees by pointer. The spliced result is byte- and error-identical to a from-scratch parse (the edit-stream fuzzer pins the equivalence; the splice refuses and falls back whenever that is not provable), so the cache is invisible to every downstream query — sub-file parse invalidation stays out of salsa by design. Semantic incrementality happens one level down: the item tree assigns insertion-stable identities (kind + name + parent + disambiguator, interned) so an edit in one item leaves sibling items’ derived values equal, and salsa’s early cutoff prunes all downstream work.- Per item: green subtree → HIR (with item-relative spans) → naming (the
mutable-slot variable model with reaching-write flow) → the inference walk
(
item_check) producing expression types, type errors, strict origins, and the exported scheme. - The package interface resolves cyclic definition groups through one
canonical fixpoint, independent of which member is queried first:
interface_sccscondenses the static item-to-winner reference graph (iterative Tarjan, canonical project order),scc_schemesruns Jacobi rounds from all-Unknownuntil the scheme table converges (the round cap pins all members), anditem_checkadopts the canonical scheme as the single exported truth. Member checks inside the fixpoint never re-enteritem_check, so no salsa cycle forms; salsa’s own cycle recovery remains only as a backstop for reference edges the static graph cannot see. - Whole-project walks read per-item projections, not full naming:
item_interface_reads(the read-name set feedinginterface_sccs) anditem_top_level_names(the binding names feeding conditional-slot resolution) are small tracked queries whose values survive body edits that only shift ranges — the common keystroke backdates the projection and the project-wide graph walks stay green instead of re-executing. - Types are interned (id equality, no deep clones). Deep resolution over
the interned type DAG is memoized per binding epoch with
cycle-cut-to-
Unknownsemantics; the decision log records the design.
Diagnostics split into two layers with one source of truth:
parse_stage_diagnostics(file)— syntax errors, typing-directive errors, and#:block-form refusals: pure functions of the parse.file_diagnostics(file)— the parse-stage set plus naming findings (unresolved, unused) and type errors;strict_diagnostics(file)renders strict-mode origins separately so hosts publish them only under[check] strictor a per-file directive.
The host (server and CLI share the assembly in crates/roughly) gates classes
by configuration, applies per-file # typing: overrides, escalates unresolved
findings to errors under strict, appends lints, and applies
# roughly: allow(...) suppression comments.
The language server
Section titled “The language server”Two threads. The async-lsp frontend on the tokio thread serializes every request and notification into jobs for one dedicated worker thread that owns the salsa database and all documents (the database handle is deliberately confined to that thread).
- Latest-edit-wins cancellation: an input-mutating notification cancels
the worker’s salsa cancellation token before enqueueing, so an in-flight
query unwinds cooperatively; every subsequent job starts on a fresh storage
handle (a cheap clone), since a flip is consumed by whichever query it
killed. Best-effort features answer empty on cancellation; authoritative
answers do not degrade — pull diagnostics return a retryable
SERVER_CANCELLED(retriggerRequest: true) and rename returnsCONTENT_MODIFIED. - Two-wave push: on every sync the server publishes the parse-stage classes plus lints immediately (versioned), then the settled full set version-less at idle time — a faithful superset, so the second wave only adds findings. Pull-capable clients suppress push entirely and get content-hash result ids with unchanged reports.
- Idle work: owed settled publishes are served most-recently-edited first; with nothing owed, the worker warms one workspace file at a time (never published). Cancelled idle work is requeued.
- Coherence failures panic. Document-sync failures are unrecoverable; a panic escaping a worker job terminates the process deterministically rather than serving corrupted state. IDE feature lookups, by contrast, must never panic.
- Positions convert at the protocol edge only, against the target document’s own text, in the negotiated encoding (UTF-16 by default, UTF-8 when offered). Signature-help label offsets are always UTF-16 per the LSP spec.
.Rtypesstub buffers andNAMESPACEfiles are served standalone — loader-problem diagnostics, semantic tokens, and type-name goto for stubs; import validation for NAMESPACE — without ever entering the database.
Differential correctness
Section titled “Differential correctness”Until the legacy stack’s final deletion, the differential suites in
legacy/differential are the regression net: the typing fixture suites and
the per-position IDE sweep run through both stacks and compare semantic
classes, targets, and ranges (never prose — wording is free to improve), with
committed allowlists for adjudicated oracle defects. The real-file corpus
arms (semantic, formatter) are running instruments. The perf and memory
witnesses (test_stats) assert the measured budgets — wall, resident set,
and resolve-step linearity — as CI-checkable thresholds.