Skip to content

algesten/drv

Repository files navigation

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 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, 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:

  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.

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 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.

About

Derived, memoized values over plain Rust structs

Resources

License

Apache-2.0, MIT licenses found

Licenses found

Apache-2.0
LICENSE-APACHE
MIT
LICENSE-MIT

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages