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 workThe 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.
- 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.
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.
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.
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 hitOnly 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.
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.
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"] }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.
Dual-licensed under MIT or Apache-2.0, at your option.