Releases: jamesgober/query-lang
Release list
v1.0.0 — Stable API
query-lang v1.0.0 — API Freeze
The surface is now stable. v1.0.0 freezes the public API introduced in 0.2.0 under Semantic Versioning. There are no code changes from 0.2.0 — this release marks the contract as fixed and completes the roadmap.
What is query-lang?
An incremental computation engine — the model behind Rust's own salsa and rust-analyzer, distilled to a small, dependency-free core. You describe a set of queries once; the engine stores the base facts (inputs), caches the computed results (derived queries), records what each result was read from, and recomputes only what a change actually affects. The whole public surface is one trait and four types — System, Database, Revision, Stats, and QueryError. It is #![forbid(unsafe_code)], no_std-compatible, wires no first-party dependency, and never panics on a query cycle.
The stability promise
As of 1.0.0 the public API is frozen. Within the 1.x series:
System(its associated types andcompute),Database(new/set/get/revision/stats/system),Revision,Stats, andQueryErrorwill not change in a breaking way. A breaking change means a new major version.- The resolution semantics are part of the contract, not an implementation detail: a
setwith an unchanged value does not advance the revision or invalidate dependents; a derived query recomputes only on a real miss or a changed dependency; and early cutoff holds — when a recomputed value equals its predecessor, dependents are validated rather than recomputed. Code may rely on these, and onStatsreflecting them. QueryErroris#[non_exhaustive], so a newly distinguished failure is an additive minor change. Match it with a wildcard arm.- The
serderepresentations are fixed within1.x: aRevisionserializes as its underlying integer, andStatsas an object withcomputed/validated/hitsfields. - MSRV (Rust 1.85) is a compatibility surface: raising it is a documented minor change, never a patch.
Not promised: the concrete Revision numbering (only its order and monotonicity are contractual), the internal cache representation, and the exact Debug output of a Database.
Full details in docs/API.md.
Since 0.2.0
Nothing functional. 0.2.0 delivered the engine; 1.0.0 promises not to break it. If you are already on query-lang = "0.2", moving to "1" is a no-op recompile.
Performance
Unchanged from 0.2.0. Latest local Criterion means, Windows x86_64 and Linux (WSL2 Ubuntu), Rust stable, release build:
| 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 of a 256-wide sum (one branch recomputes, 255 validate). | ~42 µs |
The engine's own per-query overhead is a BTreeMap lookup and a revision compare — the figures are dominated by that bookkeeping, since the benchmarked queries do trivial work. Numbers vary by CPU; run cargo bench --bench bench on your target for a baseline.
Verification
Run on Windows x86_64 with testing on Linux (WSL2 Ubuntu), Rust stable and the 1.85 MSRV; the same commands run in the CI matrix across Linux, macOS, and Windows:
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --no-default-features --all-targets -- -D warnings
cargo test
cargo test --all-features
cargo build --no-default-features
cargo build --examples
cargo +1.85 build --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo deny check
cargo auditAll green. Counts at this tag:
- Default features: 28 unit + 6 integration + 4 property + 31 doctests. The doctests include every
rustexample inREADME.mdanddocs/API.md, wired intocargo testso the published examples cannot drift from the API. --all-features: adds 3serdeserialization tests exercising theRevisionandStatsderivations.
Installation
[dependencies]
query-lang = "1"
# With serde:
query-lang = { version = "1", features = ["serde"] }
# no_std:
query-lang = { version = "1", default-features = false }MSRV: Rust 1.85.
Documentation
Full diff: v0.2.0...v1.0.0.
Changelog: CHANGELOG.md.
v0.2.0 — Foundation
query-lang v0.2.0 — The Incremental Engine
The core, and the hard part of the roadmap. v0.2.0 turns the scaffold into a
working incremental computation engine: a query database that caches derived
results, records what each result was computed from, and recomputes only what a
change actually affects. The surface is deliberately small — one trait, one
database, and three supporting types — and it is generic over the queries a
consumer defines, so the engine binds to no concrete compiler and wires no
first-party dependency.
What is query-lang?
An incremental computation engine, the model behind Rust's own salsa and
rust-analyzer, distilled to a small dependency-free core. A compiler runs the
same work over and over as source is edited — parse, resolve names, type-check,
lower to IR — and most edits touch only a fraction of it. The query model turns
each step into a memoized function whose dependencies are tracked as it runs;
when an input changes, the engine invalidates only the queries that transitively
read it and reuses everything else. query-lang is that engine and nothing more —
it owns no compiler, no IR, no syntax. It is one of the FEAT-tier crates of the
-lang language-construction family.
What's new in 0.2.0
System — the query definition
A consumer implements System once to describe an entire incremental
computation: a Key type that names a query, a Value type it produces, and a
compute function that derives one from the other. Reading a dependency through
the database handle is what the engine records as the dependency graph — nothing
is declared up front, so a query that branches on its inputs depends only on the
branch it actually took.
use query_lang::{Database, System, QueryError};
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
enum Key { Source, Parsed, Squared }
struct Pipeline;
impl System for Pipeline {
type Key = Key;
type Value = i64;
fn compute(&self, db: &Database<Self>, key: &Key) -> Result<i64, QueryError> {
match key {
Key::Source => Ok(0),
Key::Parsed => Ok(db.get(&Key::Source)?),
Key::Squared => { let n = db.get(&Key::Parsed)?; Ok(n * n) }
}
}
}Database<S> — the engine
The database stores inputs and caches derived results. set seeds an input and
get resolves a query, computing and caching it as needed. Every resolution
takes one of three paths — a hit (already current), a validation (stale
but no input actually changed, so the cached value is reused), or a
recomputation — and only the last runs compute.
# use query_lang::{Database, System, QueryError};
# #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)]
# enum Key { Source, Parsed, Squared }
# struct Pipeline;
# impl System for Pipeline {
# type Key = Key;
# type Value = i64;
# fn compute(&self, db: &Database<Self>, key: &Key) -> Result<i64, QueryError> {
# match key {
# Key::Source => Ok(0),
# Key::Parsed => Ok(db.get(&Key::Source)?),
# Key::Squared => { let n = db.get(&Key::Parsed)?; Ok(n * n) }
# }
# }
# }
let mut db = Database::new(Pipeline);
db.set(Key::Source, 12);
assert_eq!(db.get(&Key::Squared)?, 144);
assert_eq!(db.stats().computed, 2); // Source is an input; Parsed and Squared ran
assert_eq!(db.get(&Key::Squared)?, 144);
assert_eq!(db.stats().hits, 1); // no edit — a free hit
# Ok::<(), QueryError>(())Early cutoff — the property that pays for the engine
When a query recomputes to the same value it already had, its change stamp stays
old, so queries that depend on it are validated rather than recomputed. A local
edit that leaves an intermediate result unchanged stops there instead of
cascading through the whole graph. The build_pipeline example makes this
concrete: reformatting source reruns the tokenizer, sees the same tokens, and
reuses the symbol count and the report.
Revision and Stats — validation and observability
Revision is the monotonic clock that advances on every real input change;
validity is decided by comparing revisions, a single integer compare regardless
of how large a cached value is. Stats counts the three resolution paths
(computed, validated, hits) so a caller can measure exactly what an
operation cost — and a test can assert that an edit recomputed only what it
should.
QueryError — contained failure
A query graph must be acyclic. When it is not, the query that closes the cycle
resolves to QueryError::Cycle; the resolution chain unwinds cleanly and the
database stays usable, never a panic and never an unbounded recursion. The type
is #[non_exhaustive], so new resolution failure modes can be added later
without breaking a match.
Examples, benchmarks, and property tests
Two runnable examples ship: spreadsheet (cells as inputs, formulas as derived
queries, edits recomputing only affected formulas) and build_pipeline (a
miniature source → tokens → symbol count → report front end demonstrating early
cutoff). Criterion benchmarks cover the hit, cold-build, and recompute paths.
Property tests hold the engine to its core invariants: an incrementally
maintained result always equals a from-scratch computation, the revision is
monotonic, and an unchanged input never recomputes.
Dependency wiring — none, by design
No first-party dependency is wired, and none is planned. A query engine is a
generic caching and invalidation machine; coupling it to one concrete IR would
bind it to a single layer and pull in a dependency it never reads. Keys live in a
BTreeMap, so the only requirement is Key: Ord — no hashing dependency, and
the crate stays no_std-compatible on alloc alone.
Breaking changes
None. v0.1.0 exposed no public API; everything in 0.2.0 is new surface.
Verification
Run on Windows x86_64 and Linux (WSL2 Ubuntu), Rust stable and the 1.85 MSRV;
identical commands run in the configured CI matrix:
cargo fmt --all -- --check
cargo clippy --all-targets -- -D warnings
cargo clippy --all-targets --all-features -- -D warnings
cargo clippy --no-default-features --all-targets -- -D warnings
cargo test
cargo test --all-features
RUSTDOCFLAGS="-D warnings" cargo doc --no-deps --all-features
cargo build --examples
cargo build --no-default-features
cargo +1.85 build --all-features
cargo deny check
cargo auditAll green. Counts at this tag:
- Default features: 28 unit + 6 integration + 4 property + 31 doctests. The
doctests include everyrustexample inREADME.mdanddocs/API.md, wired
intocargo testso the published examples cannot drift from the API. --all-features: adds 3serdeserialization tests exercising theRevision
andStatsderivations.
Benchmark baselines (release profile): re-resolving an already-current query is a
~19–38 ns cache hit; a first build of a 256-deep query chain is ~55 µs; editing
the leaf and rebuilding that chain is ~46 µs; editing one input of a 256-wide sum
recomputes one branch and validates the other 255 in ~42 µs. The engine's own
per-query overhead is a BTreeMap lookup and a revision compare — the figures
are dominated by that bookkeeping, since the benchmarked queries do trivial work.
What's next
- 1.0.0 — API freeze. Freeze the 0.2.0 surface as the 1.0 contract, mark
docs/API.mdstable with a SemVer promise, and confirm the full test and
benchmark suite on all three platforms. Interned-key helpers, durability tiers
(marking some inputs as rarely-changing to skip revalidation), and a
query-trace/debug hook are additive later-minor work, not a 1.0 requirement.
Installation
[dependencies]
query-lang = "0.2"MSRV: Rust 1.85.
Documentation
Full diff: v0.1.0...v0.2.0.
Changelog: CHANGELOG.md.