Skip to main content

Crate drv

Crate drv 

Source
Expand description

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 HashMap probe. 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 Clone of the output.
  • O(1) cache-hit check for Arc<T> and imbl collections. 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 writeNeeds #[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: &MyStructPlain 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>()
}

§Performance

Two sources of per-call work:

  1. Per-field equality check on cache-hit lookup.
  2. Output Clone on 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.

TypeCloneCache 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 via Hash of arguments, backed by HashMap under 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.

Traits§

ToStatic
Convert a memo input to its 'static cache-storage form.

Attribute Macros§

memo
Memoize a function.

Derive Macros§

Input
Declare a struct as a memo input.