The model
Three pieces, and the whole surface fits in your head:
- A
Systemis the definition of your queries: aKeytype that names a query, aValuetype it produces, and acomputefunction that derives one from the other. - A
Databaseholds theSystem, stores your inputs, and caches the derived results. You set inputs and get results; it handles caching, dependency tracking, and invalidation. - A
getresolves a query. From application code it returns a result; from inside acomputeit reads a dependency — and the engine records that edge automatically.
An input is any key whose value you set directly; every other key is derived through compute. One Key type names both, so a query reads an input and another query the same way.
Every resolution takes one of three paths, and the engine counts each in Stats:
| Path | When | Cost |
|---|---|---|
| Hit | The query was already verified at the current revision. | A revision compare; the cached value is returned without touching dependencies. |
| Validated | The query is stale, but no dependency actually changed its inputs. | The dependencies are re-examined; the cached value is reused. This is early cutoff. |
| Computed | A genuine miss, or a dependency that truly changed. | compute runs and the new value is cached. |
Early cutoff is the property that makes the engine worth its complexity: when a recomputed query produces the same value it had before, queries that depend on it are validated rather than recomputed, so a local edit does not cascade through the whole graph.
Installation
[]
= "1"
Or from the terminal:
MSRV: Rust 1.85 (Rust 2024 edition).
Quick start
An input, a query that parses it, and a query that squares the result. Editing the input recomputes the chain; asking again with no edit is a free hit.
use ;
;
let mut db = new;
db.set;
assert_eq!;
assert_eq!; // Source is a set input; Parsed and Squared ran
// Ask again with no edit: a cache hit, nothing recomputes.
assert_eq!;
assert_eq!;
# Ok::
Values that are expensive to clone
The engine clones a value to hand it back and compares old against new for early cutoff. Wrap a large result in an Arc so a clone bumps a refcount rather than copying, and the comparison stays cheap:
use Arc;
use ;
;
let db = new;
let a = db.get?;
let b = db.get?; // second call is a hit
assert!; // and hands back the very same allocation
# Ok::
Examples
Two runnable examples ship in examples/:
- Spreadsheet — cells are inputs, formulas are derived queries; editing a cell recomputes only the formulas that transitively read it.
- Build pipeline — a miniature compiler front end (
source → tokens → symbol count → report) that shows early cutoff: reformatting the source reruns only the tokenizer; the rest is reused.
Performance
The engine's own per-query overhead is a BTreeMap lookup and a revision compare. The benchmarks in benches/ exercise the three resolution paths directly (Windows x86_64 and Linux/WSL2, Rust stable, release profile):
| Benchmark | What it measures | Time |
|---|---|---|
chain/cache_hit |
Re-resolving an already-current query (a hit). | ~19–38 ns |
chain/cold_build/256 |
First resolution of a 256-deep query chain. | ~55 µs |
chain/edit_rebuild/256 |
Editing the leaf input, rebuilding a 256-deep chain. | ~46 µs |
wide/edit_one_of/256 |
Editing one input in a 256-wide sum (one branch recomputes, 255 validate). | ~42 µs |
Run them yourself:
Criterion writes per-benchmark reports to target/criterion/. Numbers vary by CPU; use the trend across runs, not a single absolute.
Design notes
- Dependencies are recorded, not declared. A query's dependency set is whatever it read on its last run, so a query that branches on its inputs is tracked exactly — it depends on the branch it actually took, and nothing more.
- Revisions, not value diffs, drive validation. Validity is a single integer compare regardless of how large a cached value is; values are compared only once, at the point a query recomputes, to decide early cutoff.
- Single-threaded by design. Resolution walks a shared cache and a dependency stack through interior mutability — correct and allocation-light on one thread, with no atomic overhead. Run independent databases on separate threads for parallelism.
- Cycles are an error, never a panic or a hang. A query that depends on itself resolves to
QueryError::Cycle; the resolution chain unwinds cleanly and the database stays usable. no_stdand dependency-free. The engine uses onlyallocand wires no first-party crate — keys live in aBTreeMap, so the requirement isKey: Ordrather than a hashing dependency.
Testing
The suite runs on Windows, Linux (WSL2 Ubuntu), and macOS through the CI matrix, on stable and the 1.85 MSRV:
Property tests in tests/proptests.rs hold the engine to its core invariants over a wide space of edit sequences: an incrementally maintained result always equals a from-scratch computation, the revision is monotonic, and an unchanged input never recomputes. Every rust example in this README and in docs/API.md is compiled and run as a doctest, so the published examples cannot drift from the API.
Cross-platform support
- Linux (x86_64, aarch64)
- macOS (x86_64, Apple Silicon)
- Windows (x86_64)
The engine uses no operating-system facilities and no platform-specific code; behaviour is identical on every target.
Contributing
See dev/DIRECTIVES.md for engineering standards and the definition of done. Before a PR: cargo fmt --all, cargo clippy --all-targets --all-features -- -D warnings, and cargo test --all-features must be clean.