{"schemaVersion":1,"id":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks","title":"Incremental Computation with Salsa and Adapton Frameworks","description":"An examination of demand-driven incremental computation as implemented in rust-analyzer, memoization with automatic invalidation, dependency tracking, and the architectural patterns that enable sub-millisecond IDE responses.","language":"en","datePublished":"2025-10-06","dateModified":null,"contentFormat":"text/markdown","content":"Compilers\n\n# Incremental Computation with Salsa and Adapton Frameworks\n\n2025-10-068 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks>)\n\n## The problem: recomputation is everywhere\n\nConsider what happens when a character is typed in an IDE. The language server needs to:\n\n1. Re-lex the modified file (or at least the modified region)\n2. Re-parse the affected syntax tree nodes\n3. Re-resolve names in the modified scope\n4. Re-typecheck expressions that depend on any changed types\n5. Regenerate diagnostics, completions, hover info\n\nFor a naive implementation, every keystroke triggers a full pipeline rerun. In a 100K-line codebase with complex type inference, that's easily 2-5 seconds. Users notice anything above 100ms. Something has to give.\n\nThe traditional answer is \"cache everything and invalidate carefully.\" But manual cache invalidation in a compiler pipeline is a nightmare. Dependencies are deep, circular, and change shape depending on the input. Miss one invalidation edge and you get stale results. Over-invalidate and you're back to full recomputation.\n\nIncremental computation frameworks automate this. You describe your computation as a graph of queries, the framework tracks dependencies at runtime, and when an input changes, it recomputes exactly what's needed, nothing more.\n\n## Salsa: demand-driven incremental computation\n\n[Salsa](<https://github.com/salsa-rs/salsa>) is a Rust framework developed by the rust-analyzer team (originally by Niko Matsakis). It implements demand-driven incremental computation with automatic dependency tracking.\n\nThe core model has three concepts:\n\n1. **Inputs**, mutable values you set from outside (file contents, configuration)\n2. **Tracked functions**, pure computations that read inputs and other tracked functions\n3. **The database**, holds all inputs, cached results, and dependency metadata\n\nThe following is a minimal example, a system that parses files and computes a symbol index:\n\n```rust\n#[salsa::input]\npub struct SourceFile {\n #[return_ref]\n pub name: String,\n #[return_ref]\n pub contents: String,\n}\n\n#[salsa::tracked]\npub struct ParsedFile<'db> {\n #[return_ref]\n pub ast: Ast,\n #[return_ref]\n pub errors: Vec<ParseError>,\n}\n\n#[salsa::tracked]\nfn parse_file<'db>(db: &'db dyn Db, file: SourceFile) -> ParsedFile<'db> {\n let contents = file.contents(db);\n let (ast, errors) = parser::parse(&contents);\n ParsedFile::new(db, ast, errors)\n}\n\n#[salsa::tracked]\nfn file_symbols<'db>(db: &'db dyn Db, file: SourceFile) -> Vec<Symbol> {\n let parsed = parse_file(db, file);\n let ast = parsed.ast(db);\n extract_symbols(ast)\n}\n\n#[salsa::tracked]\nfn all_symbols<'db>(db: &'db dyn Db, files: &[SourceFile]) -> SymbolIndex {\n let mut index = SymbolIndex::new();\n for &file in files {\n for symbol in file_symbols(db, file) {\n index.insert(symbol);\n }\n }\n index\n}\n```\n\nWhen you call ` all_symbols `, Salsa executes the full computation and records that ` all_symbols ` depends on ` file_symbols ` for each file, which depends on ` parse_file `, which depends on the file's ` contents ` input.\n\nNow change one file's contents. On the next call to ` all_symbols `, Salsa:\n\n1. Notices the ` contents ` input changed for one file\n2. Checks if ` parse_file `'s result actually changed (maybe you only changed a comment)\n3. If the parse result changed, checks if ` file_symbols ` changed\n4. Only recomputes ` all_symbols ` if the actual symbol list for that file changed\n\nThis is the key insight: **early cutoff**. Even if an input changes, if the intermediate result is the same as before, downstream computations don't need to re-run. In practice, this means changing a function body doesn't invalidate the symbol index, the symbols didn't change.\n\n### The revision counter\n\nInternally, Salsa uses a global revision counter. Every time an input changes, the revision increments. Each cached value stores two things:\n\n- **verified\\_at**: the last revision where this value was confirmed valid\n- **changed\\_at**: the last revision where this value actually changed\n\nWhen you access a cached value, Salsa checks if ` verified_at == current_revision `. If not, it walks the dependency graph backward to see if any input that this value (transitively) depends on has ` changed_at > verified_at `. If no dependency changed, the value is still valid, update ` verified_at ` and return the cached result without recomputing.\n\n```\nRevision 1: Initial computation\n file.contents changed_at=1\n parse_file changed_at=1 verified_at=1\n file_symbols changed_at=1 verified_at=1\n all_symbols changed_at=1 verified_at=1\n\nRevision 2: Edit file A (comment only)\n file_A.contents changed_at=2\n parse_file(A) → recompute → AST identical → changed_at=1 verified_at=2\n file_symbols(A) → dependency unchanged → changed_at=1 verified_at=2\n all_symbols → dependency unchanged → changed_at=1 verified_at=2\n // Total work: re-parsed one file. Everything else was free.\n\nRevision 3: Edit file B (add a function)\n file_B.contents changed_at=3\n parse_file(B) → recompute → AST different → changed_at=3 verified_at=3\n file_symbols(B) → recompute → new symbol → changed_at=3 verified_at=3\n all_symbols → recompute (one dep changed) → changed_at=3 verified_at=3\n // parse_file(A), file_symbols(A) untouched, still verified_at=2, valid.\n```\n\n### Cycle handling\n\nCompilers often have circular dependencies, a trait implementation might reference a type that references the trait. Salsa handles cycles explicitly:\n\n```rust\n#[salsa::tracked(cycle_fn=recover_type_cycle)]\nfn infer_type<'db>(db: &'db dyn Db, expr: ExprId) -> Type {\n // ... may call infer_type on other expressions\n}\n\nfn recover_type_cycle<'db>(\n db: &'db dyn Db,\n _cycle: &salsa::Cycle,\n expr: ExprId,\n) -> Type {\n Type::Error // Break the cycle with an error type\n}\n```\n\nWhen Salsa detects a cycle (the same query is called recursively with the same key), it invokes the recovery function. The recovery function determines the behavior, error types, fixed-point iteration, or any domain-appropriate strategy.\n\n## Adapton: the academic foundation\n\n[Adapton](<http://adapton.org/>) predates Salsa and provides the theoretical framework. The core paper (Hammer et al., 2014) introduces \"demanded computation graphs\", a model where computations are nodes, data dependencies are edges, and the system lazily propagates changes through the graph.\n\nThe key contribution is the distinction between two strategies:\n\n**From-scratch consistency**: verify the entire graph is consistent before returning any result. Expensive upfront but guarantees correctness.\n\n**Demanded consistency**: only verify the portions of the graph that the current query touches. Lazy, efficient, but requires careful ordering.\n\nSalsa uses demanded consistency. When you query ` all_symbols `, it doesn't verify every cached value in the system, only the ones in the dependency chain of ` all_symbols `.\n\nThe following is a simplified Adapton-style implementation in TypeScript to make the concepts concrete:\n\n```typescript\ntype CellId = number;\ntype ThunkId = number;\n\nlet currentRevision = 0;\nlet activeThunk: ThunkId | null = null;\n\ninterface Cell<T> {\n value: T;\n changedAt: number;\n dependents: Set<ThunkId>;\n}\n\ninterface Thunk<T> {\n fn: () => T;\n value: T | undefined;\n verifiedAt: number;\n changedAt: number;\n dependencies: Set<CellId | ThunkId>;\n dependents: Set<ThunkId>;\n}\n\nconst cells = new Map<CellId, Cell<any>>();\nconst thunks = new Map<ThunkId, Thunk<any>>();\n\nfunction createCell<T>(value: T): CellId {\n const id = cells.size;\n cells.set(id, { value, changedAt: currentRevision, dependents: new Set() });\n return id;\n}\n\nfunction setCell<T>(id: CellId, value: T): void {\n const cell = cells.get(id)!;\n if (Object.is(cell.value, value)) return; // Early cutoff\n currentRevision++;\n cell.value = value;\n cell.changedAt = currentRevision;\n}\n\nfunction readCell<T>(id: CellId): T {\n const cell = cells.get(id)!;\n if (activeThunk !== null) {\n const thunk = thunks.get(activeThunk)!;\n thunk.dependencies.add(id);\n cell.dependents.add(activeThunk);\n }\n return cell.value;\n}\n\nfunction createThunk<T>(fn: () => T): ThunkId {\n const id = thunks.size + 10000; // Offset to distinguish from cells\n thunks.set(id, {\n fn,\n value: undefined,\n verifiedAt: -1,\n changedAt: -1,\n dependencies: new Set(),\n dependents: new Set(),\n });\n return id;\n}\n\nfunction queryThunk<T>(id: ThunkId): T {\n const thunk = thunks.get(id)!;\n\n if (thunk.verifiedAt === currentRevision) {\n return thunk.value!; // Already verified this revision\n }\n\n // Check if any dependency changed since last verification\n let needsRecompute = thunk.value === undefined;\n if (!needsRecompute) {\n for (const dep of thunk.dependencies) {\n if (dep < 10000) {\n // Cell dependency\n const cell = cells.get(dep)!;\n if (cell.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n } else {\n // Thunk dependency, recursively verify\n queryThunk(dep);\n const depThunk = thunks.get(dep)!;\n if (depThunk.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n }\n }\n }\n\n if (needsRecompute) {\n thunk.dependencies.clear();\n const prevActive = activeThunk;\n activeThunk = id;\n const newValue = thunk.fn();\n activeThunk = prevActive;\n\n if (!Object.is(newValue, thunk.value)) {\n thunk.changedAt = currentRevision;\n thunk.value = newValue;\n }\n }\n\n thunk.verifiedAt = currentRevision;\n return thunk.value!;\n}\n```\n\nThis is ~80 lines that capture the essential algorithm. Real frameworks add garbage collection of unused cache entries, parallel execution, durability, and proper cycle detection, but the core idea is dependency tracking plus early cutoff.\n\n## Applying this outside compilers\n\nIncremental computation patterns apply effectively in non-compiler contexts. Consider a dashboard that aggregates data from 15 microservices, as a representative example. Each service updates at different frequencies, some every second, some every hour. The dashboard computes derived metrics from combinations of these sources.\n\nBefore: every render fetches all 15 sources and recomputes all metrics. Latency: 800ms.\n\nAfter: modeling each data source as an input cell, each metric as a tracked computation, and using a lightweight Adapton-style framework. On most renders, only 1-2 sources have changed, so only the affected metrics recompute. Latency drops to 30-80ms depending on what changed.\n\nThe pattern works anywhere you have: slowly-changing inputs, expensive derived computations, and repeated queries. Build systems (Bazel's action graph), reactive UI frameworks (Solid.js signals), spreadsheet engines, they all use variations of the same idea.\n\n## Practical takeaways\n\nFor any system that repeatedly derives data from changing inputs, it is worth considering whether incremental computation fits. The upfront investment in modeling computation as a query graph pays dividends when the dataset grows. Salsa is a natural starting point for Rust projects, and the concepts port to other languages (the TypeScript sketch above serves as a starting point). The mental model, inputs, tracked functions, dependency graphs, early cutoff, is the valuable part, not any specific framework.\n\nAs a concrete example, restructuring a language server around proper Salsa query boundaries brought autocompletion from 400ms down to 8ms. The computation was the same. The only difference was defining where the query boundaries were.\n\n## Frequently asked questions\n\n### What is incremental computation?\n\nIncremental computation is a programming paradigm where computations automatically reuse previous results when inputs change partially. Instead of recomputing everything from scratch, the system identifies what changed and only recomputes the affected portions of the result.\n\n### How does Salsa differ from regular memoization?\n\nRegular memoization caches results by input but has no invalidation mechanism, stale entries persist forever. Salsa tracks dependencies between computations, so when an input changes, it knows exactly which cached results are affected and invalidates only those, while preserving all unaffected cached results.\n\n### Can I use Salsa outside of compiler tooling?\n\nYes, Salsa is a general-purpose incremental computation framework. Any workload where you repeatedly compute derived data from slowly-changing inputs benefits, build systems, data pipelines, reactive UIs, and configuration management are all viable use cases.\n\n## Tips\n\n1. **Tip:** When modeling incremental computations, make your input granularity match your invalidation granularity. If you track changes per-file but compute per-function, you'll recompute entire files when a single function changes, defeating the purpose.\n2. **Tip:** Always separate ` reads ` from ` writes ` in your query definitions. A query that both reads and mutates state breaks the incremental DAG and forces full recomputation downstream.\n3. **Tip:** Use ` salsa::accumulator ` for diagnostics collection instead of side-effecting during queries. Accumulators integrate cleanly with the dependency graph and get invalidated properly.\n4. **Tip:** Profile query hit rates with ` salsa::DebugQueryTable ` before optimizing. Teams have been observed spending weeks optimizing a query that was already 99.8% cached, the bottleneck was somewhere else entirely.\n\n- incremental-computation\n- salsa\n- adapton\n- rust-analyzer\n- compilers\n- ide\n- demand-driven\n- memoization\n\n[Back to Blog](<https://lucioduran.com/blog>)","headings":[{"level":1,"text":"Incremental Computation with Salsa and Adapton Frameworks","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#title"},{"level":2,"text":"The problem: recomputation is everywhere","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#the-problem-recomputation-is-everywhere"},{"level":2,"text":"Salsa: demand-driven incremental computation","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#salsa-demand-driven-incremental-computation"},{"level":3,"text":"The revision counter","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#the-revision-counter"},{"level":3,"text":"Cycle handling","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#cycle-handling"},{"level":2,"text":"Adapton: the academic foundation","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#adapton-the-academic-foundation"},{"level":2,"text":"Applying this outside compilers","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#applying-this-outside-compilers"},{"level":2,"text":"Practical takeaways","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#practical-takeaways"},{"level":2,"text":"Frequently asked questions","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#questions"},{"level":3,"text":"What is incremental computation?","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#question-1"},{"level":3,"text":"How does Salsa differ from regular memoization?","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#question-2"},{"level":3,"text":"Can I use Salsa outside of compiler tooling?","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#question-3"},{"level":2,"text":"Tips","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#article-tips"}],"sections":[{"heading":"Incremental Computation with Salsa and Adapton Frameworks","level":1,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks","content":"Compilers"},{"heading":"Incremental Computation with Salsa and Adapton Frameworks","level":1,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#title","content":"2025-10-068 min read\n\nLucio Durán\nEngineering Manager & AI Solutions Architect\n\nAlso available in: [Español](<https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks>)"},{"heading":"The problem: recomputation is everywhere","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#the-problem-recomputation-is-everywhere","content":"Consider what happens when a character is typed in an IDE. The language server needs to:\n\n1. Re-lex the modified file (or at least the modified region)\n2. Re-parse the affected syntax tree nodes\n3. Re-resolve names in the modified scope\n4. Re-typecheck expressions that depend on any changed types\n5. Regenerate diagnostics, completions, hover info\n\nFor a naive implementation, every keystroke triggers a full pipeline rerun. In a 100K-line codebase with complex type inference, that's easily 2-5 seconds. Users notice anything above 100ms. Something has to give.\n\nThe traditional answer is \"cache everything and invalidate carefully.\" But manual cache invalidation in a compiler pipeline is a nightmare. Dependencies are deep, circular, and change shape depending on the input. Miss one invalidation edge and you get stale results. Over-invalidate and you're back to full recomputation.\n\nIncremental computation frameworks automate this. You describe your computation as a graph of queries, the framework tracks dependencies at runtime, and when an input changes, it recomputes exactly what's needed, nothing more."},{"heading":"Salsa: demand-driven incremental computation","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#salsa-demand-driven-incremental-computation","content":"[Salsa](<https://github.com/salsa-rs/salsa>) is a Rust framework developed by the rust-analyzer team (originally by Niko Matsakis). It implements demand-driven incremental computation with automatic dependency tracking.\n\nThe core model has three concepts:\n\n1. **Inputs**, mutable values you set from outside (file contents, configuration)\n2. **Tracked functions**, pure computations that read inputs and other tracked functions\n3. **The database**, holds all inputs, cached results, and dependency metadata\n\nThe following is a minimal example, a system that parses files and computes a symbol index:\n\n```rust\n#[salsa::input]\npub struct SourceFile {\n #[return_ref]\n pub name: String,\n #[return_ref]\n pub contents: String,\n}\n\n#[salsa::tracked]\npub struct ParsedFile<'db> {\n #[return_ref]\n pub ast: Ast,\n #[return_ref]\n pub errors: Vec<ParseError>,\n}\n\n#[salsa::tracked]\nfn parse_file<'db>(db: &'db dyn Db, file: SourceFile) -> ParsedFile<'db> {\n let contents = file.contents(db);\n let (ast, errors) = parser::parse(&contents);\n ParsedFile::new(db, ast, errors)\n}\n\n#[salsa::tracked]\nfn file_symbols<'db>(db: &'db dyn Db, file: SourceFile) -> Vec<Symbol> {\n let parsed = parse_file(db, file);\n let ast = parsed.ast(db);\n extract_symbols(ast)\n}\n\n#[salsa::tracked]\nfn all_symbols<'db>(db: &'db dyn Db, files: &[SourceFile]) -> SymbolIndex {\n let mut index = SymbolIndex::new();\n for &file in files {\n for symbol in file_symbols(db, file) {\n index.insert(symbol);\n }\n }\n index\n}\n```\n\nWhen you call ` all_symbols `, Salsa executes the full computation and records that ` all_symbols ` depends on ` file_symbols ` for each file, which depends on ` parse_file `, which depends on the file's ` contents ` input.\n\nNow change one file's contents. On the next call to ` all_symbols `, Salsa:\n\n1. Notices the ` contents ` input changed for one file\n2. Checks if ` parse_file `'s result actually changed (maybe you only changed a comment)\n3. If the parse result changed, checks if ` file_symbols ` changed\n4. Only recomputes ` all_symbols ` if the actual symbol list for that file changed\n\nThis is the key insight: **early cutoff**. Even if an input changes, if the intermediate result is the same as before, downstream computations don't need to re-run. In practice, this means changing a function body doesn't invalidate the symbol index, the symbols didn't change."},{"heading":"The revision counter","level":3,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#the-revision-counter","content":"Internally, Salsa uses a global revision counter. Every time an input changes, the revision increments. Each cached value stores two things:\n\n- **verified\\_at**: the last revision where this value was confirmed valid\n- **changed\\_at**: the last revision where this value actually changed\n\nWhen you access a cached value, Salsa checks if ` verified_at == current_revision `. If not, it walks the dependency graph backward to see if any input that this value (transitively) depends on has ` changed_at > verified_at `. If no dependency changed, the value is still valid, update ` verified_at ` and return the cached result without recomputing.\n\n```\nRevision 1: Initial computation\n file.contents changed_at=1\n parse_file changed_at=1 verified_at=1\n file_symbols changed_at=1 verified_at=1\n all_symbols changed_at=1 verified_at=1\n\nRevision 2: Edit file A (comment only)\n file_A.contents changed_at=2\n parse_file(A) → recompute → AST identical → changed_at=1 verified_at=2\n file_symbols(A) → dependency unchanged → changed_at=1 verified_at=2\n all_symbols → dependency unchanged → changed_at=1 verified_at=2\n // Total work: re-parsed one file. Everything else was free.\n\nRevision 3: Edit file B (add a function)\n file_B.contents changed_at=3\n parse_file(B) → recompute → AST different → changed_at=3 verified_at=3\n file_symbols(B) → recompute → new symbol → changed_at=3 verified_at=3\n all_symbols → recompute (one dep changed) → changed_at=3 verified_at=3\n // parse_file(A), file_symbols(A) untouched, still verified_at=2, valid.\n```"},{"heading":"Cycle handling","level":3,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#cycle-handling","content":"Compilers often have circular dependencies, a trait implementation might reference a type that references the trait. Salsa handles cycles explicitly:\n\n```rust\n#[salsa::tracked(cycle_fn=recover_type_cycle)]\nfn infer_type<'db>(db: &'db dyn Db, expr: ExprId) -> Type {\n // ... may call infer_type on other expressions\n}\n\nfn recover_type_cycle<'db>(\n db: &'db dyn Db,\n _cycle: &salsa::Cycle,\n expr: ExprId,\n) -> Type {\n Type::Error // Break the cycle with an error type\n}\n```\n\nWhen Salsa detects a cycle (the same query is called recursively with the same key), it invokes the recovery function. The recovery function determines the behavior, error types, fixed-point iteration, or any domain-appropriate strategy."},{"heading":"Adapton: the academic foundation","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#adapton-the-academic-foundation","content":"[Adapton](<http://adapton.org/>) predates Salsa and provides the theoretical framework. The core paper (Hammer et al., 2014) introduces \"demanded computation graphs\", a model where computations are nodes, data dependencies are edges, and the system lazily propagates changes through the graph.\n\nThe key contribution is the distinction between two strategies:\n\n**From-scratch consistency**: verify the entire graph is consistent before returning any result. Expensive upfront but guarantees correctness.\n\n**Demanded consistency**: only verify the portions of the graph that the current query touches. Lazy, efficient, but requires careful ordering.\n\nSalsa uses demanded consistency. When you query ` all_symbols `, it doesn't verify every cached value in the system, only the ones in the dependency chain of ` all_symbols `.\n\nThe following is a simplified Adapton-style implementation in TypeScript to make the concepts concrete:\n\n```typescript\ntype CellId = number;\ntype ThunkId = number;\n\nlet currentRevision = 0;\nlet activeThunk: ThunkId | null = null;\n\ninterface Cell<T> {\n value: T;\n changedAt: number;\n dependents: Set<ThunkId>;\n}\n\ninterface Thunk<T> {\n fn: () => T;\n value: T | undefined;\n verifiedAt: number;\n changedAt: number;\n dependencies: Set<CellId | ThunkId>;\n dependents: Set<ThunkId>;\n}\n\nconst cells = new Map<CellId, Cell<any>>();\nconst thunks = new Map<ThunkId, Thunk<any>>();\n\nfunction createCell<T>(value: T): CellId {\n const id = cells.size;\n cells.set(id, { value, changedAt: currentRevision, dependents: new Set() });\n return id;\n}\n\nfunction setCell<T>(id: CellId, value: T): void {\n const cell = cells.get(id)!;\n if (Object.is(cell.value, value)) return; // Early cutoff\n currentRevision++;\n cell.value = value;\n cell.changedAt = currentRevision;\n}\n\nfunction readCell<T>(id: CellId): T {\n const cell = cells.get(id)!;\n if (activeThunk !== null) {\n const thunk = thunks.get(activeThunk)!;\n thunk.dependencies.add(id);\n cell.dependents.add(activeThunk);\n }\n return cell.value;\n}\n\nfunction createThunk<T>(fn: () => T): ThunkId {\n const id = thunks.size + 10000; // Offset to distinguish from cells\n thunks.set(id, {\n fn,\n value: undefined,\n verifiedAt: -1,\n changedAt: -1,\n dependencies: new Set(),\n dependents: new Set(),\n });\n return id;\n}\n\nfunction queryThunk<T>(id: ThunkId): T {\n const thunk = thunks.get(id)!;\n\n if (thunk.verifiedAt === currentRevision) {\n return thunk.value!; // Already verified this revision\n }\n\n // Check if any dependency changed since last verification\n let needsRecompute = thunk.value === undefined;\n if (!needsRecompute) {\n for (const dep of thunk.dependencies) {\n if (dep < 10000) {\n // Cell dependency\n const cell = cells.get(dep)!;\n if (cell.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n } else {\n // Thunk dependency, recursively verify\n queryThunk(dep);\n const depThunk = thunks.get(dep)!;\n if (depThunk.changedAt > thunk.verifiedAt) {\n needsRecompute = true;\n break;\n }\n }\n }\n }\n\n if (needsRecompute) {\n thunk.dependencies.clear();\n const prevActive = activeThunk;\n activeThunk = id;\n const newValue = thunk.fn();\n activeThunk = prevActive;\n\n if (!Object.is(newValue, thunk.value)) {\n thunk.changedAt = currentRevision;\n thunk.value = newValue;\n }\n }\n\n thunk.verifiedAt = currentRevision;\n return thunk.value!;\n}\n```\n\nThis is ~80 lines that capture the essential algorithm. Real frameworks add garbage collection of unused cache entries, parallel execution, durability, and proper cycle detection, but the core idea is dependency tracking plus early cutoff."},{"heading":"Applying this outside compilers","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#applying-this-outside-compilers","content":"Incremental computation patterns apply effectively in non-compiler contexts. Consider a dashboard that aggregates data from 15 microservices, as a representative example. Each service updates at different frequencies, some every second, some every hour. The dashboard computes derived metrics from combinations of these sources.\n\nBefore: every render fetches all 15 sources and recomputes all metrics. Latency: 800ms.\n\nAfter: modeling each data source as an input cell, each metric as a tracked computation, and using a lightweight Adapton-style framework. On most renders, only 1-2 sources have changed, so only the affected metrics recompute. Latency drops to 30-80ms depending on what changed.\n\nThe pattern works anywhere you have: slowly-changing inputs, expensive derived computations, and repeated queries. Build systems (Bazel's action graph), reactive UI frameworks (Solid.js signals), spreadsheet engines, they all use variations of the same idea."},{"heading":"Practical takeaways","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#practical-takeaways","content":"For any system that repeatedly derives data from changing inputs, it is worth considering whether incremental computation fits. The upfront investment in modeling computation as a query graph pays dividends when the dataset grows. Salsa is a natural starting point for Rust projects, and the concepts port to other languages (the TypeScript sketch above serves as a starting point). The mental model, inputs, tracked functions, dependency graphs, early cutoff, is the valuable part, not any specific framework.\n\nAs a concrete example, restructuring a language server around proper Salsa query boundaries brought autocompletion from 400ms down to 8ms. The computation was the same. The only difference was defining where the query boundaries were."},{"heading":"Frequently asked questions","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#questions","content":""},{"heading":"What is incremental computation?","level":3,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#question-1","content":"Incremental computation is a programming paradigm where computations automatically reuse previous results when inputs change partially. Instead of recomputing everything from scratch, the system identifies what changed and only recomputes the affected portions of the result."},{"heading":"How does Salsa differ from regular memoization?","level":3,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#question-2","content":"Regular memoization caches results by input but has no invalidation mechanism, stale entries persist forever. Salsa tracks dependencies between computations, so when an input changes, it knows exactly which cached results are affected and invalidates only those, while preserving all unaffected cached results."},{"heading":"Can I use Salsa outside of compiler tooling?","level":3,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#question-3","content":"Yes, Salsa is a general-purpose incremental computation framework. Any workload where you repeatedly compute derived data from slowly-changing inputs benefits, build systems, data pipelines, reactive UIs, and configuration management are all viable use cases."},{"heading":"Tips","level":2,"url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#article-tips","content":"1. **Tip:** When modeling incremental computations, make your input granularity match your invalidation granularity. If you track changes per-file but compute per-function, you'll recompute entire files when a single function changes, defeating the purpose.\n2. **Tip:** Always separate ` reads ` from ` writes ` in your query definitions. A query that both reads and mutates state breaks the incremental DAG and forces full recomputation downstream.\n3. **Tip:** Use ` salsa::accumulator ` for diagnostics collection instead of side-effecting during queries. Accumulators integrate cleanly with the dependency graph and get invalidated properly.\n4. **Tip:** Profile query hit rates with ` salsa::DebugQueryTable ` before optimizing. Teams have been observed spending weeks optimizing a query that was already 99.8% cached, the bottleneck was somewhere else entirely.\n\n- incremental-computation\n- salsa\n- adapton\n- rust-analyzer\n- compilers\n- ide\n- demand-driven\n- memoization\n\n[Back to Blog](<https://lucioduran.com/blog>)"}],"links":[{"text":"Español","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks"},{"text":"Salsa","url":"https://github.com/salsa-rs/salsa"},{"text":"Adapton","url":"http://adapton.org/"},{"text":"Back to Blog","url":"https://lucioduran.com/blog"}],"structuredData":[{"@context":"https://schema.org","@graph":[{"@type":"BlogPosting","@id":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks#article","mainEntityOfPage":{"@type":"WebPage","@id":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks"},"headline":"Incremental Computation with Salsa and Adapton Frameworks","description":"An examination of demand-driven incremental computation as implemented in rust-analyzer, memoization with automatic invalidation, dependency tracking, and the architectural patterns that enable sub-millisecond IDE responses.","datePublished":"2025-10-06","author":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"publisher":{"@type":"Person","@id":"https://lucioduran.com/#person","name":"Lucio Durán","url":"https://lucioduran.com"},"image":"https://lucioduran.com/favicons/og-default.png","url":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks","inLanguage":"en","isPartOf":{"@id":"https://lucioduran.com/blog#blog"},"articleSection":"Compilers","keywords":"incremental-computation, salsa, adapton, rust-analyzer, compilers, ide, demand-driven, memoization","wordCount":1494,"timeRequired":"PT8M","workTranslation":[{"@type":"BlogPosting","inLanguage":"es","url":"https://lucioduran.com/es/blog/incremental-computation-salsa-adapton-frameworks"}]},{"@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https://lucioduran.com/"},{"@type":"ListItem","position":2,"name":"Blog","item":"https://lucioduran.com/blog"},{"@type":"ListItem","position":3,"name":"Incremental Computation with Salsa and Adapton Frameworks","item":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks"}]},{"@type":"FAQPage","mainEntity":[{"@type":"Question","name":"What is incremental computation?","acceptedAnswer":{"@type":"Answer","text":"Incremental computation is a programming paradigm where computations automatically reuse previous results when inputs change partially. Instead of recomputing everything from scratch, the system identifies what changed and only recomputes the affected portions of the result."}},{"@type":"Question","name":"How does Salsa differ from regular memoization?","acceptedAnswer":{"@type":"Answer","text":"Regular memoization caches results by input but has no invalidation mechanism, stale entries persist forever. Salsa tracks dependencies between computations, so when an input changes, it knows exactly which cached results are affected and invalidates only those, while preserving all unaffected cached results."}},{"@type":"Question","name":"Can I use Salsa outside of compiler tooling?","acceptedAnswer":{"@type":"Answer","text":"Yes, Salsa is a general-purpose incremental computation framework. Any workload where you repeatedly compute derived data from slowly-changing inputs benefits, build systems, data pipelines, reactive UIs, and configuration management are all viable use cases."}}]}]}],"contentHash":"1ab7e469c31a01a6e98600672bf480d471ecf7ce390fad57f50f129a3f4eeb7a","provenance":{"method":"Deterministic serialization of the server-rendered main element","source":"https://lucioduran.com/blog/incremental-computation-salsa-adapton-frameworks","hashAlgorithm":"sha256","hashScope":"JSON document fields before contentHash and provenance","sourceHTMLBytes":64386,"contentBytes":13544}}
