6 releases
| 0.4.2 | Apr 24, 2026 |
|---|---|
| 0.4.1 | Apr 24, 2026 |
| 0.3.1 | Apr 19, 2026 |
| 0.1.0 | Apr 15, 2026 |
#79 in Caching
150 downloads per month
44KB
404 lines
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.
#[derive(drv::Input)]
pub struct Config {
pub threads: u32,
pub timeout_ms: u64,
}
#[drv::memo(single)]
fn worker_count(c: &Config) -> u32 {
c.threads * 2
}
let c = Config { threads: 4, timeout_ms: 5000 };
assert_eq!(worker_count(&c), 8); // computes
assert_eq!(worker_count(&c), 8); // 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, PathBuf, Arc<T>,
std::time::{Instant, Duration, SystemTime}, imbl collections
(feature-gated), and — via a reference blanket — &T for any
T: ToStatic. Containers (Vec, Option, tuples, arrays,
HashMap / HashSet / BTreeMap / BTreeSet) are recursive:
they implement ToStatic whenever their elements do, so nested
projections like Vec<MyInput<'a>> work without any extra
annotation. User types become inputs by adding
#[derive(drv::Input)] — supported on structs (named, tuple, unit)
and enums.
| 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:
#[derive(drv::Input)]
struct TotalInput<'a> {
pub hits: &'a Vec<u32>,
}
impl<'a> TotalInput<'a> {
pub fn new(s: &'a Scoreboard) -> Self { Self { hits: &s.hits } }
}
#[drv::memo(single)]
fn total_score<'a>(input: TotalInput<'a>) -> u32 {
input.hits.iter().sum()
}
let mut game = Scoreboard { hits: vec![100, 250, 50], player_x: 0 };
assert_eq!(total_score(TotalInput::new(&game)), 400); // computes
game.player_x = 42; // not in TotalInput
assert_eq!(total_score(TotalInput::new(&game)), 400); // 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:
#[derive(drv::Input)]
struct ChildA<'a> { pub a: &'a Vec<u32> }
#[derive(drv::Input)]
struct ChildB<'a> { pub b: &'a Vec<u32> }
#[derive(drv::Input)]
struct Both<'a> {
pub ca: ChildA<'a>,
pub cb: ChildB<'a>,
}
#[drv::memo(single)]
fn sum_both<'a>(input: Both<'a>) -> u32 {
input.ca.a.iter().sum::<u32>() + input.cb.b.iter().sum::<u32>()
}
Vec, Option, tuples, HashMap / BTreeMap values, and arrays
are all recursive — a field type like Vec<MyInput<'a>> or
HashMap<String, MyInput<'a>> works without any extra annotation.
The derive handles generic type parameters, tuple structs, and unit
structs as well.
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:
[dependencies]
drv = { version = "0.4", features = ["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.
Dependencies
~0–0.9MB
~19K SLoC