01 — Architecture Overview
The compiler is a demand-driven, incremental query engine (salsa), not a batch pipeline. Everything downstream of source text is a query whose result is memoised and automatically recomputed when its inputs change. The CLI and the LSP are two front-ends over the same query database — the LSP is not a special case, it is the query engine with a different driver.
This shape is the direct answer to laws L1 (no globals — the db is the state),
L2 (incremental for free), and L5 (queries compose along an explicit DAG).
Implementation status — the salsa engine IS the running engine. This banner used to say the opposite ("not yet the running engine … a spike with one input and one tracked query,
line_count"), and it was badly out of date:line_countis gone, and the real build driver constructsskydb::SkyDatabase(rust/crates/project/src/build.rs:124) and demandsskydb::go_program(:419) to produce the emitted Go — replacing the eagerlower_program_cfg+emit_programpair it used to call directly.
skydb(rust/crates/skydb/src/lib.rs) is a realsalsa0.28 database with#[salsa::input]s (SourceFile,BuildConfig) and these tracked queries spanning the whole DAG:
Query Where Grain parseskydb:482per SourceFilemodule_exportsskydb:126per module resolve_queryskydb:181per module type_world_query/check_world_queryskydb:202/:224whole program record_result_sig_query/callsite_param_records_queryskydb:243/:272whole program infer_queryskydb:304per DefIdgo_programskydb:434whole program Two things below are still not what the code does, and are called out where they appear rather than blanket-disclaimed here:
- Lowering has no per-def or per-module grain. The diagram's
typed_hir(DefId)→go_module(ModuleId)edges do not exist; there is one whole-programgo_program. See07"Position in the query graph".exhaustiveness(DefId)is not a separate tracked query.The hand-rolled
hir::db::SourceDb(rust/crates/hir/src/db.rs) still exists as a second backend with identicalModuleIdsemantics, and itsRefCellexports cache is gone (db.rs:61). Remaining engine work is tracked in12.
Data flow (target: all edges are salsa queries)
flowchart TD
subgraph Inputs["Inputs (set by the driver)"]
SRC["source_text(FileId)"]
TOML["sky_toml / project config"]
FFI["ffi_surface (pinned, deterministic)"]
end
SRC --> CST["parse(FileId) -> Lossless CST + parse diagnostics"]
CST --> AST["ast(FileId) -> typed AST view (rowan)"]
AST --> ITEMS["module_items(FileId) -> declarations, exports"]
ITEMS --> RES["resolve(ModuleId) -> names -> DefId (imports, scopes)"]
TOML --> GRAPH["module_graph(project) -> topo order"]
ITEMS --> GRAPH
RES --> INFER["infer(DefId) -> types, per-region type map, diagnostics"]
FFI --> INFER
INFER --> EXH["exhaustiveness(DefId) -> diagnostics"]
INFER --> HIR2["typed_hir(DefId) -> lowering IR (typed)"]
HIR2 --> GO["go_module(ModuleId) -> deterministic Go source"]
GO --> WRITE["build(project) -> write sky-out/, run go build"]
RES -. LSP .-> HOVER["hover / goto / completion / diagnostics"]
INFER -. LSP .-> HOVER
- Inputs are the only mutable things; the driver
set_*s them. Everything else is a pure function of inputs, memoised by salsa. Editing one file invalidates only the queries that transitively depend on it (L2). - No phase reaches into a global.
infercannot read aglobalCgEnv; it takes thedband asksresolve(module). That is L1, enforced structurally.
The interner is the spine (L3)
Everything with identity is an integer id, allocated in an arena, compared by
== on the int:
| Interned thing | Id | Replaces (Haskell) |
|---|---|---|
| File path | FileId | ad-hoc FilePath keys |
| Module name | ModuleId | ModuleName.Canonical |
| Definition (top-level/local) | DefId | name-string map keys |
| Symbol name | Name (interned str) | String everywhere |
| Type | Ty (interned) | T.Type + structural Eq |
| Type variable | TyVarId (arena) | UF.Point pointer identity ← the one real typechecker design task, solved for free |
| Source span | Span { FileId, TextRange } | A.Region |
Interning gives three wins at once (L3): O(1) identity comparison, arena allocation (no GC pressure, predictable memory — the user's TCO/memory concern), and deterministic iteration when you walk ids in allocation order (L4).
Controlled mutation, not purity theatre and not IORef soup
Haskell forced a false choice: pure-threading (verbose) or IORef globals (untraceable). Rust's idiom is the middle path the compiler actually wants:
- Interners / arenas are append-only stores inside the
db. Monotonic, single-writer, deterministic — the "register-on-first-mention" pattern the Haskell code kept reinventing, now the default. - Union-find (type inference) is a
Vec<TyVarId>with in-place path compression + union-by-rank — genuinely mutable, genuinely fast, local to the inference query, never global. This is the honest scoped-mutation sweet spot we kept reaching for. - Everything else is pure and memoised by salsa.
Diagnostics as data (L7, L8)
- Parsing produces a lossless CST (rowan): every byte, including trivia and errors, is in the tree. The LSP works on syntactically broken code; formatting is exact; error recovery is built in (L8).
- Every query returns
(result, Vec<Diagnostic>)— errors never throw, never short-circuit the whole build.Diagnosticis a structured value (span, code, severity, labels, suggested fix) rendered by one reporter for CLI and LSP.
Determinism, end to end (L4)
- No
HashMapiteration reaches output; emission walksBTreeMap/IndexMap/ interned-id order. - Fresh names (type vars, temporaries) are drawn from a counter seeded by a deterministic pre-order traversal, at the collection site, never from hashmap order at the emission site (the precise mistake the Haskell "sort keys before iterating" idea got wrong at the record-field site).
- The FFI surface is generated once, deterministically, and pinned/committed —
the platform-variant Go inspector never runs mid-build (see
09). - A CI gate compiles the corpus N× across seeds + platforms and byte-diffs.
The Go backend stays (L10, L9)
The compiler still emits Go and reuses the existing runtime-go/rt runtime
(goroutine-backed Tasks, the deploy story, SkyDeploy). The rewrite fixes the
lowering — a typed IR with a well-specified type system so coercion is the
rare, explicit exception rather than a pervasive rt.Coerce residual surface
(L9). See 07 + 08.
Why this specifically fixes the AI-velocity problem
- Bounded context: a change to inference touches the
tycrate; a model loads that crate, not a 60k-line world. Queries have typed inputs/outputs, so a local edit is locally verifiable. - The type system catches the model's mistakes the way it catches a human's
— exhaustive matches, no
any,Resulteverywhere (L6). - rust-analyzer gives the model (and you) real hover/goto/rename inside the compiler — the tooling hole, closed for the authors too.