use std::any::{Any, TypeId};
use std::cell::RefCell;
use std::collections::{HashMap, VecDeque};
use std::rc::Rc;
#[cfg(not(feature = "vec_edges"))]
use smallvec::SmallVec;
use crate::cell::{Computed, Source};
use crate::effect::{Effect, EffectCallbackResult};
use crate::merge::MergePolicy;
type ComputeFn = dyn Fn(&Compute) -> AnyValue;
type EqualsFn = dyn Fn(&AnyValue, &AnyValue) -> bool;
type EffectFn = dyn Fn(&Compute) -> Option<Box<dyn FnOnce()>>;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct SlotId(pub(crate) u64);
impl SlotId {
pub(crate) const DETACHED: SlotId = SlotId(u64::MAX);
}
#[cfg(not(feature = "vec_edges"))]
type EdgeVec = SmallVec<[SlotId; 2]>;
#[cfg(feature = "vec_edges")]
type EdgeVec = Vec<SlotId>;
#[cfg(debug_assertions)]
type TypeTag = TypeId;
#[cfg(not(debug_assertions))]
type TypeTag = ();
#[inline]
fn node_type_tag<T: 'static>() -> TypeTag {
#[cfg(debug_assertions)]
{
TypeId::of::<T>()
}
#[cfg(not(debug_assertions))]
{
()
}
}
const EDGE_INDEX_THRESHOLD: usize = 32;
const DEFAULT_DRAIN_BUDGET: usize = 100_000;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct DrainExhaustion {
pub iterations: usize,
pub budget: usize,
pub top_effects: Vec<(u64, u32)>,
}
const EDGE_INDEX_DEMOTE_THRESHOLD: usize = 24;
#[derive(Default, Clone, Copy)]
pub(crate) struct SlotIdHasher(u64);
impl std::hash::Hasher for SlotIdHasher {
fn finish(&self) -> u64 {
self.0
}
fn write(&mut self, byte_a: &[u8]) {
for byte in byte_a {
self.0 = (self.0 ^ u64::from(*byte)).wrapping_mul(0x100_0000_01b3);
}
}
fn write_u64(&mut self, value: u64) {
let mut mixed = value.wrapping_mul(0x9E37_79B9_7F4A_7C15);
mixed = (mixed ^ (mixed >> 30)).wrapping_mul(0xBF58_476D_1CE4_E5B9);
mixed = (mixed ^ (mixed >> 27)).wrapping_mul(0x94D0_49BB_1331_11EB);
self.0 = mixed ^ (mixed >> 31);
}
}
#[derive(Default, Clone)]
pub(crate) struct SlotIdHashBuilder;
impl std::hash::BuildHasher for SlotIdHashBuilder {
type Hasher = SlotIdHasher;
fn build_hasher(&self) -> Self::Hasher {
SlotIdHasher(0)
}
}
type OwnerEdgeIndex = HashMap<SlotId, usize, SlotIdHashBuilder>;
type EdgeIndex = HashMap<SlotId, OwnerEdgeIndex, SlotIdHashBuilder>;
fn edge_insert(edges: &mut EdgeVec, id: SlotId, owner: SlotId, index: &mut EdgeIndex) -> bool {
let len = edges.len();
if len > EDGE_INDEX_DEMOTE_THRESHOLD
&& let Some(owner_index) = index.get_mut(&owner)
{
if owner_index.contains_key(&id) {
return false;
}
owner_index.insert(id, len);
edges.push(id);
return true;
}
if edges.contains(&id) {
return false;
}
edges.push(id);
if len + 1 > EDGE_INDEX_THRESHOLD && !index.contains_key(&owner) {
index.insert(
owner,
edges
.iter()
.enumerate()
.map(|(pos, edge)| (*edge, pos))
.collect(),
);
}
true
}
fn edge_remove(edges: &mut EdgeVec, id: SlotId, owner: SlotId, index: &mut EdgeIndex) -> bool {
if edges.len() > EDGE_INDEX_DEMOTE_THRESHOLD
&& let Some(owner_index) = index.get_mut(&owner)
{
let pos = {
#[cfg(naive_edge_remove)]
{
let scanned = edges.iter().position(|edge| *edge == id);
owner_index.remove(&id);
scanned
}
#[cfg(not(naive_edge_remove))]
{
owner_index.remove(&id)
}
};
let Some(pos) = pos else {
return false;
};
let last = edges
.pop()
.expect("index non-empty implies edges non-empty");
if pos < edges.len() {
edges[pos] = last;
owner_index.insert(last, pos);
}
if edges.len() <= EDGE_INDEX_DEMOTE_THRESHOLD {
index.remove(&owner);
}
return true;
}
if let Some(pos) = edges.iter().position(|edge| *edge == id) {
edges.swap_remove(pos);
true
} else {
false
}
}
const VALUE_INLINE_CAP: usize = 24;
const VALUE_INLINE_ALIGN: usize = 16;
#[repr(C, align(16))]
pub(crate) struct InlineBuf([core::mem::MaybeUninit<u8>; VALUE_INLINE_CAP]);
pub(crate) enum AnyValue {
None,
Inline(InlineBuf),
Heap(Rc<dyn Any>),
}
impl AnyValue {
#[inline]
pub(crate) fn from_value<T: 'static>(value: T) -> Self {
if core::mem::size_of::<T>() <= VALUE_INLINE_CAP
&& core::mem::align_of::<T>() <= VALUE_INLINE_ALIGN
&& !core::mem::needs_drop::<T>()
{
let mut buf = InlineBuf([core::mem::MaybeUninit::uninit(); VALUE_INLINE_CAP]);
unsafe {
core::ptr::write(buf.0.as_mut_ptr() as *mut T, value);
}
AnyValue::Inline(buf)
} else {
AnyValue::Heap(Rc::new(value))
}
}
#[inline]
pub(crate) unsafe fn as_t_ref_unchecked<T: 'static>(&self) -> &T {
match self {
AnyValue::Inline(buf) => unsafe { &*(buf.0.as_ptr() as *const T) },
AnyValue::Heap(rc) => unsafe { &*(&**rc as *const dyn Any as *const T) },
AnyValue::None => unsafe { core::hint::unreachable_unchecked() },
}
}
#[inline]
pub(crate) unsafe fn rc_clone_unchecked<T: 'static>(&self) -> Rc<T> {
match self {
AnyValue::Heap(rc) => {
let rc: Rc<dyn Any> = Rc::clone(rc);
unsafe {
let ptr = Rc::into_raw(rc) as *const T;
Rc::from_raw(ptr)
}
}
AnyValue::Inline(buf) => {
Rc::new(unsafe { core::ptr::read(buf.0.as_ptr() as *const T) })
}
AnyValue::None => unsafe { core::hint::unreachable_unchecked() },
}
}
#[inline]
pub(crate) fn is_none(&self) -> bool {
matches!(self, AnyValue::None)
}
}
pub(crate) struct ComputedNode {
pub(crate) value: AnyValue,
pub(crate) type_id: TypeTag,
pub(crate) compute: Rc<ComputeFn>,
pub(crate) equals: Option<Box<EqualsFn>>,
pub(crate) dependencies: EdgeVec,
pub(crate) dependents: EdgeVec,
pub(crate) dirty: bool,
pub(crate) force_recompute: bool,
pub(crate) in_progress: bool,
pub(crate) eager: bool,
pub(crate) verified_at: u64,
}
pub(crate) struct SourceNode {
pub(crate) value: AnyValue,
pub(crate) type_id: TypeTag,
pub(crate) dependents: EdgeVec,
}
pub(crate) struct EffectNode {
pub(crate) run: Rc<EffectFn>,
pub(crate) dependencies: EdgeVec,
pub(crate) cleanup: Option<Box<dyn FnOnce()>>,
pub(crate) force_run: bool,
}
pub(crate) enum Node {
Computed(ComputedNode),
Source(SourceNode),
Effect(EffectNode),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
enum FactoryKind {
Slot,
Cell,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct FactoryKey {
kind: FactoryKind,
factory_type: TypeId,
}
struct FactoryEntry {
value_type: TypeId,
handle: Rc<dyn Any>,
}
struct ContextInner {
nodes: Vec<Option<Node>>,
next_id: u64,
free_ids: Vec<u64>,
dependents_index: EdgeIndex,
dependencies_index: EdgeIndex,
roots_scratch: EdgeVec,
pending_effects: VecDeque<SlotId>,
scheduled_effects: Vec<bool>,
flushing_effects: bool,
drain_budget: usize,
drain_runs: Vec<u32>,
last_drain_exhaustion: Option<DrainExhaustion>,
batch_depth: usize,
batched_cells: EdgeVec,
batched_cell_clears: EdgeVec,
batched_slots: EdgeVec,
mark_scratch: Vec<(SlotId, bool)>,
effects_scratch: Vec<(SlotId, bool)>,
factory_handles: HashMap<FactoryKey, FactoryEntry>,
revision: u64,
revision_mode: bool,
all_effect_ids: Vec<SlotId>,
eager_by: HashMap<SlotId, SlotId>,
#[cfg(feature = "instrumentation")]
instrumentation: crate::instrumentation::InstrumentationCounters,
}
pub struct Context {
inner: RefCell<ContextInner>,
}
struct BatchGuard<'a> {
ctx: &'a Context,
}
impl Drop for BatchGuard<'_> {
fn drop(&mut self) {
self.ctx.finish_batch();
}
}
struct RefreshGuard<'a> {
ctx: &'a Context,
id: SlotId,
}
impl Drop for RefreshGuard<'_> {
fn drop(&mut self) {
let mut inner = self.ctx.inner.borrow_mut();
if let Some(Node::Computed(slot)) = Context::get_node_mut(&mut inner.nodes, self.id) {
slot.in_progress = false;
}
}
}
impl Context {
pub fn new() -> Self {
Self::new_impl(false)
}
pub fn with_revision_engine() -> Self {
Self::new_impl(true)
}
fn new_impl(revision_mode: bool) -> Self {
Self {
inner: RefCell::new(ContextInner {
nodes: Vec::new(),
next_id: 0,
free_ids: Vec::new(),
dependents_index: EdgeIndex::default(),
dependencies_index: EdgeIndex::default(),
roots_scratch: EdgeVec::new(),
pending_effects: VecDeque::new(),
drain_budget: DEFAULT_DRAIN_BUDGET,
drain_runs: Vec::new(),
last_drain_exhaustion: None,
scheduled_effects: Vec::new(),
flushing_effects: false,
batch_depth: 0,
batched_cells: EdgeVec::new(),
batched_cell_clears: EdgeVec::new(),
batched_slots: EdgeVec::new(),
mark_scratch: Vec::new(),
effects_scratch: Vec::new(),
factory_handles: HashMap::new(),
revision: 0,
revision_mode,
all_effect_ids: Vec::new(),
eager_by: HashMap::new(),
#[cfg(feature = "instrumentation")]
instrumentation: crate::instrumentation::InstrumentationCounters::default(),
}),
}
}
pub(crate) fn alloc_id(&self) -> SlotId {
let mut inner = self.inner.borrow_mut();
let slot_id = match inner.free_ids.pop() {
Some(id) => {
let id = SlotId(id);
if !inner.dependents_index.is_empty() {
inner.dependents_index.remove(&id);
}
if !inner.dependencies_index.is_empty() {
inner.dependencies_index.remove(&id);
}
id
}
None => {
let id = SlotId(inner.next_id);
inner.next_id += 1;
id
}
};
#[cfg(feature = "instrumentation")]
{
inner.instrumentation.record_node_allocation();
}
slot_id
}
fn node_index(id: SlotId) -> Option<usize> {
usize::try_from(id.0).ok()
}
fn get_node(nodes: &[Option<Node>], id: SlotId) -> Option<&Node> {
nodes.get(Self::node_index(id)?)?.as_ref()
}
fn get_node_mut(nodes: &mut [Option<Node>], id: SlotId) -> Option<&mut Node> {
nodes.get_mut(Self::node_index(id)?)?.as_mut()
}
fn take_node(nodes: &mut [Option<Node>], id: SlotId) -> Option<Node> {
nodes.get_mut(Self::node_index(id)?)?.take()
}
fn insert_node(&self, id: SlotId, node: Node) {
let index = Self::node_index(id).expect("SlotId does not fit usize");
let mut inner = self.inner.borrow_mut();
if inner.nodes.len() <= index {
inner.nodes.resize_with(index + 1, || None);
}
inner.nodes[index] = Some(node);
}
pub(crate) fn register_dependency(&self, dependency_id: SlotId, dependent_id: SlotId) {
if dependency_id == dependent_id {
return;
}
#[cfg(feature = "instrumentation")]
let mut edge_added = false;
let mut inner = self.inner.borrow_mut();
let inner_mut = &mut *inner;
if let Some(node) = Self::get_node_mut(&mut inner_mut.nodes, dependency_id) {
let index = &mut inner_mut.dependents_index;
match node {
Node::Computed(s) => {
edge_insert(&mut s.dependents, dependent_id, dependency_id, index);
}
Node::Source(c) => {
edge_insert(&mut c.dependents, dependent_id, dependency_id, index);
}
Node::Effect(_) => {}
}
}
if let Some(node) = Self::get_node_mut(&mut inner_mut.nodes, dependent_id) {
let index = &mut inner_mut.dependencies_index;
match node {
Node::Computed(parent) => {
#[cfg(feature = "instrumentation")]
{
edge_added = edge_insert(
&mut parent.dependencies,
dependency_id,
dependent_id,
index,
);
}
#[cfg(not(feature = "instrumentation"))]
{
edge_insert(&mut parent.dependencies, dependency_id, dependent_id, index);
}
}
Node::Effect(parent) => {
#[cfg(feature = "instrumentation")]
{
edge_added = edge_insert(
&mut parent.dependencies,
dependency_id,
dependent_id,
index,
);
}
#[cfg(not(feature = "instrumentation"))]
{
edge_insert(&mut parent.dependencies, dependency_id, dependent_id, index);
}
}
Node::Source(_) => {}
}
}
#[cfg(feature = "instrumentation")]
if edge_added {
inner.instrumentation.record_dependency_edge_added();
}
}
fn remove_dependency_edges_locked(
inner: &mut ContextInner,
dependency_id: SlotId,
dependent_a: &[SlotId],
) {
for dependent_id in dependent_a {
let inner_mut = &mut *inner;
if let Some(node) = Self::get_node_mut(&mut inner_mut.nodes, *dependent_id) {
let index = &mut inner_mut.dependencies_index;
match node {
Node::Computed(slot) => {
edge_remove(&mut slot.dependencies, dependency_id, *dependent_id, index);
}
Node::Effect(effect) => {
edge_remove(
&mut effect.dependencies,
dependency_id,
*dependent_id,
index,
);
}
Node::Source(_) => {}
}
}
}
}
fn drop_edge_index_entries(inner: &mut ContextInner, id: SlotId) {
if !inner.dependencies_index.is_empty() {
inner.dependencies_index.remove(&id);
}
if !inner.dependents_index.is_empty() {
inner.dependents_index.remove(&id);
}
}
fn remove_dependent_edges_locked(
inner: &mut ContextInner,
dependent_id: SlotId,
old_deps: &[SlotId],
) {
for dependency_id in old_deps {
#[cfg(feature = "instrumentation")]
let mut edge_removed = false;
let inner_mut = &mut *inner;
if let Some(dep_node) = Self::get_node_mut(&mut inner_mut.nodes, *dependency_id) {
let index = &mut inner_mut.dependents_index;
match dep_node {
Node::Computed(s) => {
#[cfg(feature = "instrumentation")]
{
edge_removed =
edge_remove(&mut s.dependents, dependent_id, *dependency_id, index);
}
#[cfg(not(feature = "instrumentation"))]
{
edge_remove(&mut s.dependents, dependent_id, *dependency_id, index);
}
}
Node::Source(c) => {
#[cfg(feature = "instrumentation")]
{
edge_removed =
edge_remove(&mut c.dependents, dependent_id, *dependency_id, index);
}
#[cfg(not(feature = "instrumentation"))]
{
edge_remove(&mut c.dependents, dependent_id, *dependency_id, index);
}
}
Node::Effect(_) => {}
}
}
#[cfg(feature = "instrumentation")]
if edge_removed {
inner.instrumentation.record_dependency_edge_removed();
}
}
}
pub fn slot<T, F>(&self, compute: F) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
{
self.slot_with_equals(compute, None)
}
pub fn computed<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static,
{
self.slot_with_equals(
compute,
Some(Box::new(|old: &AnyValue, new: &AnyValue| {
let old = unsafe { old.as_t_ref_unchecked::<T>() };
let new = unsafe { new.as_t_ref_unchecked::<T>() };
old == new
})),
)
}
pub fn computed_ripple_when<T, F, C>(&self, compute: F, changed: C) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
C: Fn(&T, &T) -> bool + 'static,
{
self.slot_with_equals(
compute,
Some(Box::new(move |old: &AnyValue, new: &AnyValue| {
let old = unsafe { old.as_t_ref_unchecked::<T>() };
let new = unsafe { new.as_t_ref_unchecked::<T>() };
!changed(old, new)
})),
)
}
pub fn memoized_slot<K, T, F>(&self, compute: F) -> Computed<T>
where
K: 'static,
T: 'static,
F: Fn(&Compute) -> T + 'static,
{
let key = FactoryKey {
kind: FactoryKind::Slot,
factory_type: TypeId::of::<K>(),
};
if let Some(handle) = self.factory_handle::<Computed<T>>(key, TypeId::of::<T>()) {
return handle;
}
let handle = self.slot(compute);
self.insert_factory_handle(key, TypeId::of::<T>(), handle);
handle
}
fn slot_with_equals<T, F>(&self, compute: F, equals: Option<Box<EqualsFn>>) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
{
let id = self.alloc_id();
let node = ComputedNode {
value: AnyValue::None,
type_id: node_type_tag::<T>(),
compute: Rc::new(move |ctx| AnyValue::from_value(compute(ctx))),
equals,
dependencies: EdgeVec::new(),
dependents: EdgeVec::new(),
dirty: false,
force_recompute: false,
in_progress: false,
eager: false,
verified_at: 0,
};
self.insert_node(id, Node::Computed(node));
Computed::from_id(id)
}
pub fn get<H: Read<Self> + ?Sized>(&self, handle: &H) -> <H as Read<Self>>::Output {
handle.read(self)
}
pub fn set<H: Write<Self> + ?Sized>(&self, handle: &H, value: <H as Write<Self>>::Value) {
handle.write(self, value)
}
pub fn get_rc<T: 'static>(&self, handle: &Computed<T>) -> Rc<T> {
self.get_rc_untracked(handle)
}
fn get_rc_untracked<T: 'static>(&self, handle: &Computed<T>) -> Rc<T> {
self.refresh_slot(handle.id);
let inner = self.inner.borrow();
if let Some(Node::Computed(slot)) = Self::get_node(&inner.nodes, handle.id)
&& !slot.value.is_none()
{
assert!(
slot.type_id == node_type_tag::<T>(),
"type mismatch in slot"
);
return unsafe { slot.value.rc_clone_unchecked::<T>() };
}
panic!("get_rc called on unset or non-slot id");
}
fn read_source_untracked<T: Clone + 'static>(&self, id: SlotId) -> T {
let inner = self.inner.borrow();
if let Some(Node::Source(c)) = Self::get_node(&inner.nodes, id) {
assert!(c.type_id == node_type_tag::<T>(), "type mismatch in cell");
unsafe { c.value.as_t_ref_unchecked::<T>() }.clone()
} else {
panic!("get called on non-cell id");
}
}
fn get_slot<T: Clone + 'static>(&self, id: SlotId) -> T {
self.get_slot_untracked(id)
}
fn get_slot_untracked<T: Clone + 'static>(&self, id: SlotId) -> T {
self.refresh_slot(id);
let inner = self.inner.borrow();
if let Some(Node::Computed(slot)) = Self::get_node(&inner.nodes, id)
&& !slot.value.is_none()
{
assert!(
slot.type_id == node_type_tag::<T>(),
"type mismatch in slot"
);
return unsafe { slot.value.as_t_ref_unchecked::<T>() }.clone();
}
panic!("get_slot called on unset or non-slot id");
}
fn enter_refresh(&self, id: SlotId) -> Option<RefreshGuard<'_>> {
let mut inner = self.inner.borrow_mut();
match Self::get_node_mut(&mut inner.nodes, id) {
Some(Node::Computed(slot)) => {
if slot.in_progress {
drop(inner);
panic!(
"lazily: circular dependency detected at slot {id:?}; a \
computed/memo slot depends on itself (directly or \
transitively) and would recompute infinitely. Break the \
cycle (e.g. via a base case or an untracked read)."
);
}
slot.in_progress = true;
Some(RefreshGuard { ctx: self, id })
}
_ => None,
}
}
fn refresh_slot(&self, id: SlotId) -> bool {
{
let inner = self.inner.borrow();
match Self::get_node(&inner.nodes, id) {
Some(Node::Computed(slot)) => {
if inner.revision_mode {
if !slot.value.is_none() && slot.verified_at == inner.revision {
return false;
}
} else if !slot.value.is_none() && !slot.dirty && !slot.force_recompute {
return false;
}
}
_ => return false,
}
}
let Some(_cycle_guard) = self.enter_refresh(id) else {
return false;
};
let dependencies = {
let inner = self.inner.borrow();
match Self::get_node(&inner.nodes, id) {
Some(Node::Computed(slot)) => slot.dependencies.clone(),
_ => return false,
}
};
let mut dependency_changed = false;
for dep_id in dependencies {
if self.refresh_slot(dep_id) {
dependency_changed = true;
}
}
let needs_recompute = {
let inner = self.inner.borrow();
let slot = match Self::get_node(&inner.nodes, id) {
Some(Node::Computed(slot)) => slot,
_ => return false,
};
if inner.revision_mode {
true
} else {
slot.value.is_none() || slot.force_recompute || dependency_changed
}
};
if !needs_recompute {
self.clear_slot_dirty_flags(id);
return false;
}
self.recompute_slot_now(id)
}
fn is_slot_node(&self, id: SlotId) -> bool {
let inner = self.inner.borrow();
matches!(Self::get_node(&inner.nodes, id), Some(Node::Computed(_)))
}
fn clear_slot_dirty_flags(&self, id: SlotId) {
let mut inner = self.inner.borrow_mut();
if let Some(Node::Computed(slot)) = Self::get_node_mut(&mut inner.nodes, id) {
slot.dirty = false;
slot.force_recompute = false;
}
}
fn recompute_slot_now(&self, id: SlotId) -> bool {
let compute: Rc<ComputeFn>;
let old_deps;
{
let mut inner = self.inner.borrow_mut();
#[cfg(feature = "instrumentation")]
{
inner.instrumentation.record_slot_recompute();
}
let slot = match Self::get_node_mut(&mut inner.nodes, id) {
Some(Node::Computed(s)) => s,
_ => panic!("get_slot called on non-slot id"),
};
old_deps = std::mem::take(&mut slot.dependencies);
compute = Rc::clone(&slot.compute);
if !inner.dependencies_index.is_empty() {
inner.dependencies_index.remove(&id);
}
Self::remove_dependent_edges_locked(&mut inner, id, &old_deps);
}
let result = {
let cx = Compute::new(self, id, 0);
(compute.as_ref())(&cx)
};
let changed = {
let mut inner = self.inner.borrow_mut();
let (rev_mode, rev) = (inner.revision_mode, inner.revision);
let slot = match Self::get_node_mut(&mut inner.nodes, id) {
Some(Node::Computed(slot)) => slot,
_ => return false,
};
let had_value = !slot.value.is_none();
let unchanged = match (&slot.value, &slot.equals) {
(AnyValue::None, _) | (_, None) => false,
(old, Some(equals)) => equals(old, &result),
};
slot.dirty = false;
slot.force_recompute = false;
if rev_mode {
slot.verified_at = rev;
}
if unchanged {
false
} else {
slot.value = result;
had_value
}
};
if changed {
self.notify_slot_value_changed(id);
}
changed
}
#[deprecated(note = "use `Context::get` — the unified cell read (#lzcellkernel)")]
pub fn get_cell<T: Clone + 'static>(&self, handle: &Source<T>) -> T {
self.get(handle)
}
pub fn get_cell_rc<T: 'static>(&self, handle: &Source<T>) -> Rc<T> {
let inner = self.inner.borrow();
if let Some(Node::Source(c)) = Self::get_node(&inner.nodes, handle.id) {
assert!(c.type_id == node_type_tag::<T>(), "type mismatch in cell");
unsafe { c.value.rc_clone_unchecked::<T>() }
} else {
panic!("get_cell_rc called on non-cell id");
}
}
pub(crate) fn read_value<T: Clone + 'static>(&self, id: SlotId) -> T {
self.read_value_untracked(id)
}
pub(crate) fn read_value_untracked<T: Clone + 'static>(&self, id: SlotId) -> T {
self.refresh_slot(id);
let inner = self.inner.borrow();
match Self::get_node(&inner.nodes, id) {
Some(Node::Computed(slot)) if !slot.value.is_none() => {
assert!(
slot.type_id == node_type_tag::<T>(),
"type mismatch in read"
);
unsafe { slot.value.as_t_ref_unchecked::<T>() }.clone()
}
Some(Node::Source(c)) => {
assert!(c.type_id == node_type_tag::<T>(), "type mismatch in read");
unsafe { c.value.as_t_ref_unchecked::<T>() }.clone()
}
_ => panic!("read_value called on unset or unknown id"),
}
}
pub(crate) fn eval_detached<T>(&self, f: impl FnOnce(&Compute) -> T) -> T {
f(&Compute::new(self, SlotId::DETACHED, 0))
}
pub fn source<T>(&self, value: T) -> Source<T>
where
T: PartialEq + 'static,
{
self.cell(value)
}
pub fn source_with<M, T>(&self, value: T) -> Source<T, M>
where
T: PartialEq + 'static,
M: MergePolicy<T>,
{
Source::from_id(self.cell(value).id)
}
pub(crate) fn make_eager<T: 'static>(&self, id: SlotId) {
{
let inner = self.inner.borrow();
match Self::get_node(&inner.nodes, id) {
Some(Node::Computed(slot)) if slot.eager => return,
Some(Node::Computed(_)) => {}
_ => return,
}
}
let effect = self.effect(move |ctx| {
let _ = ctx.get_rc::<T>(&Computed::<T>::from_id(id));
});
let mut inner = self.inner.borrow_mut();
if let Some(Node::Computed(slot)) = Self::get_node_mut(&mut inner.nodes, id) {
slot.eager = true;
}
inner.eager_by.insert(id, effect.id);
}
pub(crate) fn make_lazy(&self, id: SlotId) {
let effect_id = {
let mut inner = self.inner.borrow_mut();
match Self::get_node_mut(&mut inner.nodes, id) {
Some(Node::Computed(slot)) if slot.eager => slot.eager = false,
_ => return,
}
inner.eager_by.remove(&id)
};
if let Some(effect_id) = effect_id {
self.dispose_effect(&Effect::new(effect_id));
}
}
pub(crate) fn is_eager(&self, id: SlotId) -> bool {
let inner = self.inner.borrow();
matches!(
Self::get_node(&inner.nodes, id),
Some(Node::Computed(slot)) if slot.eager
)
}
pub(crate) fn dispose_node(&self, id: SlotId) {
self.dispose_id(id);
}
pub fn cell<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
let id = self.alloc_id();
let node = SourceNode {
value: AnyValue::from_value(value),
type_id: node_type_tag::<T>(),
dependents: EdgeVec::new(),
};
self.insert_node(id, Node::Source(node));
Source::from_id(id)
}
pub fn merge_cell<T, M>(&self, initial: T) -> Source<T, M>
where
T: PartialEq + 'static,
M: MergePolicy<T>,
{
Source::from_id(self.cell(initial).id)
}
pub fn apply_merge<T, M>(&self, handle: &Source<T>, op: T)
where
T: PartialEq + Clone + 'static,
M: MergePolicy<T>,
{
self.merge_source::<T, M>(handle.id, op);
}
pub(crate) fn merge_source<T, M>(&self, id: SlotId, op: T)
where
T: PartialEq + Clone + 'static,
M: MergePolicy<T>,
{
let merged = {
let inner = self.inner.borrow();
if let Some(Node::Source(c)) = Self::get_node(&inner.nodes, id) {
assert!(
c.type_id == node_type_tag::<T>(),
"type mismatch in apply_merge"
);
let old = unsafe { c.value.as_t_ref_unchecked::<T>() };
M::merge(old, op)
} else {
panic!("apply_merge on non-cell id");
}
};
self.set_source::<T>(id, merged);
}
pub fn memoized_cell<K, T, F>(&self, init: F) -> Source<T>
where
K: 'static,
T: PartialEq + 'static,
F: FnOnce(&Compute) -> T,
{
let key = FactoryKey {
kind: FactoryKind::Cell,
factory_type: TypeId::of::<K>(),
};
if let Some(handle) = self.factory_handle::<Source<T>>(key, TypeId::of::<T>()) {
return handle;
}
let value = self.eval_detached(init);
let handle = self.cell(value);
self.insert_factory_handle(key, TypeId::of::<T>(), handle);
handle
}
#[deprecated(note = "use `Context::set` — the unified cell write (#lzcellkernel)")]
pub fn set_cell<T: PartialEq + 'static>(&self, handle: &Source<T>, new_value: T) {
self.set(handle, new_value);
}
pub(crate) fn set_source<T: PartialEq + 'static>(&self, id: SlotId, new_value: T) {
let handle_id = id;
let changed = {
let inner = self.inner.borrow();
if let Some(Node::Source(c)) = Self::get_node(&inner.nodes, handle_id) {
assert!(
c.type_id == node_type_tag::<T>(),
"type mismatch in cell set"
);
let old = unsafe { c.value.as_t_ref_unchecked::<T>() };
*old != new_value
} else {
panic!("set_cell on non-cell id");
}
};
if changed {
{
let mut inner = self.inner.borrow_mut();
if let Some(Node::Source(c)) = Self::get_node_mut(&mut inner.nodes, handle_id) {
c.value = AnyValue::from_value(new_value);
}
}
if self.is_batching() {
self.inner.borrow_mut().batched_cells.push(handle_id);
} else if self.inner.borrow().revision_mode {
self.inner.borrow_mut().revision += 1;
self.flush_effects_revision();
} else {
if self.invalidate_cell_dependents_now(handle_id) {
self.flush_effects();
}
}
}
}
pub fn batch<F, R>(&self, run: F) -> R
where
F: FnOnce(&Context) -> R,
{
self.inner.borrow_mut().batch_depth += 1;
let _guard = BatchGuard { ctx: self };
run(self)
}
fn finish_batch(&self) {
let should_flush = {
let mut inner = self.inner.borrow_mut();
assert!(
inner.batch_depth > 0,
"finish_batch called without active batch"
);
inner.batch_depth -= 1;
inner.batch_depth == 0
};
if should_flush {
self.flush_batched_invalidations();
}
}
fn is_batching(&self) -> bool {
self.inner.borrow().batch_depth > 0
}
fn flush_batched_invalidations(&self) {
if self.inner.borrow().revision_mode {
self.inner.borrow_mut().revision += 1;
self.flush_effects_revision();
self.inner.borrow_mut().batched_cells.clear();
self.inner.borrow_mut().batched_cell_clears.clear();
self.inner.borrow_mut().batched_slots.clear();
return;
}
let all_effects = {
let mut inner = self.inner.borrow_mut();
inner.batched_cells.sort_unstable();
inner.batched_cells.dedup();
inner.batched_cell_clears.sort_unstable();
inner.batched_cell_clears.dedup();
inner.batched_slots.sort_unstable();
inner.batched_slots.dedup();
let cells = std::mem::take(&mut inner.batched_cells);
let cell_clears = std::mem::take(&mut inner.batched_cell_clears);
let slots = std::mem::take(&mut inner.batched_slots);
inner.effects_scratch.clear();
let mut roots: Vec<SlotId> = Vec::new();
for cell_id in &cells {
if let Some(Node::Source(c)) = Self::get_node(&inner.nodes, *cell_id) {
roots.extend_from_slice(&c.dependents);
}
}
Self::mark_frontier_locked(&mut inner, &roots);
let mut clear_roots: Vec<SlotId> = Vec::new();
for cell_id in &cell_clears {
if let Some(Node::Source(c)) = Self::get_node(&inner.nodes, *cell_id) {
clear_roots.extend_from_slice(&c.dependents);
}
}
Self::clear_frontier_locked(&mut inner, &clear_roots);
Self::clear_frontier_locked(&mut inner, &slots);
std::mem::take(&mut inner.effects_scratch)
};
for (effect_id, force) in &all_effects {
self.schedule_effect(*effect_id, *force);
}
self.inner.borrow_mut().effects_scratch = all_effects;
self.flush_effects();
}
pub fn effect<F, R>(&self, run: F) -> Effect
where
F: Fn(&Compute) -> R + 'static,
R: EffectCallbackResult + 'static,
{
let id = self.alloc_id();
let node = EffectNode {
run: Rc::new(move |ctx| run(ctx).into_cleanup()),
dependencies: EdgeVec::new(),
cleanup: None,
force_run: true,
};
self.insert_node(id, Node::Effect(node));
self.inner.borrow_mut().all_effect_ids.push(id);
let handle = Effect::new(id);
self.schedule_effect(id, false);
self.flush_effects();
handle
}
pub fn dispose_effect(&self, handle: &Effect) {
let torn_down = {
let mut inner = self.inner.borrow_mut();
#[cfg(audit_probe)]
crate::context::audit_probe::record_dispose_queue_len(inner.pending_effects.len());
#[cfg(naive_dispose_scan)]
inner.pending_effects.retain(|queued| *queued != handle.id);
Self::deschedule_effect(&mut inner, handle.id);
let Some(Node::Effect(effect)) = Self::take_node(&mut inner.nodes, handle.id) else {
return;
};
Self::remove_dependent_edges_locked(&mut inner, handle.id, &effect.dependencies);
Self::drop_edge_index_entries(&mut inner, handle.id);
inner.free_ids.push(handle.id.0);
effect
};
let mut torn_down = torn_down;
let cleanup = torn_down.cleanup.take();
drop(torn_down);
if let Some(cleanup) = cleanup {
cleanup();
}
}
pub fn dispose_slot<T>(&self, handle: &Computed<T>) {
self.make_lazy(handle.id);
let torn_down = {
let mut inner = self.inner.borrow_mut();
if !matches!(
Self::get_node(&inner.nodes, handle.id),
Some(Node::Computed(_))
) {
return;
}
let Some(Node::Computed(slot)) = Self::take_node(&mut inner.nodes, handle.id) else {
return;
};
Self::remove_dependent_edges_locked(&mut inner, handle.id, &slot.dependencies);
Self::remove_dependency_edges_locked(&mut inner, handle.id, &slot.dependents);
Self::invalidate_disposed_dependents_locked(&mut inner, &slot.dependents);
Self::drop_edge_index_entries(&mut inner, handle.id);
inner.free_ids.push(handle.id.0);
slot
};
drop(torn_down);
}
pub fn dispose_cell<T>(&self, handle: &Source<T>) {
let mut inner = self.inner.borrow_mut();
if !matches!(
Self::get_node(&inner.nodes, handle.id),
Some(Node::Source(_))
) {
return;
}
let Some(Node::Source(cell)) = Self::take_node(&mut inner.nodes, handle.id) else {
return;
};
Self::remove_dependency_edges_locked(&mut inner, handle.id, &cell.dependents);
Self::invalidate_disposed_dependents_locked(&mut inner, &cell.dependents);
Self::drop_edge_index_entries(&mut inner, handle.id);
inner.free_ids.push(handle.id.0);
drop(inner);
drop(cell);
}
pub fn scope(&self) -> TeardownScope<'_> {
TeardownScope {
ctx: self,
owned: RefCell::new(Vec::new()),
}
}
fn dispose_id(&self, id: SlotId) {
let kind = match Self::get_node(&self.inner.borrow().nodes, id) {
Some(Node::Computed(_)) => 0u8,
Some(Node::Source(_)) => 1,
Some(Node::Effect(_)) => 2,
None => return,
};
match kind {
0 => self.dispose_slot(&Computed::<()>::from_id(id)),
1 => self.dispose_cell(&Source::<()>::from_id(id)),
_ => self.dispose_effect(&Effect::new(id)),
}
}
pub fn dependent_count(&self, node: &impl GraphNode) -> usize {
let inner = self.inner.borrow();
match Self::get_node(&inner.nodes, node.node_id()) {
Some(Node::Computed(slot)) => slot.dependents.len(),
Some(Node::Source(cell)) => cell.dependents.len(),
Some(Node::Effect(_)) | None => 0,
}
}
pub fn dependency_count(&self, node: &impl GraphNode) -> usize {
let inner = self.inner.borrow();
match Self::get_node(&inner.nodes, node.node_id()) {
Some(Node::Computed(slot)) => slot.dependencies.len(),
Some(Node::Effect(effect)) => effect.dependencies.len(),
Some(Node::Source(_)) | None => 0,
}
}
pub fn is_effect_active(&self, handle: &Effect) -> bool {
let inner = self.inner.borrow();
matches!(
Self::get_node(&inner.nodes, handle.id),
Some(Node::Effect(_))
)
}
fn schedule_effect(&self, id: SlotId, force: bool) {
let mut inner = self.inner.borrow_mut();
let exists = match Self::get_node_mut(&mut inner.nodes, id) {
Some(Node::Effect(effect)) => {
if force {
effect.force_run = true;
}
true
}
_ => false,
};
if !exists {
return;
}
let idx = Self::node_index(id).expect("SlotId does not fit usize");
let already_scheduled = idx < inner.scheduled_effects.len() && inner.scheduled_effects[idx];
if !already_scheduled {
if idx >= inner.scheduled_effects.len() {
inner.scheduled_effects.resize(idx + 1, false);
}
inner.scheduled_effects[idx] = true;
inner.pending_effects.push_back(id);
#[cfg(feature = "instrumentation")]
{
let depth = inner.pending_effects.len();
inner.instrumentation.record_effect_queue_push(depth);
}
}
}
fn deschedule_effect(inner: &mut ContextInner, id: SlotId) {
let idx = Self::node_index(id).expect("SlotId does not fit usize");
if idx < inner.scheduled_effects.len() {
inner.scheduled_effects[idx] = false;
}
}
#[cfg(test)]
fn is_effect_scheduled(&self, id: SlotId) -> bool {
let inner = self.inner.borrow();
let idx = Self::node_index(id).expect("SlotId does not fit usize");
idx < inner.scheduled_effects.len() && inner.scheduled_effects[idx]
}
fn remove_pending_effect(&self, id: SlotId) {
let mut inner = self.inner.borrow_mut();
let idx = Self::node_index(id).expect("SlotId does not fit usize");
if idx < inner.scheduled_effects.len() && inner.scheduled_effects[idx] {
inner.pending_effects.retain(|queued| *queued != id);
inner.scheduled_effects[idx] = false;
}
}
fn pop_scheduled_effect(inner: &mut ContextInner) -> Option<SlotId> {
while let Some(id) = inner.pending_effects.pop_front() {
let idx = Self::node_index(id).expect("SlotId does not fit usize");
if idx < inner.scheduled_effects.len() && inner.scheduled_effects[idx] {
inner.scheduled_effects[idx] = false;
return Some(id);
}
}
None
}
pub(crate) fn flush_effects(&self) {
{
let mut inner = self.inner.borrow_mut();
if inner.flushing_effects {
return;
}
inner.flushing_effects = true;
inner.drain_runs.clear();
}
let mut iterations: usize = 0;
loop {
let id = {
let mut inner = self.inner.borrow_mut();
match Self::pop_scheduled_effect(&mut inner) {
Some(id) => id,
None => {
inner.flushing_effects = false;
return;
}
}
};
{
let mut inner = self.inner.borrow_mut();
if let Some(idx) = Self::node_index(id) {
if idx >= inner.drain_runs.len() {
inner.drain_runs.resize(idx + 1, 0);
}
inner.drain_runs[idx] = inner.drain_runs[idx].saturating_add(1);
}
iterations += 1;
if iterations >= inner.drain_budget {
let report = Self::drain_exhaustion_report(&inner, iterations);
inner.last_drain_exhaustion = Some(report);
inner.flushing_effects = false;
return;
}
}
self.run_effect(id);
}
}
fn drain_exhaustion_report(inner: &ContextInner, iterations: usize) -> DrainExhaustion {
const TOP_N: usize = 8;
let mut top: Vec<(u64, u32)> = inner
.drain_runs
.iter()
.enumerate()
.filter(|&(_, &runs)| runs > 0)
.map(|(idx, &runs)| (idx as u64, runs))
.collect();
top.sort_unstable_by(|a, b| b.1.cmp(&a.1).then(a.0.cmp(&b.0)));
top.truncate(TOP_N);
DrainExhaustion {
iterations,
budget: inner.drain_budget,
top_effects: top,
}
}
pub fn last_drain_exhaustion(&self) -> Option<DrainExhaustion> {
self.inner.borrow().last_drain_exhaustion.clone()
}
pub fn clear_drain_exhaustion(&self) {
self.inner.borrow_mut().last_drain_exhaustion = None;
}
pub fn drain_budget(&self) -> usize {
self.inner.borrow().drain_budget
}
pub fn set_drain_budget(&self, budget: usize) {
assert!(budget > 0, "drain budget must be non-zero");
self.inner.borrow_mut().drain_budget = budget;
}
fn flush_effects_revision(&self) {
let stale_effects: Vec<SlotId> = {
let inner = self.inner.borrow();
inner
.all_effect_ids
.iter()
.filter(|&&eid| match Self::get_node(&inner.nodes, eid) {
Some(Node::Effect(e)) => {
e.dependencies.iter().any(|dep| {
matches!(Self::get_node(&inner.nodes, *dep),
Some(Node::Computed(s)) if s.verified_at < inner.revision)
}) || e.force_run
}
_ => false,
})
.copied()
.collect()
};
for eid in stale_effects {
self.schedule_effect(eid, true);
}
self.flush_effects();
}
fn run_effect(&self, id: SlotId) {
if !self.effect_should_run(id) {
return;
}
self.remove_pending_effect(id);
let run: Rc<EffectFn>;
let old_deps;
let cleanup: Option<Box<dyn FnOnce()>>;
{
let mut inner = self.inner.borrow_mut();
let effect = match Self::get_node_mut(&mut inner.nodes, id) {
Some(Node::Effect(effect)) => effect,
_ => return,
};
old_deps = std::mem::take(&mut effect.dependencies);
cleanup = effect.cleanup.take();
effect.force_run = false;
run = Rc::clone(&effect.run);
if !inner.dependencies_index.is_empty() {
inner.dependencies_index.remove(&id);
}
Self::remove_dependent_edges_locked(&mut inner, id, &old_deps);
}
if let Some(cleanup) = cleanup {
cleanup();
}
let next_cleanup = {
let cx = Compute::new(self, id, 0);
(run.as_ref())(&cx)
};
let mut inner = self.inner.borrow_mut();
if let Some(Node::Effect(effect)) = Self::get_node_mut(&mut inner.nodes, id) {
effect.cleanup = next_cleanup;
} else if let Some(cleanup) = next_cleanup {
drop(inner);
cleanup();
}
}
fn effect_should_run(&self, id: SlotId) -> bool {
let (force_run, dependencies) = {
let inner = self.inner.borrow();
let Some(Node::Effect(effect)) = Self::get_node(&inner.nodes, id) else {
return false;
};
(effect.force_run, effect.dependencies.clone())
};
if force_run {
return true;
}
dependencies
.into_iter()
.any(|dep_id| self.is_slot_node(dep_id) && self.refresh_slot(dep_id))
}
pub fn signal<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static,
{
let computed = self.computed(compute);
self.make_eager::<T>(computed.id);
computed
}
pub fn get_signal<T: Clone + 'static>(&self, handle: &Computed<T>) -> T {
self.get(handle)
}
pub fn get_signal_rc<T: 'static>(&self, handle: &Computed<T>) -> Rc<T> {
self.get_rc(handle)
}
pub fn dispose_signal<T>(&self, handle: &Computed<T>) {
self.make_lazy(handle.id);
}
pub fn is_signal_active<T>(&self, handle: &Computed<T>) -> bool {
self.is_eager(handle.id)
}
pub(crate) fn clear_slot(&self, id: SlotId) {
if self.is_batching() {
self.inner.borrow_mut().batched_slots.push(id);
return;
}
self.clear_slot_now(id);
}
pub(crate) fn clear_slots(&self, ids: &[SlotId]) {
if ids.is_empty() {
return;
}
if self.is_batching() {
self.inner.borrow_mut().batched_slots.extend_from_slice(ids);
return;
}
let effects_to_schedule = {
let mut inner = self.inner.borrow_mut();
inner.effects_scratch.clear();
Self::clear_frontier_locked(&mut inner, ids);
std::mem::take(&mut inner.effects_scratch)
};
if !effects_to_schedule.is_empty() {
for (effect_id, force) in effects_to_schedule.iter().copied() {
self.schedule_effect(effect_id, force);
}
self.flush_effects();
}
self.inner.borrow_mut().effects_scratch = effects_to_schedule;
}
pub(crate) fn flush_effects_after_invalidation(&self) {
if !self.is_batching() {
self.flush_effects();
}
}
fn clear_slot_now(&self, id: SlotId) {
let effects_to_schedule = {
let mut inner = self.inner.borrow_mut();
let roots = [id];
inner.effects_scratch.clear();
Self::clear_frontier_locked(&mut inner, &roots);
std::mem::take(&mut inner.effects_scratch)
};
for (effect_id, force) in effects_to_schedule.iter().copied() {
self.schedule_effect(effect_id, force);
}
self.inner.borrow_mut().effects_scratch = effects_to_schedule;
}
pub(crate) fn clear_cell_dependents(&self, id: SlotId) {
if self.is_batching() {
self.inner.borrow_mut().batched_cell_clears.push(id);
return;
}
self.clear_cell_dependents_now(id);
self.flush_effects();
}
fn invalidate_cell_dependents_now(&self, id: SlotId) -> bool {
self.invalidate_dependents_now(id)
}
fn clear_cell_dependents_now(&self, id: SlotId) {
let effects_to_schedule = {
let mut inner = self.inner.borrow_mut();
let roots = match Self::get_node(&inner.nodes, id) {
Some(Node::Source(c)) => c.dependents.clone(),
_ => return,
};
inner.effects_scratch.clear();
Self::clear_frontier_locked(&mut inner, &roots);
std::mem::take(&mut inner.effects_scratch)
};
for (effect_id, force) in effects_to_schedule.iter().copied() {
self.schedule_effect(effect_id, force);
}
self.inner.borrow_mut().effects_scratch = effects_to_schedule;
}
fn invalidate_dependents_now(&self, id: SlotId) -> bool {
let effects_to_schedule = {
let mut inner = self.inner.borrow_mut();
let inner_mut = &mut *inner;
match Self::get_node_mut(&mut inner_mut.nodes, id) {
Some(Node::Source(c)) if c.dependents.is_empty() => return false,
Some(Node::Computed(s)) if s.dependents.is_empty() => return false,
Some(Node::Source(c)) => {
std::mem::swap(&mut c.dependents, &mut inner_mut.roots_scratch)
}
Some(Node::Computed(s)) => {
std::mem::swap(&mut s.dependents, &mut inner_mut.roots_scratch)
}
_ => return false,
}
let roots = std::mem::take(&mut inner.roots_scratch);
inner.effects_scratch.clear();
Self::mark_frontier_locked(&mut inner, &roots);
inner.roots_scratch = roots;
let inner_mut = &mut *inner;
match Self::get_node_mut(&mut inner_mut.nodes, id) {
Some(Node::Source(c)) => {
std::mem::swap(&mut c.dependents, &mut inner_mut.roots_scratch)
}
Some(Node::Computed(s)) => {
std::mem::swap(&mut s.dependents, &mut inner_mut.roots_scratch)
}
_ => {}
}
std::mem::take(&mut inner.effects_scratch)
};
let scheduled = !effects_to_schedule.is_empty();
for (effect_id, force) in effects_to_schedule.iter().copied() {
self.schedule_effect(effect_id, force);
}
self.inner.borrow_mut().effects_scratch = effects_to_schedule;
scheduled
}
fn invalidate_disposed_dependents_locked(inner: &mut ContextInner, dependents: &[SlotId]) {
if dependents.is_empty() {
return;
}
inner.effects_scratch.clear();
Self::mark_frontier_locked(inner, dependents);
inner.effects_scratch.clear();
}
fn mark_frontier_locked(inner: &mut ContextInner, roots: &[SlotId]) {
let nodes = &mut inner.nodes;
let stack = &mut inner.mark_scratch;
let effects = &mut inner.effects_scratch;
stack.clear();
for &root in roots {
stack.push((root, true));
}
while let Some((id, force)) = stack.pop() {
match Self::get_node_mut(nodes, id) {
Some(Node::Computed(slot)) => {
let should_propagate = !slot.dirty || (force && !slot.force_recompute);
slot.dirty = true;
if force {
slot.force_recompute = true;
}
if should_propagate {
for dep_id in &slot.dependents {
stack.push((*dep_id, false));
}
}
}
Some(Node::Effect(_)) => {
effects.push((id, force));
}
_ => {}
}
}
}
fn clear_frontier_locked(inner: &mut ContextInner, roots: &[SlotId]) {
let nodes = &mut inner.nodes;
let stack = &mut inner.mark_scratch;
let effects = &mut inner.effects_scratch;
stack.clear();
for &root in roots {
stack.push((root, true));
}
while let Some((id, _)) = stack.pop() {
match Self::get_node_mut(nodes, id) {
Some(Node::Computed(slot)) => {
if slot.value.is_none() && !slot.dirty {
continue;
}
slot.value = AnyValue::None;
slot.dirty = false;
slot.force_recompute = false;
for dep_id in &slot.dependents {
stack.push((*dep_id, true));
}
}
Some(Node::Effect(_)) => {
effects.push((id, true));
}
_ => {}
}
}
}
fn notify_slot_value_changed(&self, id: SlotId) {
if self.inner.borrow().revision_mode {
return;
}
self.invalidate_dependents_now(id);
}
pub fn is_set<T: 'static>(&self, handle: &Computed<T>) -> bool {
let inner = self.inner.borrow();
if let Some(Node::Computed(slot)) = Self::get_node(&inner.nodes, handle.id) {
!slot.value.is_none() && !slot.dirty
} else {
false
}
}
fn factory_handle<H>(&self, key: FactoryKey, value_type: TypeId) -> Option<H>
where
H: Copy + 'static,
{
let inner = self.inner.borrow();
let entry = inner.factory_handles.get(&key)?;
assert!(
entry.value_type == value_type,
"lazily: factory key {:?} was reused with an incompatible value type",
key.factory_type
);
Some(
*entry
.handle
.downcast_ref::<H>()
.expect("lazily: factory handle type mismatch"),
)
}
fn insert_factory_handle<H>(&self, key: FactoryKey, value_type: TypeId, handle: H)
where
H: Copy + 'static,
{
self.inner.borrow_mut().factory_handles.insert(
key,
FactoryEntry {
value_type,
handle: Rc::new(handle),
},
);
}
#[cfg(feature = "instrumentation")]
pub fn instrumentation_snapshot(&self) -> crate::instrumentation::InstrumentationSnapshot {
self.inner.borrow().instrumentation.snapshot()
}
#[cfg(feature = "instrumentation")]
pub fn reset_instrumentation(&self) {
self.inner.borrow_mut().instrumentation.reset();
}
}
impl Default for Context {
fn default() -> Self {
Self::new()
}
}
pub(crate) mod sealed {
pub trait Sealed {}
}
pub trait GraphNode: sealed::Sealed {
#[doc(hidden)]
fn node_id(&self) -> SlotId;
}
impl<T, M> sealed::Sealed for Source<T, M> {}
impl<T, M> GraphNode for Source<T, M> {
fn node_id(&self) -> SlotId {
self.id
}
}
impl<T> sealed::Sealed for Computed<T> {}
impl<T> GraphNode for Computed<T> {
fn node_id(&self) -> SlotId {
self.id
}
}
impl sealed::Sealed for Effect {}
impl GraphNode for Effect {
fn node_id(&self) -> SlotId {
self.id
}
}
pub struct TeardownScope<'ctx> {
ctx: &'ctx Context,
owned: RefCell<Vec<SlotId>>,
}
impl TeardownScope<'_> {
pub fn computed<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static,
{
let handle = self.ctx.computed(compute);
self.owned.borrow_mut().push(handle.id);
handle
}
pub fn source<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
let handle = self.ctx.source(value);
self.owned.borrow_mut().push(handle.id);
handle
}
#[deprecated(note = "use `TeardownScope::source`")]
pub fn cell<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
self.source(value)
}
pub fn effect<F, R>(&self, run: F) -> Effect
where
F: Fn(&Compute) -> R + 'static,
R: EffectCallbackResult + 'static,
{
let handle = self.ctx.effect(run);
self.owned.borrow_mut().push(handle.id);
handle
}
pub fn context(&self) -> &Context {
self.ctx
}
pub fn len(&self) -> usize {
self.owned.borrow().len()
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
pub fn disarm(self) {
self.owned.borrow_mut().clear();
}
}
impl Drop for TeardownScope<'_> {
fn drop(&mut self) {
let owned = std::mem::take(&mut *self.owned.borrow_mut());
for id in owned.into_iter().rev() {
self.ctx.dispose_id(id);
}
}
}
impl crate::reactive_graph::Teardown for TeardownScope<'_> {
fn len(&self) -> usize {
TeardownScope::len(self)
}
fn disarm(self) {
TeardownScope::disarm(self);
}
}
impl crate::reactive_graph::ReactiveGraph for Context {
type Computed<T> = crate::cell::Computed<T>;
type Source<T> = crate::cell::Source<T>;
type Effect = crate::effect::Effect;
type Scope<'a> = TeardownScope<'a>;
fn dispose_slot<T: 'static>(&self, handle: &Self::Computed<T>) {
Context::dispose_slot(self, handle);
}
fn dispose_cell<T: 'static>(&self, handle: &Self::Source<T>) {
Context::dispose_cell(self, handle);
}
fn dispose_effect(&self, handle: &Self::Effect) {
Context::dispose_effect(self, handle);
}
fn scope(&self) -> Self::Scope<'_> {
Context::scope(self)
}
fn batch<R>(&self, run: impl FnOnce(&Self) -> R) -> R {
Context::batch(self, run)
}
fn dependent_count(&self, node: &impl GraphNode) -> usize {
Context::dependent_count(self, node)
}
fn dependency_count(&self, node: &impl GraphNode) -> usize {
Context::dependency_count(self, node)
}
}
impl crate::reactive_graph::SyncReactiveGraph for Context {
type Compute<'a> = Compute<'a>;
fn source<T>(&self, value: T) -> Self::Source<T>
where
T: PartialEq + Send + Sync + 'static,
{
Context::source(self, value)
}
fn computed<T, F>(&self, compute: F) -> Self::Computed<T>
where
T: PartialEq + Send + Sync + 'static,
F: Fn(&Self::Compute<'_>) -> T + Send + Sync + 'static,
{
Context::computed(self, compute)
}
fn effect<F, C>(&self, run: F) -> Self::Effect
where
F: Fn(&Self::Compute<'_>) -> C + Send + Sync + 'static,
C: FnOnce() + Send + Sync + 'static,
{
Context::effect(self, run)
}
}
pub trait Read<Ctx: ?Sized> {
type Output;
fn read(&self, ctx: &Ctx) -> Self::Output;
}
pub trait Write<Ctx: ?Sized> {
type Value;
fn write(&self, ctx: &Ctx, value: Self::Value);
}
impl<T: Clone + 'static> Read<Context> for Computed<T> {
type Output = T;
fn read(&self, ctx: &Context) -> T {
ctx.get_slot(self.id)
}
}
impl<T: Clone + 'static, M> Read<Context> for Source<T, M> {
type Output = T;
fn read(&self, ctx: &Context) -> T {
ctx.read_source_untracked(self.id)
}
}
impl<T: PartialEq + 'static> Write<Context> for Source<T> {
type Value = T;
fn write(&self, ctx: &Context, value: T) {
ctx.set_source::<T>(self.id, value);
}
}
pub struct Compute<'a> {
ctx: &'a Context,
slot_id: SlotId,
#[allow(dead_code)]
generation: u64,
_not_send: std::marker::PhantomData<*const ()>,
}
impl<'a> Compute<'a> {
pub(crate) fn new(ctx: &'a Context, slot_id: SlotId, generation: u64) -> Self {
Self {
ctx,
slot_id,
generation,
_not_send: std::marker::PhantomData,
}
}
pub(crate) fn slot_id(&self) -> SlotId {
self.slot_id
}
pub fn get<H: Read<Self> + ?Sized>(&self, handle: &H) -> <H as Read<Self>>::Output {
handle.read(self)
}
pub fn set<H: Write<Self> + ?Sized>(&self, handle: &H, value: <H as Write<Self>>::Value) {
handle.write(self, value)
}
pub fn untracked(&self) -> &'a Context {
self.ctx
}
pub fn get_rc<T: 'static>(&self, handle: &Computed<T>) -> Rc<T> {
self.ctx.register_dependency(handle.id, self.slot_id);
self.ctx.get_rc_untracked(handle)
}
pub fn get_cell_rc<T: 'static>(&self, handle: &Source<T>) -> Rc<T> {
self.ctx.register_dependency(handle.id, self.slot_id);
self.ctx.get_cell_rc(handle)
}
pub fn cell<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
self.ctx.cell(value)
}
pub fn source<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
self.ctx.source(value)
}
pub fn computed<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static,
{
self.ctx.computed(compute)
}
pub fn computed_ripple_when<T, F, C>(&self, compute: F, changed: C) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
C: Fn(&T, &T) -> bool + 'static,
{
self.ctx.computed_ripple_when(compute, changed)
}
pub fn slot<T, F>(&self, compute: F) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
{
self.ctx.slot(compute)
}
pub fn batch<F, R>(&self, f: F) -> R
where
F: FnOnce(&Context) -> R,
{
self.ctx.batch(f)
}
}
impl<'a, T: Clone + 'static> Read<Compute<'a>> for Computed<T> {
type Output = T;
fn read(&self, cx: &Compute<'a>) -> T {
cx.ctx.register_dependency(self.id, cx.slot_id);
cx.ctx.get_slot_untracked(self.id)
}
}
impl<'a, T: Clone + 'static, M> Read<Compute<'a>> for Source<T, M> {
type Output = T;
fn read(&self, cx: &Compute<'a>) -> T {
cx.ctx.register_dependency(self.id, cx.slot_id);
cx.ctx.read_source_untracked(self.id)
}
}
impl<'a, T: PartialEq + 'static> Write<Compute<'a>> for Source<T> {
type Value = T;
fn write(&self, cx: &Compute<'a>, value: T) {
cx.ctx.set_source::<T>(self.id, value);
}
}
pub trait ComputeOps {
fn read_value<T: Clone + 'static>(&self, id: SlotId) -> T;
fn get_rc<T: 'static>(&self, handle: &Computed<T>) -> Rc<T>;
fn get<H: Read<Self> + ?Sized>(&self, handle: &H) -> <H as Read<Self>>::Output
where
Self: Sized,
{
handle.read(self)
}
fn set<H: Write<Self> + ?Sized>(&self, handle: &H, value: <H as Write<Self>>::Value)
where
Self: Sized,
{
handle.write(self, value)
}
fn cell<T: PartialEq + 'static>(&self, value: T) -> Source<T>;
fn source<T: PartialEq + 'static>(&self, value: T) -> Source<T>;
fn computed<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static;
fn computed_ripple_when<T, F, C>(&self, compute: F, changed: C) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
C: Fn(&T, &T) -> bool + 'static;
fn slot<T, F>(&self, compute: F) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static;
fn effect<F, R>(&self, run: F) -> Effect
where
F: Fn(&Compute) -> R + 'static,
R: EffectCallbackResult + 'static;
fn batch<F, R>(&self, f: F) -> R
where
F: FnOnce(&Context) -> R;
fn dispose_node(&self, id: SlotId);
}
impl ComputeOps for Context {
fn read_value<T: Clone + 'static>(&self, id: SlotId) -> T {
Context::read_value::<T>(self, id)
}
fn get_rc<T: 'static>(&self, handle: &Computed<T>) -> Rc<T> {
Context::get_rc::<T>(self, handle)
}
fn cell<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
Context::cell::<T>(self, value)
}
fn source<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
Context::source::<T>(self, value)
}
fn computed<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static,
{
Context::computed(self, compute)
}
fn computed_ripple_when<T, F, C>(&self, compute: F, changed: C) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
C: Fn(&T, &T) -> bool + 'static,
{
Context::computed_ripple_when(self, compute, changed)
}
fn slot<T, F>(&self, compute: F) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
{
Context::slot(self, compute)
}
fn effect<F, R>(&self, run: F) -> Effect
where
F: Fn(&Compute) -> R + 'static,
R: EffectCallbackResult + 'static,
{
Context::effect(self, run)
}
fn batch<F, R>(&self, f: F) -> R
where
F: FnOnce(&Context) -> R,
{
Context::batch(self, f)
}
fn dispose_node(&self, id: SlotId) {
Context::dispose_node(self, id)
}
}
impl ComputeOps for Compute<'_> {
fn read_value<T: Clone + 'static>(&self, id: SlotId) -> T {
self.ctx.register_dependency(id, self.slot_id);
self.ctx.read_value_untracked::<T>(id)
}
fn get_rc<T: 'static>(&self, handle: &Computed<T>) -> Rc<T> {
self.ctx.register_dependency(handle.id, self.slot_id);
self.ctx.get_rc_untracked(handle)
}
fn cell<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
self.ctx.cell(value)
}
fn source<T: PartialEq + 'static>(&self, value: T) -> Source<T> {
self.ctx.source(value)
}
fn computed<T, F>(&self, compute: F) -> Computed<T>
where
T: PartialEq + 'static,
F: Fn(&Compute) -> T + 'static,
{
self.ctx.computed(compute)
}
fn computed_ripple_when<T, F, C>(&self, compute: F, changed: C) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
C: Fn(&T, &T) -> bool + 'static,
{
self.ctx.computed_ripple_when(compute, changed)
}
fn slot<T, F>(&self, compute: F) -> Computed<T>
where
T: 'static,
F: Fn(&Compute) -> T + 'static,
{
self.ctx.slot(compute)
}
fn effect<F, R>(&self, run: F) -> Effect
where
F: Fn(&Compute) -> R + 'static,
R: EffectCallbackResult + 'static,
{
self.ctx.effect(run)
}
fn batch<F, R>(&self, f: F) -> R
where
F: FnOnce(&Context) -> R,
{
self.ctx.batch(f)
}
fn dispose_node(&self, id: SlotId) {
self.ctx.dispose_node(id)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn disposal_invalidates_surviving_readers() {
let ctx = Context::new();
let src = ctx.cell(4i64);
let derived = ctx.computed(move |c| c.get(&src));
let reader = ctx.computed(move |c| c.get(&derived) + 1);
assert_eq!(ctx.get(&reader), 5);
ctx.dispose_slot(&derived);
let after = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| ctx.get(&reader)));
assert!(
after.is_err(),
"reader must not serve its pre-disposal cache"
);
ctx.set(&src, 99);
let after_publish =
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| ctx.get(&reader)));
assert!(after_publish.is_err());
}
#[test]
fn degree_accessors_report_live_edge_set_sizes() {
let ctx = Context::new();
let topic = ctx.cell(1usize);
let a = ctx.computed(move |c| c.get(&topic) + 1);
let b = ctx.computed(move |c| c.get(&a) + 1);
assert_eq!(ctx.dependent_count(&topic), 0);
assert_eq!(ctx.dependency_count(&b), 0);
assert_eq!(ctx.get(&b), 3);
assert_eq!(ctx.dependent_count(&topic), 1);
assert_eq!(ctx.dependent_count(&a), 1);
assert_eq!(ctx.dependency_count(&a), 1);
assert_eq!(ctx.dependency_count(&b), 1);
assert_eq!(ctx.dependency_count(&topic), 0);
assert_eq!(ctx.dependent_count(&b), 0);
ctx.dispose_slot(&b);
assert_eq!(ctx.dependent_count(&a), 0);
assert_eq!(ctx.dependency_count(&b), 0);
}
#[test]
fn degree_accessors_cover_effects() {
let ctx = Context::new();
let topic = ctx.cell(1usize);
let watch = ctx.effect(move |c| {
let _ = c.get(&topic);
});
assert_eq!(ctx.dependency_count(&watch), 1);
assert_eq!(ctx.dependent_count(&topic), 1);
assert_eq!(ctx.dependent_count(&watch), 0);
ctx.dispose_effect(&watch);
assert_eq!(ctx.dependency_count(&watch), 0);
assert_eq!(ctx.dependent_count(&topic), 0);
}
#[test]
fn teardown_scope_disposes_its_nodes_on_drop() {
let ctx = Context::new();
let topic = ctx.cell(1usize);
let probe;
{
let conn = ctx.scope();
let a = conn.computed(move |c| c.get(&topic) + 1);
let _b = conn.computed(move |c| c.get(&a) * 10);
assert_eq!(conn.len(), 2);
probe = a;
assert_eq!(ctx.get(&probe), 2);
match Context::get_node(&ctx.inner.borrow().nodes, topic.id) {
Some(Node::Source(c)) => assert!(c.dependents.contains(&probe.id)),
_ => panic!("expected cell"),
}
}
let inner = ctx.inner.borrow();
match Context::get_node(&inner.nodes, topic.id) {
Some(Node::Source(c)) => assert!(
c.dependents.is_empty(),
"the scope's edges must be gone with it"
),
_ => panic!("expected cell"),
}
assert!(inner.free_ids.contains(&probe.id.0), "ids recycled");
}
#[test]
fn teardown_scope_keeps_handles_copy() {
let ctx = Context::new();
let topic = ctx.cell(5usize);
let conn = ctx.scope();
let a = conn.computed(move |c| c.get(&topic) + 1);
let b = conn.computed(move |c| c.get(&topic) + 2);
assert_eq!(ctx.get(&a) + ctx.get(&b), 13);
}
#[test]
fn teardown_scope_reads_across_scopes() {
let ctx = Context::new();
let topic = ctx.cell(2usize);
let outer = ctx.computed(move |c| c.get(&topic) * 3);
let conn = ctx.scope();
let inner = conn.computed(move |c| c.get(&outer) + 1);
assert_eq!(ctx.get(&inner), 7);
}
#[test]
fn teardown_scope_owns_cells_and_effects_too() {
let ctx = Context::new();
let (cell_id, effect_id);
{
let conn = ctx.scope();
let cell = conn.source(1usize);
cell_id = cell.id;
let effect = conn.effect(move |c| {
let _ = c.get(&cell);
});
effect_id = effect.id;
assert_eq!(conn.len(), 2);
}
let inner = ctx.inner.borrow();
assert!(inner.free_ids.contains(&cell_id.0), "cell recycled");
assert!(inner.free_ids.contains(&effect_id.0), "effect recycled");
}
#[test]
fn teardown_scope_disarm_keeps_the_nodes_alive() {
let ctx = Context::new();
let topic = ctx.cell(1usize);
let escaped = {
let conn = ctx.scope();
let slot = conn.computed(move |c| c.get(&topic) * 10);
conn.disarm();
slot
};
assert_eq!(ctx.get(&escaped), 10);
ctx.set(&topic, 4);
assert_eq!(ctx.get(&escaped), 40);
}
#[test]
fn teardown_scope_keeps_churn_flat() {
let ctx = Context::new();
let topic = ctx.cell(0usize);
for cycle in 0..500usize {
let conn = ctx.scope();
let slot = conn.computed(move |c| c.get(&topic) + cycle);
assert_eq!(ctx.get(&slot), cycle);
}
match Context::get_node(&ctx.inner.borrow().nodes, topic.id) {
Some(Node::Source(c)) => assert!(
c.dependents.is_empty(),
"every scope must have torn itself down"
),
_ => panic!("expected cell"),
}
}
#[test]
fn disposal_tolerates_a_capture_whose_drop_reenters() {
struct ReentersOnDrop {
ctx: std::rc::Weak<Context>,
victim: SlotId,
}
impl Drop for ReentersOnDrop {
fn drop(&mut self) {
if let Some(ctx) = self.ctx.upgrade() {
ctx.dispose_slot(&Computed::<u64> {
id: self.victim,
_marker: std::marker::PhantomData,
});
}
}
}
let ctx = std::rc::Rc::new(Context::new());
let base = ctx.cell(1u64);
let victim = ctx.computed(move |c| c.get(&base) + 1);
assert_eq!(ctx.get(&victim), 2);
let reentrant = ReentersOnDrop {
ctx: std::rc::Rc::downgrade(&ctx),
victim: victim.id,
};
let holder = ctx.computed(move |c| {
let _ = &reentrant;
c.get(&base) + 100
});
assert_eq!(ctx.get(&holder), 101);
ctx.dispose_slot(&holder);
assert!(
ctx.inner.borrow().free_ids.contains(&victim.id.0),
"the re-entrant disposal must have completed"
);
}
#[test]
fn dispose_slot_detaches_both_edge_directions_and_recycles_the_id() {
let ctx = Context::new();
let src = ctx.cell(1usize);
let mid = ctx.computed(move |ctx| ctx.get(&src) + 1);
let sink = ctx.computed(move |ctx| ctx.get(&mid) * 10);
assert_eq!(ctx.get(&sink), 20);
let recycled = mid.id;
ctx.dispose_slot(&mid);
{
let inner = ctx.inner.borrow();
match Context::get_node(&inner.nodes, src.id) {
Some(Node::Source(c)) => assert!(
!c.dependents.contains(&recycled),
"dependency must drop the disposed dependent"
),
_ => panic!("expected cell"),
}
match Context::get_node(&inner.nodes, sink.id) {
Some(Node::Computed(s)) => assert!(
!s.dependencies.contains(&recycled),
"dependent must drop the disposed dependency"
),
_ => panic!("expected slot"),
}
assert!(inner.free_ids.contains(&recycled.0), "id must be recycled");
}
let reused = ctx.computed(move |ctx| ctx.get(&src) + 100);
assert_eq!(reused.id, recycled);
assert_eq!(ctx.get(&reused), 101);
ctx.set(&src, 5);
assert_eq!(ctx.get(&reused), 105);
}
#[test]
fn dispose_cell_detaches_dependents_and_recycles_the_id() {
let ctx = Context::new();
let src = ctx.cell(2usize);
let derived = ctx.computed(move |ctx| ctx.get(&src) * 3);
assert_eq!(ctx.get(&derived), 6);
let recycled = src.id;
ctx.dispose_cell(&src);
let inner = ctx.inner.borrow();
match Context::get_node(&inner.nodes, derived.id) {
Some(Node::Computed(s)) => assert!(
!s.dependencies.contains(&recycled),
"dependent must drop the disposed cell"
),
_ => panic!("expected slot"),
}
assert!(inner.free_ids.contains(&recycled.0), "id must be recycled");
}
#[test]
fn dispose_slot_ignores_a_handle_naming_another_kind() {
let ctx = Context::new();
let cell = ctx.cell(1usize);
let stale: Computed<usize> = Computed {
id: cell.id,
_marker: std::marker::PhantomData,
};
ctx.dispose_slot(&stale);
assert_eq!(ctx.get(&cell), 1, "the cell must survive");
assert!(
!ctx.inner.borrow().free_ids.contains(&cell.id.0),
"a live node's id must not be recycled"
);
}
#[test]
fn dispose_slot_returns_the_graph_to_its_prior_size() {
let ctx = Context::new();
let topic = ctx.cell(0usize);
let live_width = 8usize;
let mut live_a: Vec<_> = (0..live_width)
.map(|i| {
let slot = ctx.computed(move |ctx| ctx.get(&topic) + i);
ctx.get(&slot);
slot
})
.collect();
for cycle in 0..500usize {
let victim = live_a.swap_remove(cycle % live_a.len());
ctx.dispose_slot(&victim);
let slot = ctx.computed(move |ctx| ctx.get(&topic) + cycle);
ctx.get(&slot);
live_a.push(slot);
}
let inner = ctx.inner.borrow();
match Context::get_node(&inner.nodes, topic.id) {
Some(Node::Source(c)) => assert_eq!(
c.dependents.len(),
live_width,
"dependent list must track live subscribers, not total ever created"
),
_ => panic!("expected cell"),
}
}
#[test]
fn edge_index_promotes_past_the_threshold_and_still_dedups() {
let ctx = Context::new();
let src = ctx.cell(0usize);
let width = EDGE_INDEX_THRESHOLD * 4;
let dep_a: Vec<_> = (0..width)
.map(|i| ctx.computed(move |ctx| ctx.get(&src) + i))
.collect();
for slot in &dep_a {
ctx.get(slot);
}
{
let inner = ctx.inner.borrow();
let index = inner
.dependents_index
.get(&src.id)
.expect("wide cell must be indexed");
assert_eq!(index.len(), width, "one index entry per dependent");
}
for slot in &dep_a {
ctx.get(slot);
}
{
let inner = ctx.inner.borrow();
match Context::get_node(&inner.nodes, src.id) {
Some(Node::Source(c)) => {
assert_eq!(c.dependents.len(), width, "no duplicate edges")
}
_ => panic!("expected cell"),
}
}
ctx.set(&src, 7);
for (i, slot) in dep_a.iter().enumerate() {
assert_eq!(ctx.get(slot), 7 + i, "every dependent recomputed");
}
}
#[test]
fn edge_index_demotes_when_the_list_shrinks_below_the_threshold() {
let ctx = Context::new();
let src = ctx.cell(0usize);
let toggle = ctx.cell(true);
let width = EDGE_INDEX_THRESHOLD * 2;
let dep_a: Vec<_> = (0..width)
.map(|i| {
ctx.computed(move |ctx| {
if ctx.get(&toggle) {
ctx.get(&src) + i
} else {
i
}
})
})
.collect();
for slot in &dep_a {
ctx.get(slot);
}
assert!(
ctx.inner.borrow().dependents_index.contains_key(&src.id),
"wide list must be indexed"
);
ctx.set(&toggle, false);
for slot in &dep_a {
ctx.get(slot);
}
assert!(
!ctx.inner.borrow().dependents_index.contains_key(&src.id),
"index entry must be dropped once the list is short again"
);
ctx.set(&src, 99);
for (i, slot) in dep_a.iter().enumerate() {
assert_eq!(ctx.get(slot), i, "src is no longer a dependency");
}
}
#[test]
fn edge_index_does_not_survive_id_recycling() {
let ctx = Context::new();
let src = ctx.cell(0usize);
let width = EDGE_INDEX_THRESHOLD * 2;
let cell_a: Vec<_> = (0..width).map(|i| ctx.cell(i)).collect();
let effect = ctx.effect(move |ctx| {
for cell in &cell_a {
let _ = ctx.get(cell);
}
});
assert!(
ctx.inner
.borrow()
.dependencies_index
.contains_key(&effect.id),
"wide effect must be indexed"
);
let recycled = effect.id;
ctx.dispose_effect(&effect);
assert!(
!ctx.inner
.borrow()
.dependencies_index
.contains_key(&recycled),
"disposal must drop the index entry before the id is recycled"
);
let reused = ctx.computed(move |ctx| ctx.get(&src) + 1);
assert_eq!(reused.id, recycled, "id was recycled as expected");
assert_eq!(ctx.get(&reused), 1, "recycled node computes normally");
ctx.set(&src, 41);
assert_eq!(ctx.get(&reused), 42, "recycled node propagates normally");
}
#[test]
fn context_nodes_are_vec_indexed_by_sequential_slot_ids_and_reuse_effect_ids() {
let ctx = Context::new();
let cell = ctx.cell(1i32);
let slot = ctx.slot(|_| 2i32);
let effect = ctx.effect(move |ctx| {
let _ = ctx.get(&slot);
});
assert_eq!(cell.id, SlotId(0));
assert_eq!(slot.id, SlotId(1));
assert_eq!(effect.id, SlotId(2));
{
let inner = ctx.inner.borrow();
assert_eq!(inner.nodes.len(), 3);
assert_eq!(inner.next_id, 3);
assert!(inner.free_ids.is_empty());
assert!(matches!(inner.nodes[0].as_ref(), Some(Node::Source(_))));
assert!(matches!(inner.nodes[1].as_ref(), Some(Node::Computed(_))));
assert!(matches!(inner.nodes[2].as_ref(), Some(Node::Effect(_))));
}
effect.dispose(&ctx);
{
let inner = ctx.inner.borrow();
assert_eq!(inner.nodes.len(), 3);
assert_eq!(inner.next_id, 3);
assert_eq!(inner.free_ids.as_slice(), &[2]);
assert!(inner.nodes[2].is_none());
}
let reused = ctx.computed(|_| 3i32);
assert_eq!(reused.id, SlotId(2));
{
let inner = ctx.inner.borrow();
assert_eq!(inner.nodes.len(), 3);
assert_eq!(inner.next_id, 3);
assert!(inner.free_ids.is_empty());
assert!(matches!(inner.nodes[2].as_ref(), Some(Node::Computed(_))));
}
}
#[test]
fn a_set_scheduled_flag_implies_queue_membership() {
let ctx = Context::new();
let cell = ctx.cell(0i32);
let effect_a: Vec<_> = (0..8)
.map(|_| {
ctx.effect(move |ctx| {
let _ = ctx.get(&cell);
})
})
.collect();
for effect in &effect_a {
ctx.schedule_effect(effect.id, true);
ctx.schedule_effect(effect.id, true);
}
let inner = ctx.inner.borrow();
for effect in &effect_a {
let idx = Context::node_index(effect.id).unwrap();
if inner.scheduled_effects[idx] {
assert_eq!(
inner
.pending_effects
.iter()
.filter(|queued| **queued == effect.id)
.count(),
1,
"a set scheduled flag must mean exactly one queue entry"
);
}
}
}
#[test]
fn wide_fan_out_effects_each_run_once_per_publish() {
use std::cell::RefCell;
use std::rc::Rc;
const WIDTH: usize = 64;
let ctx = Context::new();
let cell = ctx.cell(0i32);
let run_count = Rc::new(RefCell::new(vec![0usize; WIDTH]));
for i in 0..WIDTH {
let run_count = Rc::clone(&run_count);
ctx.effect(move |ctx| {
let _ = ctx.get(&cell);
run_count.borrow_mut()[i] += 1;
});
}
assert_eq!(*run_count.borrow(), vec![1usize; WIDTH]);
ctx.set(&cell, 1);
assert_eq!(*run_count.borrow(), vec![2usize; WIDTH]);
ctx.set(&cell, 2);
assert_eq!(*run_count.borrow(), vec![3usize; WIDTH]);
}
#[test]
fn dispose_effect_deschedules_without_scanning_the_queue() {
let ctx = Context::new();
let cell = ctx.cell(0i32);
let effect = ctx.effect(move |ctx| {
let _ = ctx.get(&cell);
});
ctx.schedule_effect(effect.id, true);
{
let inner = ctx.inner.borrow();
assert!(inner.pending_effects.contains(&effect.id));
}
assert!(ctx.is_effect_scheduled(effect.id));
effect.dispose(&ctx);
let inner = ctx.inner.borrow();
assert!(
!ctx.is_effect_scheduled(effect.id),
"dispose must clear the scheduled flag"
);
assert_eq!(inner.free_ids.as_slice(), &[effect.id.0]);
}
#[test]
fn a_tombstone_does_not_disturb_a_recycled_id() {
use std::cell::RefCell;
use std::rc::Rc;
let ctx = Context::new();
let cell = ctx.cell(0i32);
let doomed = ctx.effect(move |ctx| {
let _ = ctx.get(&cell);
});
ctx.schedule_effect(doomed.id, true);
doomed.dispose(&ctx);
let run_count = Rc::new(RefCell::new(0usize));
let recycled = {
let run_count = Rc::clone(&run_count);
ctx.effect(move |ctx| {
let _ = ctx.get(&cell);
*run_count.borrow_mut() += 1;
})
};
assert_eq!(recycled.id, doomed.id, "id must actually be recycled");
assert_eq!(*run_count.borrow(), 1, "creation runs it once");
ctx.set(&cell, 1);
assert_eq!(
*run_count.borrow(),
2,
"recycled effect must run exactly once per publish"
);
recycled.dispose(&ctx);
ctx.set(&cell, 2);
assert_eq!(
*run_count.borrow(),
2,
"a disposed effect must never run again"
);
}
fn cycle_panic_message(result: Box<dyn std::any::Any + Send>) -> String {
result
.downcast_ref::<String>()
.cloned()
.or_else(|| result.downcast_ref::<&str>().map(|s| s.to_string()))
.unwrap_or_default()
}
#[test]
fn two_slot_dependency_cycle_panics_instead_of_overflowing() {
let ctx = Context::new();
let link: std::rc::Rc<std::cell::RefCell<Option<Computed<i32>>>> =
std::rc::Rc::new(std::cell::RefCell::new(None));
let link_a = std::rc::Rc::clone(&link);
let a = ctx.computed(move |ctx| match *link_a.borrow() {
Some(b) => ctx.get(&b) + 1,
None => 0,
});
let b = ctx.computed(move |ctx| ctx.get(&a) + 1);
*link.borrow_mut() = Some(b);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| ctx.get(&a)));
assert!(
result.is_err(),
"circular dependency must panic, not diverge"
);
let msg = cycle_panic_message(result.unwrap_err());
assert!(
msg.contains("circular dependency"),
"unexpected panic message: {msg}"
);
{
let inner = ctx.inner.borrow();
for node in inner.nodes.iter().flatten() {
if let Node::Computed(slot) = node {
assert!(!slot.in_progress, "in_progress must be cleared on unwind");
}
}
}
let fresh = ctx.computed(|_| 42i32);
assert_eq!(ctx.get(&fresh), 42, "context must still work after a cycle");
}
#[test]
fn self_referential_slot_panics() {
let ctx = Context::new();
let link: std::rc::Rc<std::cell::RefCell<Option<Computed<i32>>>> =
std::rc::Rc::new(std::cell::RefCell::new(None));
let link_self = std::rc::Rc::clone(&link);
let s = ctx.computed(move |ctx| match *link_self.borrow() {
Some(me) => ctx.get(&me) + 1,
None => 0,
});
*link.borrow_mut() = Some(s);
let result = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| ctx.get(&s)));
assert!(result.is_err(), "self-referential slot must panic");
let msg = cycle_panic_message(result.unwrap_err());
assert!(msg.contains("circular dependency"), "got: {msg}");
}
#[test]
fn diamond_dependencies_do_not_false_positive() {
let ctx = Context::new();
let a = ctx.cell(1i32);
let b = ctx.computed(move |ctx| ctx.get(&a) + 1);
let c = ctx.computed(move |ctx| ctx.get(&a) + 2);
let d = ctx.computed(move |ctx| ctx.get(&b) + ctx.get(&c));
assert_eq!(ctx.get(&d), (1 + 1) + (1 + 2));
a.set(&ctx, 10);
assert_eq!(ctx.get(&d), (10 + 1) + (10 + 2));
}
#[test]
fn batch_dedup_invalidates_shared_dependent_once() {
let ctx = Context::new();
let a = ctx.cell(0i32);
let b = ctx.cell(0i32);
let total = ctx.computed(move |ctx| ctx.get(&a) + ctx.get(&b));
assert_eq!(ctx.get(&total), 0);
ctx.batch(|ctx| {
a.set(ctx, 1);
a.set(ctx, 2); a.set(ctx, 3); b.set(ctx, 10);
});
assert_eq!(ctx.get(&total), 13);
}
}
#[cfg(audit_probe)]
pub mod audit_probe {
use std::cell::Cell as StdCell;
thread_local! {
static MAX_LEN: StdCell<usize> = const { StdCell::new(0) };
static TOTAL_LEN: StdCell<u64> = const { StdCell::new(0) };
static CALLS: StdCell<u64> = const { StdCell::new(0) };
}
pub(crate) fn record_dispose_queue_len(len: usize) {
MAX_LEN.with(|m| m.set(m.get().max(len)));
TOTAL_LEN.with(|t| t.set(t.get() + len as u64));
CALLS.with(|c| c.set(c.get() + 1));
}
pub fn layout_rows() -> Vec<(&'static str, usize, usize)> {
use core::mem::{align_of, size_of};
macro_rules! row {
($t:ty) => {
(stringify!($t), size_of::<$t>(), align_of::<$t>())
};
}
vec![
row!(super::SlotId),
row!(super::TypeTag),
row!(super::InlineBuf),
row!(super::AnyValue),
row!(super::EdgeVec),
row!(std::rc::Rc<super::ComputeFn>),
row!(Option<Box<super::EqualsFn>>),
row!(std::rc::Rc<super::EffectFn>),
row!(Option<Box<dyn FnOnce()>>),
row!(super::ComputedNode),
row!(super::SourceNode),
row!(super::EffectNode),
row!(super::Node),
row!(Option<super::Node>),
]
}
pub fn slot_node_field_offsets() -> Vec<(&'static str, usize)> {
use core::mem::offset_of;
vec![
("value", offset_of!(super::ComputedNode, value)),
("type_id", offset_of!(super::ComputedNode, type_id)),
("compute", offset_of!(super::ComputedNode, compute)),
("equals", offset_of!(super::ComputedNode, equals)),
(
"dependencies",
offset_of!(super::ComputedNode, dependencies),
),
("dependents", offset_of!(super::ComputedNode, dependents)),
("dirty", offset_of!(super::ComputedNode, dirty)),
(
"force_recompute",
offset_of!(super::ComputedNode, force_recompute),
),
("in_progress", offset_of!(super::ComputedNode, in_progress)),
("verified_at", offset_of!(super::ComputedNode, verified_at)),
]
}
pub fn take() -> (usize, f64, u64) {
let max = MAX_LEN.with(|m| m.replace(0));
let total = TOTAL_LEN.with(|t| t.replace(0));
let calls = CALLS.with(|c| c.replace(0));
let mean = if calls == 0 {
0.0
} else {
total as f64 / calls as f64
};
(max, mean, calls)
}
}