drv
[!WARNING] Vibe coded. I've focused on the overall architecture rather than reviewing the code output in detail. For projects I've written by hand, see ureq and str0m.
Memoize a function with #[drv::memo]. The attribute is liberal about
parameter types — owned values, references, struct refs, &str,
&[u8], #[derive(drv::Input)] projections — and caches results in a
per-memo thread-local slot array keyed by value equality.
let c = Config ;
assert_eq!; // computes
assert_eq!; // cache hit, no work
The companion derive #[derive(drv::Input)] is a helper for one
specific situation: when you want to cache on a subset of a source
struct's fields without cloning the whole struct on every call. See
Zero-copy projections.
Why drv
- Equality-keyed, not hash-keyed. No hashing on the hot path, no
HashMapprobe. Cache lookup is a linear scan through a fixed-shape slot array with per-field equality. - Thread-local caches. Every memo owns its own cache — single-writer, lock-free.
- Zero allocations on cache hit. A hit is an equality check plus a
Cloneof the output. - O(1) cache-hit check for
Arc<T>andimblcollections. A pointer-equality fast path skips deep comparison when the field's pointer hasn't changed since the last miss. - One concept for inputs.
#[derive(drv::Input)]is the single opt-in: any struct you want as a memo parameter derives it. Plain structs, borrowed projections, and nested-input bundles all work the same way.
Writing a memo
Every memo picks a cache strategy:
#[drv::memo(single)]— one slot, last-call caching. A hit requires today's inputs to equal the most recent recompute's inputs.#[drv::memo(lru = N)]— N slots, least-recently-used eviction. For inputs that cycle between a small number of recurring states.
Parameters
Every parameter must implement [ToStatic]. That trait is implemented
by drv for primitives, String, Vec<T>, HashMap, Arc<T>, imbl
collections (feature-gated), Option<T>, tuples, and — via a
reference blanket — &T for any T: ToStatic. User types become
inputs by adding #[derive(drv::Input)].
| You write | Needs #[derive(drv::Input)] |
Notes |
|---|---|---|
x: T (primitive, std type) |
Shipped impl. | |
x: &str, x: &[u8] |
Reference blanket. | |
x: Arc<T> |
ptr_eq fast path. | |
x: MyInput<'a> |
✅ | Borrowed projection. |
x: &MyStruct |
✅ | Plain owned struct. |
Bodies see the exact type you declared: strip #[drv::memo] and the
function still compiles.
Zero-copy projections with #[derive(drv::Input)]
Take the previous Config example. If Config grows a big
Vec<Worker> field that worker_count doesn't read, the default
&Config form still snapshots the whole struct into the cache slot
and every cache-hit check compares the whole thing.
#[derive(drv::Input)] lets you declare a lightweight view that
borrows only the fields the memo actually depends on. The derive
auto-generates the owned snapshot and the machinery #[drv::memo]
uses internally:
let mut game = Scoreboard ;
assert_eq!; // computes
game.player_x = 42; // not in TotalInput
assert_eq!; // cache hit
Only hits enters the cache key. Changes to player_x don't
invalidate; changes to hits do. The projection is whatever code you
write — a ::new method, a From<&Source> impl, or an inline struct
literal at the call site. drv doesn't prescribe one.
Nested inputs
A #[derive(drv::Input)] struct can have another
#[derive(drv::Input)] struct as a field — useful for bundling a
handful of sub-projections into one memo parameter:
Performance
Two sources of per-call work:
- Per-field equality check on cache-hit lookup.
- Output
Cloneon every return.
drv's ToStatic impls for Arc<T> and (under the imbl feature)
imbl's persistent collections take a pointer-equality fast path —
O(1) when the field hasn't been mutated since the last miss.
| Type | Clone |
Cache hit (same pointer) | Cache hit (equal contents) | Mutation |
|---|---|---|---|---|
Vec<T> |
O(n) | O(n) | O(n) | O(1) amortised |
HashMap<K, V> |
O(n) | O(n) | O(n) | O(1) amortised |
Arc<T> |
O(1) | O(1) | O(eq of T) | n/a |
imbl::Vector<T> (imbl feature) |
O(1) | O(1) | O(n) | O(log n) |
imbl::HashMap<K, V> (imbl feature) |
O(1) | O(1) | O(n) | O(log n) |
Rule of thumb: scalars are free; small Vec / String is fine; for
collections with more than a handful of elements, wrap in Arc<T> or
reach for imbl.
Enable imbl:
[]
= { = "0.4", = ["imbl"] }
Comparison
Ranked from most to least alike.
comemo— closest in spirit. Memoises functions with fine-grained dependency tracking via runtime access recording (#[track]). drv's static input struct is cheaper per call but asks you to declare dependencies up front rather than discovering them at runtime.salsa— incremental-computation database used by rust-analyzer. Tracks a dependency graph across queries; much more powerful than drv for deeply chained derivations, and much heavier.cached/memoize— general-purpose memoisation viaHashof arguments, backed byHashMapunder a lock. Work for any hashable input; drv skips hashing entirely and trades generality for hot-path speed and field-level invalidation.moka/quick_cache/stretto— concurrent in-memory cache data structures (Caffeine / Ristretto ports). Not memoisation crates — they're backing stores you'd build a cache on top of. drv's thread-local single-writer model is the opposite design choice.
License
Dual-licensed under MIT or Apache-2.0, at your option.