///|
/// A typed articulation: an immutable value, mutable cell, or cached computation.
/// Values and values captured by thunks must be treated as immutable snapshots.
pub struct Art[T] {
priv engine : Engine
priv node : Node
priv mut value : T?
priv mut compute : (() -> T)?
}
///|
pub impl[T] Eq for Art[T] with fn equal(self, other) {
self.engine.id == other.engine.id && self.node.id == other.node.id
}
///|
pub impl[T] Hash for Art[T] with fn hash_combine(self, hasher) {
hasher.combine(self.engine.id)
hasher.combine(self.node.id)
}
///|
pub impl[T] Debug for Art[T] with fn to_repr(self) {
Debug::to_repr((self.engine.id, self.node.id))
}
///|
pub extend Art with Eq::{equal, not_equal}
///|
pub extend Art with Hash::{hash, hash_combine}
///|
pub extend Art with Debug::{to_repr}
///|
fn[T] Engine::art(
self : Engine,
kind : Kind,
value : T?,
compute : (() -> T)?,
) -> Art[T] {
let art = { engine: self, node: self.node(kind), value, compute, }
self.releases.push(() => {
art.value = None
art.compute = None
})
art
}
///|
/// Create a fresh mutable input. Equal writes do not invalidate observers.
pub fn[T] Engine::cell(self : Engine, value : T) -> Art[T] {
self.art(Cell, Some(value), None)
}
///|
/// Create an immutable articulation.
pub fn[T] Engine::put(self : Engine, value : T) -> Art[T] {
self.art(Constant, Some(value), None)
}
///|
/// Suspend a computation. Its dependencies are discovered by `force` calls.
/// Bodies must be deterministic and may read cells but may not call `set`.
pub fn[T] Engine::thunk(self : Engine, compute : () -> T) -> Art[T] {
self.art(Thunk, None, Some(compute))
}
///|
/// Update an input between computations. Panics for immutable values or thunks.
pub fn[T : Eq] Art::set(self : Art[T], value : T) -> Unit {
self.engine.check_context()
guard executing_engine.val is None else {
engine_abort("set is only allowed outside computations")
}
guard self.node.kind is Cell else {
engine_abort("set requires a mutable cell")
}
self.write(value)
}
///|
fn[T : Eq] Art::write(self : Art[T], value : T) -> Unit {
if self.value != Some(value) {
self.value = Some(value)
self.engine.dirty_users(self.node)
}
}
///|
fn[T] Art::read(self : Art[T], cycle : T?) -> T {
self.engine.check_context()
let engine = self.engine
let node = self.node
if node.running {
return match cycle {
Some(value) => value
None => engine_abort("unexpected cycle in incremental computation")
}
}
if node.kind is Thunk {
node.running = true
let result = executing_engine.protect(Some(engine.id), () => {
engine.current.protect(None, () => {
let mut rerun = engine.naive || self.value is None || node.changed
if node.dirty && !rerun {
// Stop at the first changed dependency. Later edges can belong to a
// branch that is no longer demanded (including division by zero).
for edge in node.deps {
if (edge.differs)() {
rerun = true
break
}
}
engine.cleanings = next_counter(engine.cleanings)
}
if rerun || node.changed {
node.detach()
let compute = self.compute.unwrap()
engine.evaluations = next_counter(engine.evaluations)
node.evaluating = true
let value = engine.path.protect(node.path, () => {
engine.current.protect(Some(node), compute)
})
node.evaluating = false
self.value = Some(value)
} else {
engine.cache_hits = next_counter(engine.cache_hits)
}
node.dirty = false
node.changed = false
self.value.unwrap()
})
})
node.running = false
result
} else {
self.value.unwrap()
}
}
///|
/// Demand a result and record an observation in the calling thunk.
pub fn[T : Eq] Art::force(self : Art[T]) -> T {
self.force_cycle(None)
}
///|
/// Use `fallback` when this force encounters an active thunk, or panic if absent.
/// This is cycle cutting, not a fixed-point solver.
pub fn[T : Eq] Art::force_cycle(self : Art[T], fallback : T?) -> T {
let value = self.read(fallback)
if !(self.node.kind is Constant) {
self.engine.observe(
self.node,
() => self.read(fallback) != value,
false,
Observe,
)
}
value
}
///|
/// Observe a pure projection. Changes outside the projection do not rerun users.
/// The mapping must not read or allocate articulations.
pub fn[T, U : Eq] Art::force_map(self : Art[T], map : (T) -> U) -> U {
let value = map(self.read(None))
if !(self.node.kind is Constant) {
self.engine.observe(
self.node,
() => map(self.read(None)) != value,
self.node.kind is Cell,
Observe,
)
}
value
}
///|
/// Create a cached derived articulation in the same engine.
pub fn[T : Eq, U] Art::map(self : Art[T], map : (T) -> U) -> Art[U] {
self.engine.thunk(() => map(self.force()))
}