dowel

Architecture

This is a design document about internals. As a user, the behavior you can rely on is covered by 60-cli.md (commands and the output contract) and 63-guides.md (working with the cache).

1. One core, multiple frontends

Incremental evaluation, typed values with provenance, and the language server are not separate features but three aspects of the same core — the same structure as rust-analyzer sitting on the same query foundation (Salsa) as rustc.

So “build the build system first, add the language server later” is not a viable order. The constraints that cannot be retrofitted are fixed first; shipping the language server itself is deferred.

2. The four constraints that cannot be retrofitted

Write the evaluator as a naive tree-walking interpreter and the following become unrecoverable:

  1. Error-tolerant parsing — do not stop at syntax errors; keep partial trees and continue evaluating. The lossless CST is the source of truth; the AST is a projection of it
  2. Spans everywhere — every value carries a source location, surviving string expansion and transformation
  3. Cancellability — every keystroke cancels the previous query; every layer of the evaluator must propagate cancellation
  4. Guaranteed termination — the reason for non-Turing-completeness is not simplicity of authoring but guaranteeing the language server always responds

3. Query catalog (initial draft)

Query Input Output
parse(file) file contents CST
eval(module) CST, parent scope value environment
resolve(target) name target definition
interface(target) target merged propagated properties
probe(toolchain, check) toolchain hash, check contents fact
plan(target, config) target, configuration action graph

Required optimizations

4. Value representation

Value = { type, data, provenance }

Provenance is a constituent of the value, not side-band data. Since provenance is a projection of the query graph, it costs almost nothing extra once the incremental engine exists.

5. Persistence (no daemon)

There is no resident daemon (ADR-0002). Since the in-memory graph cannot be kept across processes, three things substitute:

5.1 Make restoration cost O(touched nodes)

Avoid serialize-everything / restore-everything — it forfeits the incremental advantage as the graph grows.

5.2 Change detection

File watching is unavailable, so changes are judged by a stat sweep over the known input set.

5.3 Concurrent access from multiple processes

Required because the CLI and the language server touch the same store.

5.4 What this drags in

6. Where the language server stands

“No daemon” and a language server do not conflict. The distinction is who starts it and how long it lives.

  Started by Lifetime User’s perception
daemon implicit outlives projects unaware it exists; unclear how to stop it
language server the editor ends with the editor something explicitly started

Invariants:

7. Intended scale and limits

A daemonless design loses at extreme scale. Bazel / Buck2 adopt daemons because at hundreds of thousands of nodes, index validation itself dominates.

The intended scale is 10^3–10^4 targets. Whole-monorepo single-graph usage is not a goal.

8. Relation to a future execution layer

Content addressing, append-only logs, and fingerprint validation are exactly the machinery an action cache needs; extending to one later reuses them as-is. Nothing gets built twice.

9. Reducing cold configure

Configure time decomposes into three terms:

  1. Probe executiontry_compile and friends; each one launches a compiler and linker process
  2. Discovery sweeps — file-system walking by find_package / pkg-config
  3. Manifest evaluation and file writing

The felt slowness comes mostly from 1 and 2, independent of implementation language. 3 dominates only at thousands of targets.

The countermeasure: treat probe results not as an implicit cache (CMakeCache.txt-style) but as an independent fact database.