Architecture
picante is intentionally layered:
- Runtime (
Runtime): owns the globalRevisioncounter, dependency graphs, and event channels. - Execution frames (
frame): tokio task-local stack frames that record dependencies and detect cycles. - Ingredients: storage and logic for each query kind (input, derived, interned).
Revisions and change tracking
picante uses a monotonically increasing Revision as a logical clock. Each cached value tracks two revisions:
verified_at: when we last checked if the value is still validchanged_at: when the value actually changed (may be older thanverified_at)
This distinction enables early cutoff: if a query recomputes and produces the same result, changed_at stays the same. Downstream queries see that their dep's changed_at hasn't advanced, so they don't need to recompute.
Dependency tracking
picante maintains both forward and reverse dependency graphs:
- Forward deps: each derived query records which keys it read during computation
- Reverse deps: maps each key to the set of queries that depend on it
When an input changes, propagate_invalidation() walks the reverse dep graph to find all affected queries. Only those queries need revalidation — everything else is untouched.
Derived queries (single-flight)
Each derived key maps to a "cell":
Vacant: never computedRunning: one task is computing itReady: value + deps + verified_at revisionPoisoned: previous compute failed or panicked at that revision
Waiters use a Notify + loop pattern: nobody holds locks while awaiting.
Compile-time optimization: trait object dispatch
The derived query state machine (~300 lines) is type-erased to avoid monomorphization bloat. Instead of being generic over each query's compute function type, it uses:
trait ErasedCompute<DB>: trait object for compute functionsTypedCompute<DB, K, V>: small adapter that implements the traiteq_erased: fn(&dyn Any, &dyn Any) -> bool: function pointer for deep equality
This means the state machine compiles once per database type (e.g., Database + DatabaseSnapshot) instead of once per query (50+ in a typical codebase).
Compile-time win:
- 96% reduction in LLVM IR (235k → 9k lines for state machine)
- 36% faster clean builds
Runtime trade-offs (deliberate, acceptable):
- One vtable dispatch per compute
- One
BoxFutureallocation per compute - Key decode (
Key→K) per compute - Deep equality check on recompute (uses
facet_assert::check_same)
These small runtime costs enable dramatic compile-time improvements without affecting correctness.
Persistence
picante can snapshot inputs and memoized derived values (including dependency lists) to a single on-disk file, encoded with facet-postcard.
This is conceptually similar to Salsa's Database::serialize/deserialize (which Dodeca previously stored as postcard), but picante uses facet instead of serde.