use alloc::boxed::Box;
use alloc::collections::BTreeMap;
use alloc::string::String;
use alloc::vec::Vec;
use core::any::Any;
use crate::nexus::effect::EffectMarker;
use crate::nexus::row::Row;
pub const INCREMENTAL_BIT: u128 = 1 << 32;
#[derive(Copy, Clone, Debug)]
pub struct IncrementalEffect;
impl EffectMarker for IncrementalEffect {
const BIT: u128 = INCREMENTAL_BIT;
const NAME: &'static str = "Incremental";
}
pub type IncrementalRow = Row<INCREMENTAL_BIT>;
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct InputId {
name: String,
generation: u64,
}
impl InputId {
pub fn new(name: impl Into<String>) -> Self {
InputId {
name: name.into(),
generation: 0,
}
}
pub fn name(&self) -> &str {
&self.name
}
pub fn generation(&self) -> u64 {
self.generation
}
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct MemoKey {
name: String,
}
impl MemoKey {
pub fn new(name: impl Into<String>) -> Self {
MemoKey { name: name.into() }
}
pub fn name(&self) -> &str {
&self.name
}
}
#[derive(Clone, Debug)]
struct Dependency {
input_id: InputId,
recorded_generation: u64,
}
impl Dependency {
fn is_valid(&self, current_generation: u64) -> bool {
self.recorded_generation == current_generation
}
}
struct CachedResult {
value: Box<dyn Any>,
dependencies: Vec<Dependency>,
}
pub struct IncrementalContext {
inputs: BTreeMap<InputId, Box<dyn Any>>,
generations: BTreeMap<InputId, u64>,
cache: BTreeMap<MemoKey, CachedResult>,
current_dependencies: Vec<Dependency>,
recording: bool,
stats: IncrementalStats,
}
#[derive(Clone, Debug, Default)]
pub struct IncrementalStats {
pub cache_hits: usize,
pub cache_misses: usize,
pub invalidations: usize,
}
impl IncrementalContext {
pub fn new() -> Self {
IncrementalContext {
inputs: BTreeMap::new(),
generations: BTreeMap::new(),
cache: BTreeMap::new(),
current_dependencies: Vec::with_capacity(8),
recording: false,
stats: IncrementalStats::default(),
}
}
pub fn create_input<T: Clone + 'static>(
&mut self,
name: impl Into<String>,
value: T,
) -> InputId {
let id = InputId::new(name);
self.inputs.insert(id.clone(), Box::new(value));
self.generations.insert(id.clone(), 0);
id
}
pub fn read_input<T: Clone + 'static>(&mut self, id: &InputId) -> Option<T> {
if self.recording {
let generation = self.generations.get(id).copied().unwrap_or(0);
self.current_dependencies.push(Dependency {
input_id: id.clone(),
recorded_generation: generation,
});
}
self.inputs
.get(id)
.and_then(|v| v.downcast_ref::<T>())
.cloned()
}
pub fn set_input<T: Clone + 'static>(&mut self, id: &InputId, value: T) {
self.inputs.insert(id.clone(), Box::new(value));
let generation = self.generations.entry(id.clone()).or_insert(0);
*generation += 1;
self.stats.invalidations += 1;
}
pub fn memo<T: Clone + 'static, F>(&mut self, key: impl Into<String>, compute: F) -> T
where
F: FnOnce(&mut Self) -> T,
{
let memo_key = MemoKey::new(key);
if let Some(cached) = self.cache.get(&memo_key) {
let all_valid = cached.dependencies.iter().all(|dep| {
self.generations
.get(&dep.input_id)
.is_some_and(|&g| dep.is_valid(g))
});
if all_valid && let Some(value) = cached.value.downcast_ref::<T>() {
self.stats.cache_hits += 1;
return value.clone();
}
}
self.stats.cache_misses += 1;
let was_recording = self.recording;
let old_deps = core::mem::take(&mut self.current_dependencies);
self.recording = true;
let result = compute(self);
self.recording = was_recording;
let new_deps = core::mem::replace(&mut self.current_dependencies, old_deps);
self.cache.insert(
memo_key,
CachedResult {
value: Box::new(result.clone()),
dependencies: new_deps,
},
);
result
}
pub fn invalidate(&mut self, id: &InputId) {
if let Some(generation) = self.generations.get_mut(id) {
*generation += 1;
self.stats.invalidations += 1;
}
}
pub fn clear_cache(&mut self) {
self.cache.clear();
}
pub fn stats(&self) -> &IncrementalStats {
&self.stats
}
pub fn reset_stats(&mut self) {
self.stats = IncrementalStats::default();
}
pub fn cache_size(&self) -> usize {
self.cache.len()
}
pub fn input_count(&self) -> usize {
self.inputs.len()
}
}
impl Default for IncrementalContext {
fn default() -> Self {
Self::new()
}
}
pub struct IncrementalComputation<A> {
compute: Box<dyn FnOnce(&mut IncrementalContext) -> A>,
}
impl<A: 'static> IncrementalComputation<A> {
#[inline]
pub fn new<F: FnOnce(&mut IncrementalContext) -> A + 'static>(f: F) -> Self {
IncrementalComputation {
compute: Box::new(f),
}
}
#[inline]
pub fn run(self, ctx: &mut IncrementalContext) -> A {
(self.compute)(ctx)
}
pub fn pure(value: A) -> Self
where
A: Clone,
{
IncrementalComputation::new(move |_| value)
}
pub fn map<B: 'static, F: FnOnce(A) -> B + 'static>(self, f: F) -> IncrementalComputation<B> {
IncrementalComputation::new(move |ctx| {
let a = (self.compute)(ctx);
f(a)
})
}
pub fn and_then<B: 'static, F: FnOnce(A) -> IncrementalComputation<B> + 'static>(
self,
f: F,
) -> IncrementalComputation<B> {
IncrementalComputation::new(move |ctx| {
let a = (self.compute)(ctx);
f(a).run(ctx)
})
}
}
pub fn read_input<T: Clone + 'static>(id: InputId) -> IncrementalComputation<Option<T>> {
IncrementalComputation::new(move |ctx| ctx.read_input::<T>(&id))
}
pub fn memo<T: Clone + 'static, F>(
key: impl Into<String> + 'static,
compute: F,
) -> IncrementalComputation<T>
where
F: FnOnce(&mut IncrementalContext) -> T + 'static,
{
let key_string = key.into();
IncrementalComputation::new(move |ctx| ctx.memo(key_string, compute))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_create_and_read_input() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 42);
let value = ctx.read_input::<i32>(&id);
assert_eq!(value, Some(42));
}
#[test]
fn test_set_input() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 42);
ctx.set_input(&id, 100);
let value = ctx.read_input::<i32>(&id);
assert_eq!(value, Some(100));
}
#[test]
fn test_memo_caches_result() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 10);
let result1 = ctx.memo("doubled", |ctx| {
let x = ctx
.read_input::<i32>(&id)
.expect("input 'x' was just registered in this context");
x * 2
});
assert_eq!(result1, 20);
assert_eq!(ctx.stats().cache_misses, 1);
assert_eq!(ctx.stats().cache_hits, 0);
let result2 = ctx.memo("doubled", |ctx| {
let x = ctx
.read_input::<i32>(&id)
.expect("input 'x' was just registered in this context");
x * 2
});
assert_eq!(result2, 20);
assert_eq!(ctx.stats().cache_misses, 1);
assert_eq!(ctx.stats().cache_hits, 1);
}
#[test]
fn test_invalidation_triggers_recompute() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 10);
let result1 = ctx.memo("doubled", |ctx| {
let x = ctx
.read_input::<i32>(&id)
.expect("input 'x' should exist and be i32");
x * 2
});
assert_eq!(result1, 20);
ctx.set_input(&id, 50);
let result2 = ctx.memo("doubled", |ctx| {
let x = ctx
.read_input::<i32>(&id)
.expect("input 'x' should exist and be i32 after update");
x * 2
});
assert_eq!(result2, 100);
assert_eq!(ctx.stats().cache_misses, 2); }
#[test]
fn test_independent_inputs() {
let mut ctx = IncrementalContext::new();
let x = ctx.create_input("x", 10);
let y = ctx.create_input("y", 5);
let result1 = ctx.memo("x_doubled", |ctx| {
let val = ctx
.read_input::<i32>(&x)
.expect("input 'x' should exist and be i32");
val * 2
});
assert_eq!(result1, 20);
ctx.set_input(&y, 100);
let result2 = ctx.memo("x_doubled", |ctx| {
let val = ctx
.read_input::<i32>(&x)
.expect("input 'x' should still exist after updating y");
val * 2
});
assert_eq!(result2, 20);
assert_eq!(ctx.stats().cache_hits, 1); }
#[test]
fn test_chained_dependencies() {
let mut ctx = IncrementalContext::new();
let x = ctx.create_input("x", 10);
let a = ctx.memo("a", |ctx| {
let val = ctx
.read_input::<i32>(&x)
.expect("input 'x' should be registered in context");
val + 1
});
assert_eq!(a, 11);
let b = ctx.memo("b", |ctx| {
let val = ctx
.read_input::<i32>(&x)
.expect("input 'x' should be registered in context");
(val + 1) * 2 });
assert_eq!(b, 22);
ctx.set_input(&x, 20);
let a2 = ctx.memo("a", |ctx| {
let val = ctx
.read_input::<i32>(&x)
.expect("input 'x' should remain registered after set_input");
val + 1
});
assert_eq!(a2, 21);
let b2 = ctx.memo("b", |ctx| {
let val = ctx
.read_input::<i32>(&x)
.expect("input 'x' should remain registered after set_input");
(val + 1) * 2
});
assert_eq!(b2, 42);
}
#[test]
fn test_clear_cache() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 42);
ctx.memo("test", |ctx| {
ctx.read_input::<i32>(&id)
.expect("input 'x' should be registered in test_clear_cache")
});
assert_eq!(ctx.cache_size(), 1);
ctx.clear_cache();
assert_eq!(ctx.cache_size(), 0);
}
#[test]
fn test_incremental_computation_type() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 5);
let comp = IncrementalComputation::new(move |ctx| {
ctx.read_input::<i32>(&id)
.expect("input 'x' was registered in this context")
* 3
});
let result = comp.run(&mut ctx);
assert_eq!(result, 15);
}
#[test]
fn test_incremental_computation_map() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 10);
let comp = IncrementalComputation::new(move |ctx| {
ctx.read_input::<i32>(&id)
.expect("input 'x' was registered and must be readable as i32")
})
.map(|x| x * 2)
.map(|x| x + 1);
let result = comp.run(&mut ctx);
assert_eq!(result, 21); }
#[test]
fn test_stats() {
let mut ctx = IncrementalContext::new();
let id = ctx.create_input("x", 1);
assert_eq!(ctx.stats().cache_hits, 0);
assert_eq!(ctx.stats().cache_misses, 0);
assert_eq!(ctx.stats().invalidations, 0);
ctx.memo("test", |ctx| ctx.read_input::<i32>(&id));
assert_eq!(ctx.stats().cache_misses, 1);
ctx.memo("test", |ctx| ctx.read_input::<i32>(&id));
assert_eq!(ctx.stats().cache_hits, 1);
ctx.set_input(&id, 2);
assert_eq!(ctx.stats().invalidations, 1);
ctx.reset_stats();
assert_eq!(ctx.stats().cache_hits, 0);
}
#[test]
fn test_recomputation_avoidance_benchmark() {
let mut ctx = IncrementalContext::new();
let x = ctx.create_input("x", 10);
let y = ctx.create_input("y", 20);
let z = ctx.create_input("z", 30);
let mut compute_count = 0;
let result1 = ctx.memo("expensive", |ctx| {
compute_count += 1;
let vx = ctx
.read_input::<i32>(&x)
.expect("input 'x' was created in this context");
let vy = ctx
.read_input::<i32>(&y)
.expect("input 'y' was created in this context");
let vz = ctx
.read_input::<i32>(&z)
.expect("input 'z' was created in this context");
vx * 100 + vy * 10 + vz
});
assert_eq!(result1, 1230); assert_eq!(compute_count, 1);
for _ in 0..10 {
let _ = ctx.memo("expensive", |ctx| {
compute_count += 1;
let vx = ctx
.read_input::<i32>(&x)
.expect("input 'x' was created in this context");
let vy = ctx
.read_input::<i32>(&y)
.expect("input 'y' was created in this context");
let vz = ctx
.read_input::<i32>(&z)
.expect("input 'z' was created in this context");
vx * 100 + vy * 10 + vz
});
}
assert_eq!(compute_count, 1);
assert_eq!(ctx.stats().cache_hits, 10);
ctx.set_input(&z, 99);
let result2 = ctx.memo("expensive", |ctx| {
compute_count += 1;
let vx = ctx
.read_input::<i32>(&x)
.expect("input 'x' was created in this context");
let vy = ctx
.read_input::<i32>(&y)
.expect("input 'y' was created in this context");
let vz = ctx
.read_input::<i32>(&z)
.expect("input 'z' was created in this context");
vx * 100 + vy * 10 + vz
});
assert_eq!(result2, 1299); assert_eq!(compute_count, 2);
assert!(ctx.stats().cache_hits >= 10);
}
}