1use std::cell::{Cell, Ref, RefCell};
32use std::collections::{HashMap, HashSet, VecDeque};
33use std::marker::PhantomData;
34use std::panic::{catch_unwind, resume_unwind, AssertUnwindSafe};
35use std::rc::{Rc, Weak};
36
37use crate::batch::{
38 active_batch_committed_token, boundary_drains_blocked, collecting_batch,
39 committed_after_batch_for_target, defer_to_batch, register_boundary_root,
40};
41use crate::checkpoint::{
42 unregister_backend_state_contributor, unregister_backend_state_contributor_key,
43};
44use crate::ctx::{Ctx, DepRecord, DepTerminal, WaveData};
45use crate::dispatcher::{default_dispatcher, Dispatcher, NodeFn, PoolKind};
46use crate::environment::EnvironmentDrivers;
47use crate::host_boundary::is_host_boundary_abort_payload;
48use crate::protocol::{AnyValue, GraphError, Handle, LockId, Message, PullDemand, Tier, Wave};
49use crate::versioning::{
50 advance_node_version, assert_node_version_data_compatible, create_node_version,
51 resolve_node_versioning_policy, NodeVersion, NodeVersioningPolicy,
52 ResolvedNodeVersioningPolicy, RestoredNodeVersion,
53};
54
55#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
57pub enum Status {
58 Sentinel,
61 Pending,
64 Dirty,
66 Settled,
68 Resolved,
71 Completed,
73 Errored,
75}
76
77impl Status {
78 pub fn is_fresh(self) -> bool {
80 matches!(self, Status::Settled | Status::Resolved)
81 }
82 pub fn is_terminal(self) -> bool {
84 matches!(self, Status::Completed | Status::Errored)
85 }
86}
87
88#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
91pub enum Pausable {
92 #[default]
97 True,
98 ResumeAll,
102 False,
106}
107
108#[derive(Debug, Clone)]
112pub struct NodeOpts {
113 pub factory: Option<String>,
116 pub pool: PoolKind,
118 pub pausable: Pausable,
120 pub partial: bool,
123 pub pull_id: Option<LockId>,
126 pub complete_when_deps_complete: bool,
130 pub error_when_deps_error: bool,
132 pub terminal_as_real_input: bool,
136 pub versioning: Option<NodeVersioningPolicy>,
139}
140
141impl Default for NodeOpts {
145 fn default() -> Self {
146 Self {
147 factory: None,
148 pool: PoolKind::default(),
149 pausable: Pausable::default(),
150 partial: false,
151 pull_id: None,
152 complete_when_deps_complete: true,
153 error_when_deps_error: true,
154 terminal_as_real_input: false,
155 versioning: None,
156 }
157 }
158}
159
160type Msg = Message<AnyValue>;
161type Sink = Rc<dyn Fn(&Msg)>;
162type Unsub = Box<dyn FnOnce()>;
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub(crate) enum SubscriberKind {
166 External,
167 GraphObserver,
168}
169
170struct SubscriberEntry {
171 id: u64,
172 kind: SubscriberKind,
173 sink: Sink,
174}
175
176enum SubscriberSnapshot {
177 Empty,
178 One(Sink),
179 Many(Vec<Sink>),
180}
181
182#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
183enum MaybeRunDecision {
184 #[default]
185 Skip,
186 Passthrough,
187 Run,
188}
189
190fn snapshot_subscribers(a: &NodeAux) -> SubscriberSnapshot {
191 match a.subscribers.as_slice() {
192 [] => SubscriberSnapshot::Empty,
193 [entry] => SubscriberSnapshot::One(entry.sink.clone()),
194 many => SubscriberSnapshot::Many(many.iter().map(|entry| entry.sink.clone()).collect()),
195 }
196}
197
198fn deliver_subscriber_snapshot(subs: SubscriberSnapshot, msg: &Msg) {
199 match subs {
200 SubscriberSnapshot::Empty => {}
201 SubscriberSnapshot::One(s) => s(msg),
202 SubscriberSnapshot::Many(subs) => {
203 for s in subs {
204 s(msg);
205 }
206 }
207 }
208}
209
210pub(crate) enum RewireRequest {
215 Add(Core, NodeFn),
217 Remove(CoreIdentity, NodeFn),
221 Set(Vec<Core>, NodeFn),
223}
224
225impl RewireRequest {
226 pub(crate) fn remove(dep: &Core, f: NodeFn) -> Self {
227 Self::Remove(CoreIdentity::from_core(dep), f)
228 }
229
230 fn validate_deps_same_graph(&self, owner: &Core) {
231 match self {
232 RewireRequest::Set(deps, _) => {
233 for dep in deps {
234 assert!(
235 dep.same_graph(owner),
236 "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
237 );
238 }
239 }
240 RewireRequest::Add(dep, _) => {
241 assert!(
242 dep.same_graph(owner),
243 "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
244 );
245 }
246 RewireRequest::Remove(_, _) => {}
247 }
248 }
249
250 fn project_deps(&self, current: Vec<Core>) -> Vec<Core> {
251 match self {
252 RewireRequest::Set(deps, _) => deps.clone(),
253 RewireRequest::Add(dep, _) => {
254 let mut next = current;
255 if !next.iter().any(|d| d.ptr_eq(dep)) {
256 next.push(dep.clone());
257 }
258 next
259 }
260 RewireRequest::Remove(dep, _) => {
261 current.into_iter().filter(|d| !dep.matches(d)).collect()
262 }
263 }
264 }
265
266 fn into_deps_and_fn(self, current: Vec<Core>) -> (Vec<Core>, NodeFn) {
267 match self {
268 RewireRequest::Set(deps, f) => (deps, f),
269 RewireRequest::Add(dep, f) => {
270 let mut next = current;
271 if !next.iter().any(|d| d.ptr_eq(&dep)) {
272 next.push(dep);
273 }
274 (next, f)
275 }
276 RewireRequest::Remove(dep, f) => {
277 let next = current.into_iter().filter(|d| !dep.matches(d)).collect();
278 (next, f)
279 }
280 }
281 }
282}
283
284#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
285struct NodeId(usize);
286
287#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
288struct NodeKey {
289 id: NodeId,
290 generation: u64,
291}
292
293#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
294struct ArenaNodeKey {
295 graph: usize,
296 node: NodeKey,
297}
298
299pub(crate) struct CoreIdentity {
300 arena_key: ArenaNodeKey,
301}
302
303impl CoreIdentity {
304 fn from_core(core: &Core) -> Self {
305 Self {
306 arena_key: core.arena_node_key(),
307 }
308 }
309
310 fn matches(&self, core: &Core) -> bool {
311 self.arena_key == core.arena_node_key()
312 }
313}
314
315pub(crate) struct GraphArena(Rc<RefCell<GraphCore>>);
320
321impl GraphArena {
322 pub(crate) fn new() -> Self {
323 Self(Rc::new(RefCell::new(GraphCore::new())))
324 }
325
326 pub(crate) fn default() -> Self {
327 DEFAULT_GRAPH_CORE.with(|graph| Self(graph.clone()))
328 }
329}
330
331struct GraphCore {
332 slots: Vec<Option<NodeInner>>,
333 topology_slots: Vec<Option<NodeTopologySlot>>,
334 call_slots: Vec<Option<NodeCallSlot>>,
335 config_slots: Vec<Option<NodeConfigSlot>>,
336 run_slots: Vec<Option<NodeRunState>>,
337 edge_slots: Vec<Option<DepEdges>>,
338 version_slots: Vec<Option<NodeVersionState>>,
339 aux_slots: Vec<Option<NodeAux>>,
340 generations: Vec<u64>,
341 touched_waves: Vec<u64>,
342 pins: Vec<usize>,
343 free: Vec<usize>,
344 deferred_boundary: VecDeque<BoundaryTask>,
345 draining_boundary: bool,
346}
347
348type TakenNodeSlot = (
349 NodeInner,
350 NodeTopologySlot,
351 NodeCallSlot,
352 NodeConfigSlot,
353 NodeRunState,
354 DepEdges,
355 NodeVersionState,
356 NodeAux,
357);
358
359impl GraphCore {
360 fn new() -> Self {
361 Self {
362 slots: Vec::new(),
363 topology_slots: Vec::new(),
364 call_slots: Vec::new(),
365 config_slots: Vec::new(),
366 run_slots: Vec::new(),
367 edge_slots: Vec::new(),
368 version_slots: Vec::new(),
369 aux_slots: Vec::new(),
370 generations: Vec::new(),
371 touched_waves: Vec::new(),
372 pins: Vec::new(),
373 free: Vec::new(),
374 deferred_boundary: VecDeque::new(),
375 draining_boundary: false,
376 }
377 }
378
379 fn alloc(
380 &mut self,
381 inner: NodeInner,
382 topology: NodeTopologySlot,
383 call: NodeCallSlot,
384 config: NodeConfigSlot,
385 version: NodeVersionState,
386 ) -> NodeId {
387 let edges = DepEdges::new(topology.deps.len());
388 let aux = NodeAux::new();
389 let run = NodeRunState::new();
390 if let Some(id) = self.free.last().copied() {
391 let next_generation = self.generations[id]
392 .checked_add(1)
393 .expect("GraphCore generation overflow on slot reuse");
394 self.free.pop();
395 self.slots[id] = Some(inner);
396 self.topology_slots[id] = Some(topology);
397 self.call_slots[id] = Some(call);
398 self.config_slots[id] = Some(config);
399 self.run_slots[id] = Some(run);
400 self.edge_slots[id] = Some(edges);
401 self.version_slots[id] = Some(version);
402 self.aux_slots[id] = Some(aux);
403 self.generations[id] = next_generation;
404 self.touched_waves[id] = 0;
405 self.pins[id] = 0;
406 NodeId(id)
407 } else {
408 let id = self.slots.len();
409 self.slots.push(Some(inner));
410 self.topology_slots.push(Some(topology));
411 self.call_slots.push(Some(call));
412 self.config_slots.push(Some(config));
413 self.run_slots.push(Some(run));
414 self.edge_slots.push(Some(edges));
415 self.version_slots.push(Some(version));
416 self.aux_slots.push(Some(aux));
417 self.generations.push(0);
418 self.touched_waves.push(0);
419 self.pins.push(0);
420 NodeId(id)
421 }
422 }
423
424 fn key_for(&self, id: NodeId) -> NodeKey {
425 NodeKey {
426 id,
427 generation: self.generations[id.0],
428 }
429 }
430
431 fn get(&self, key: NodeKey) -> &NodeTopologySlot {
432 assert!(
433 self.is_live_key(key),
434 "Core points at a stale or freed GraphCore slot"
435 );
436 self.topology_slots
437 .get(key.id.0)
438 .and_then(Option::as_ref)
439 .expect("Core points at a live GraphCore topology slot")
440 }
441
442 fn get_node_and_edges_mut(&mut self, key: NodeKey) -> (&mut NodeTopologySlot, &mut DepEdges) {
443 assert!(
444 self.is_live_key(key),
445 "Core points at a stale or freed GraphCore slot"
446 );
447 let n = self
448 .topology_slots
449 .get_mut(key.id.0)
450 .and_then(Option::as_mut)
451 .expect("Core points at a live GraphCore topology slot");
452 let e = self
453 .edge_slots
454 .get_mut(key.id.0)
455 .and_then(Option::as_mut)
456 .expect("Core points at a live GraphCore edge slot");
457 (n, e)
458 }
459
460 fn get_node_call_config_run_edges(
461 &self,
462 key: NodeKey,
463 ) -> (
464 &NodeTopologySlot,
465 &NodeCallSlot,
466 &NodeConfigSlot,
467 &NodeRunState,
468 &DepEdges,
469 ) {
470 assert!(
471 self.is_live_key(key),
472 "Core points at a stale or freed GraphCore slot"
473 );
474 let n = self
475 .topology_slots
476 .get(key.id.0)
477 .and_then(Option::as_ref)
478 .expect("Core points at a live GraphCore topology slot");
479 let c = self
480 .call_slots
481 .get(key.id.0)
482 .and_then(Option::as_ref)
483 .expect("Core points at a live GraphCore call slot");
484 let cfg = self
485 .config_slots
486 .get(key.id.0)
487 .and_then(Option::as_ref)
488 .expect("Core points at a live GraphCore config slot");
489 let r = self
490 .run_slots
491 .get(key.id.0)
492 .and_then(Option::as_ref)
493 .expect("Core points at a live GraphCore run slot");
494 let e = self
495 .edge_slots
496 .get(key.id.0)
497 .and_then(Option::as_ref)
498 .expect("Core points at a live GraphCore edge slot");
499 (n, c, cfg, r, e)
500 }
501
502 fn get_call(&self, key: NodeKey) -> &NodeCallSlot {
503 assert!(
504 self.is_live_key(key),
505 "Core points at a stale or freed GraphCore slot"
506 );
507 self.call_slots
508 .get(key.id.0)
509 .and_then(Option::as_ref)
510 .expect("Core points at a live GraphCore call slot")
511 }
512
513 fn get_call_mut(&mut self, key: NodeKey) -> &mut NodeCallSlot {
514 assert!(
515 self.is_live_key(key),
516 "Core points at a stale or freed GraphCore slot"
517 );
518 self.call_slots
519 .get_mut(key.id.0)
520 .and_then(Option::as_mut)
521 .expect("Core points at a live GraphCore call slot")
522 }
523
524 fn get_config(&self, key: NodeKey) -> &NodeConfigSlot {
525 assert!(
526 self.is_live_key(key),
527 "Core points at a stale or freed GraphCore slot"
528 );
529 self.config_slots
530 .get(key.id.0)
531 .and_then(Option::as_ref)
532 .expect("Core points at a live GraphCore config slot")
533 }
534
535 fn get_version(&self, key: NodeKey) -> &NodeVersionState {
536 assert!(
537 self.is_live_key(key),
538 "Core points at a stale or freed GraphCore slot"
539 );
540 self.version_slots
541 .get(key.id.0)
542 .and_then(Option::as_ref)
543 .expect("Core points at a live GraphCore version slot")
544 }
545
546 fn get_version_mut(&mut self, key: NodeKey) -> &mut NodeVersionState {
547 assert!(
548 self.is_live_key(key),
549 "Core points at a stale or freed GraphCore slot"
550 );
551 self.version_slots
552 .get_mut(key.id.0)
553 .and_then(Option::as_mut)
554 .expect("Core points at a live GraphCore version slot")
555 }
556
557 fn get_node_edges_aux_mut(
558 &mut self,
559 key: NodeKey,
560 ) -> (&mut NodeTopologySlot, &mut DepEdges, &mut NodeAux) {
561 assert!(
562 self.is_live_key(key),
563 "Core points at a stale or freed GraphCore slot"
564 );
565 let topology_slots = &mut self.topology_slots;
566 let edge_slots = &mut self.edge_slots;
567 let aux_slots = &mut self.aux_slots;
568 let n = topology_slots
569 .get_mut(key.id.0)
570 .and_then(Option::as_mut)
571 .expect("Core points at a live GraphCore topology slot");
572 let e = edge_slots
573 .get_mut(key.id.0)
574 .and_then(Option::as_mut)
575 .expect("Core points at a live GraphCore edge slot");
576 let a = aux_slots
577 .get_mut(key.id.0)
578 .and_then(Option::as_mut)
579 .expect("Core points at a live GraphCore aux slot");
580 (n, e, a)
581 }
582
583 fn get_node_call_config_run_edges_mut(
584 &mut self,
585 key: NodeKey,
586 ) -> (
587 &mut NodeTopologySlot,
588 &mut NodeCallSlot,
589 &mut NodeConfigSlot,
590 &mut NodeRunState,
591 &mut DepEdges,
592 ) {
593 assert!(
594 self.is_live_key(key),
595 "Core points at a stale or freed GraphCore slot"
596 );
597 let topology_slots = &mut self.topology_slots;
598 let call_slots = &mut self.call_slots;
599 let config_slots = &mut self.config_slots;
600 let run_slots = &mut self.run_slots;
601 let edge_slots = &mut self.edge_slots;
602 let n = topology_slots
603 .get_mut(key.id.0)
604 .and_then(Option::as_mut)
605 .expect("Core points at a live GraphCore topology slot");
606 let c = call_slots
607 .get_mut(key.id.0)
608 .and_then(Option::as_mut)
609 .expect("Core points at a live GraphCore call slot");
610 let cfg = config_slots
611 .get_mut(key.id.0)
612 .and_then(Option::as_mut)
613 .expect("Core points at a live GraphCore config slot");
614 let r = run_slots
615 .get_mut(key.id.0)
616 .and_then(Option::as_mut)
617 .expect("Core points at a live GraphCore run slot");
618 let e = edge_slots
619 .get_mut(key.id.0)
620 .and_then(Option::as_mut)
621 .expect("Core points at a live GraphCore edge slot");
622 (n, c, cfg, r, e)
623 }
624
625 fn get_node_call_config_run_edges_aux_mut(
626 &mut self,
627 key: NodeKey,
628 ) -> (
629 &mut NodeTopologySlot,
630 &mut NodeCallSlot,
631 &mut NodeConfigSlot,
632 &mut NodeRunState,
633 &mut DepEdges,
634 &mut NodeAux,
635 ) {
636 assert!(
637 self.is_live_key(key),
638 "Core points at a stale or freed GraphCore slot"
639 );
640 let topology_slots = &mut self.topology_slots;
641 let call_slots = &mut self.call_slots;
642 let config_slots = &mut self.config_slots;
643 let run_slots = &mut self.run_slots;
644 let edge_slots = &mut self.edge_slots;
645 let aux_slots = &mut self.aux_slots;
646 let n = topology_slots
647 .get_mut(key.id.0)
648 .and_then(Option::as_mut)
649 .expect("Core points at a live GraphCore topology slot");
650 let c = call_slots
651 .get_mut(key.id.0)
652 .and_then(Option::as_mut)
653 .expect("Core points at a live GraphCore call slot");
654 let cfg = config_slots
655 .get_mut(key.id.0)
656 .and_then(Option::as_mut)
657 .expect("Core points at a live GraphCore config slot");
658 let r = run_slots
659 .get_mut(key.id.0)
660 .and_then(Option::as_mut)
661 .expect("Core points at a live GraphCore run slot");
662 let e = edge_slots
663 .get_mut(key.id.0)
664 .and_then(Option::as_mut)
665 .expect("Core points at a live GraphCore edge slot");
666 let a = aux_slots
667 .get_mut(key.id.0)
668 .and_then(Option::as_mut)
669 .expect("Core points at a live GraphCore aux slot");
670 (n, c, cfg, r, e, a)
671 }
672
673 fn get_aux(&self, key: NodeKey) -> &NodeAux {
674 assert!(
675 self.is_live_key(key),
676 "Core points at a stale or freed GraphCore slot"
677 );
678 self.aux_slots
679 .get(key.id.0)
680 .and_then(Option::as_ref)
681 .expect("Core points at a live GraphCore aux slot")
682 }
683
684 fn is_live(&self, id: NodeId) -> bool {
685 let live = self.slots.get(id.0).is_some_and(Option::is_some);
686 if live {
687 debug_assert!(self.topology_slots.get(id.0).is_some_and(Option::is_some));
688 debug_assert!(self.call_slots.get(id.0).is_some_and(Option::is_some));
689 debug_assert!(self.config_slots.get(id.0).is_some_and(Option::is_some));
690 debug_assert!(self.run_slots.get(id.0).is_some_and(Option::is_some));
691 debug_assert!(self.edge_slots.get(id.0).is_some_and(Option::is_some));
692 debug_assert!(self.version_slots.get(id.0).is_some_and(Option::is_some));
693 debug_assert!(self.aux_slots.get(id.0).is_some_and(Option::is_some));
694 }
695 live
696 }
697
698 fn is_live_key(&self, key: NodeKey) -> bool {
699 self.generations.get(key.id.0) == Some(&key.generation) && self.is_live(key.id)
700 }
701
702 fn pin_live_key(&mut self, key: NodeKey) -> bool {
703 if !self.is_live_key(key) {
704 return false;
705 }
706 self.pins[key.id.0] = self.pins[key.id.0]
707 .checked_add(1)
708 .expect("GraphCore pin count overflow");
709 true
710 }
711
712 fn release_pin(&mut self, key: NodeKey) {
713 if self.generations.get(key.id.0) != Some(&key.generation) {
714 return;
715 }
716 let Some(pin) = self.pins.get_mut(key.id.0) else {
717 return;
718 };
719 if *pin > 0 {
720 *pin -= 1;
721 }
722 }
723
724 fn pin_count(&self, key: NodeKey) -> usize {
725 if self.generations.get(key.id.0) != Some(&key.generation) {
726 return 0;
727 }
728 self.pins.get(key.id.0).copied().unwrap_or(0)
729 }
730
731 fn take_live(&mut self, key: NodeKey) -> Option<TakenNodeSlot> {
732 if !self.is_live_key(key) {
733 return None;
734 }
735 let inner = self.slots.get_mut(key.id.0)?.take()?;
736 let topology = self.topology_slots.get_mut(key.id.0)?.take()?;
737 let call = self.call_slots.get_mut(key.id.0)?.take()?;
738 let config = self.config_slots.get_mut(key.id.0)?.take()?;
739 let run = self.run_slots.get_mut(key.id.0)?.take()?;
740 let edges = self.edge_slots.get_mut(key.id.0)?.take()?;
741 let version = self.version_slots.get_mut(key.id.0)?.take()?;
742 let aux = self.aux_slots.get_mut(key.id.0)?.take()?;
743 Some((inner, topology, call, config, run, edges, version, aux))
744 }
745}
746
747thread_local! {
748 static DEFAULT_GRAPH_CORE: Rc<RefCell<GraphCore>> = Rc::new(RefCell::new(GraphCore::new()));
752}
753
754struct DepState {
756 batch: Vec<Option<Vec<AnyValue>>>,
757 batch_waves: Vec<Vec<Vec<WaveData>>>,
758 batch_wave_id: Vec<Option<u64>>,
759 prev: Vec<Option<AnyValue>>,
760 has_data: Vec<bool>,
761 dirty: Vec<bool>,
762 tier: Vec<u8>,
763 pending: i32,
764 terminal: Vec<Option<DepTerminal>>,
765 terminal_wave: Vec<Option<DepTerminal>>,
766}
767
768impl DepState {
769 fn new(dep_count: usize) -> Self {
770 Self {
771 batch: vec![None; dep_count],
772 batch_waves: vec![Vec::new(); dep_count],
773 batch_wave_id: vec![None; dep_count],
774 prev: vec![None; dep_count],
775 has_data: vec![false; dep_count],
776 dirty: vec![false; dep_count],
777 tier: vec![0; dep_count],
778 pending: 0,
779 terminal: vec![None; dep_count],
780 terminal_wave: vec![None; dep_count],
781 }
782 }
783
784 fn all_settled(&self, terminal_as_real_input: bool) -> bool {
785 for i in 0..self.has_data.len() {
786 if self.has_data[i] {
787 continue;
788 }
789 if terminal_as_real_input && self.terminal[i].is_some() {
790 continue;
791 }
792 return false;
793 }
794 true
795 }
796
797 fn current_wave_mut(&mut self, idx: usize) -> &mut Vec<WaveData> {
798 let delivery_id = current_delivery_id();
799 if self.batch_wave_id[idx] != Some(delivery_id) {
800 self.batch_waves[idx].push(Vec::new());
801 self.batch_wave_id[idx] = Some(delivery_id);
802 }
803 self.batch_waves[idx]
804 .last_mut()
805 .expect("current_wave_mut just pushed an entry")
806 }
807
808 fn record_wave_data(&mut self, idx: usize, value: AnyValue) {
809 self.current_wave_mut(idx).push(WaveData::Data(value));
810 }
811
812 fn record_wave_sentinel(&mut self, idx: usize) -> bool {
813 let had_current_projection = self.batch_wave_id[idx] == Some(current_delivery_id());
814 self.current_wave_mut(idx).push(WaveData::Sentinel);
815 had_current_projection
816 }
817
818 fn has_run_triggering_projection(&self) -> bool {
819 self.batch_waves.iter().any(|dep_waves| {
820 dep_waves.iter().any(|wave| {
821 wave.is_empty() || wave.iter().any(|item| matches!(item, WaveData::Data(_)))
822 })
823 })
824 }
825
826 fn clear_wave_projection(&mut self, idx: usize) {
827 self.batch_waves[idx].clear();
828 self.batch_wave_id[idx] = None;
829 }
830
831 fn record_resolved_wave(&mut self, idx: usize) {
832 let _ = self.current_wave_mut(idx);
833 }
834}
835
836struct NodeWaveState {
838 emitted_dirty_this_wave: bool,
839 emitted_tier3_this_wave: bool,
840 inside_run_wave: bool,
841 in_dep_mutation: bool,
842 rewire_run_pending: bool,
843 batch_dirty_owed: bool,
844}
845
846impl NodeWaveState {
847 fn new() -> Self {
848 Self {
849 emitted_dirty_this_wave: false,
850 emitted_tier3_this_wave: false,
851 inside_run_wave: false,
852 in_dep_mutation: false,
853 rewire_run_pending: false,
854 batch_dirty_owed: false,
855 }
856 }
857}
858
859struct NodeValueState {
860 cache: Option<AnyValue>,
861 has_data: bool,
862 status: Status,
863 terminal: bool,
864}
865
866impl NodeValueState {
867 fn new() -> Self {
868 Self {
869 cache: None,
870 has_data: false,
871 status: Status::Sentinel,
872 terminal: false,
873 }
874 }
875}
876
877struct DepEdges {
880 value: NodeValueState,
881 wave: NodeWaveState,
882 state: DepState,
883 unsubs: Vec<Option<Unsub>>,
884 idx_boxes: Vec<Rc<Cell<i64>>>,
885 restored_activation_handshake: Vec<bool>,
886}
887
888impl DepEdges {
889 fn new(dep_count: usize) -> Self {
890 Self {
891 value: NodeValueState::new(),
892 wave: NodeWaveState::new(),
893 state: DepState::new(dep_count),
894 unsubs: (0..dep_count).map(|_| None).collect(),
895 idx_boxes: (0..dep_count).map(|_| Rc::new(Cell::new(-1i64))).collect(),
896 restored_activation_handshake: vec![false; dep_count],
897 }
898 }
899}
900
901struct NodeCallSlot {
905 factory: Option<String>,
906 handle: Option<Handle>,
907 dispatcher: Dispatcher,
908 environment: EnvironmentDrivers,
909}
910
911struct NodeConfigSlot {
914 partial: bool,
915 pausable: Pausable,
916 pull_id: Option<LockId>,
917 complete_when_deps_complete: bool,
918 error_when_deps_error: bool,
919 terminal_as_real_input: bool,
920 versioning: Option<NodeVersioningPolicy>,
921}
922
923impl NodeConfigSlot {
924 fn from_opts(opts: &NodeOpts) -> Self {
925 Self {
926 partial: opts.partial,
927 pausable: opts.pausable,
928 pull_id: None,
929 complete_when_deps_complete: opts.complete_when_deps_complete,
930 error_when_deps_error: opts.error_when_deps_error,
931 terminal_as_real_input: opts.terminal_as_real_input,
932 versioning: opts.versioning.clone(),
933 }
934 }
935
936 fn all_deps_settled(&self, dep: &DepState) -> bool {
937 dep.all_settled(self.terminal_as_real_input)
938 }
939}
940
941struct NodeVersionState {
942 policy: ResolvedNodeVersioningPolicy,
943 value: Option<NodeVersion>,
944}
945
946impl NodeVersionState {
947 fn new(policy: Option<NodeVersioningPolicy>, initial: Option<&AnyValue>) -> Self {
948 let policy = resolve_node_versioning_policy(policy);
949 let value = create_node_version(&policy, initial)
950 .unwrap_or_else(|err| panic!("node versioning: {err}"));
951 Self { policy, value }
952 }
953}
954
955struct NodeRunState {
959 has_called_fn_once: bool,
960}
961
962impl NodeRunState {
963 fn new() -> Self {
964 Self {
965 has_called_fn_once: false,
966 }
967 }
968}
969
970struct NodeAux {
973 subscribers: Vec<SubscriberEntry>,
974 next_sub_id: u64,
975 activated: bool,
976 state: Option<AnyValue>,
977 state_persist: bool,
978 on_deactivation: Vec<Box<dyn FnOnce()>>,
979 on_invalidate: Vec<Rc<dyn Fn()>>,
980 pull_demand_owed: Option<PullDemand>,
984 active_pull: Option<PullDemand>,
986 in_deliver_demand: bool,
988 pull_dirty_owed: bool,
991 pause_lockset: HashSet<LockId>,
993 paused_dep_wave_occurred: bool,
995 pause_buffer: Vec<Wave<AnyValue>>,
997}
998
999impl NodeAux {
1000 fn new() -> Self {
1001 Self {
1002 subscribers: Vec::new(),
1003 next_sub_id: 0,
1004 activated: false,
1005 state: None,
1006 state_persist: false,
1007 on_deactivation: Vec::new(),
1008 on_invalidate: Vec::new(),
1009 pull_demand_owed: None,
1010 active_pull: None,
1011 in_deliver_demand: false,
1012 pull_dirty_owed: false,
1013 pause_lockset: HashSet::new(),
1014 paused_dep_wave_occurred: false,
1015 pause_buffer: Vec::new(),
1016 }
1017 }
1018}
1019
1020pub(crate) struct NodeCheckpointRuntime {
1021 pub cache: Option<AnyValue>,
1022 pub has_data: bool,
1023 pub version: Option<NodeVersion>,
1024 pub status: Status,
1025 pub terminal: bool,
1026 pub activated: bool,
1027 pub has_called_fn_once: bool,
1028 pub ctx_state: Option<AnyValue>,
1029 pub ctx_state_persist: bool,
1030}
1031
1032pub(crate) struct NodeRestoreRuntime {
1033 pub cache: Option<AnyValue>,
1034 pub has_data: bool,
1035 pub version: RestoredNodeVersion,
1036 pub status: Status,
1037 pub terminal: bool,
1038 pub activated: bool,
1039 pub has_called_fn_once: bool,
1040 pub ctx_state: Option<AnyValue>,
1041 pub ctx_state_persist: bool,
1042}
1043
1044type TopologyDepsChangedObserver = Rc<dyn Fn(&[Core], &[Core])>;
1047
1048struct NodeTopologySlot {
1049 deps: Vec<Core>,
1050 topology_deps_changed: Option<TopologyDepsChangedObserver>,
1051}
1052
1053struct NodeInner;
1054
1055impl NodeInner {
1068 fn cleanup_before_free(
1069 &mut self,
1070 call: &mut NodeCallSlot,
1071 edges: &mut DepEdges,
1072 aux: &mut NodeAux,
1073 ) {
1074 if let Some(handle) = call.handle {
1084 call.dispatcher.unregister(handle);
1085 }
1086 for u in edges.unsubs.drain(..).flatten() {
1087 u();
1088 }
1089 for h in std::mem::take(&mut aux.on_deactivation) {
1090 h();
1091 }
1092 }
1093}
1094
1095pub struct Core {
1100 graph: Rc<RefCell<GraphCore>>,
1101 id: NodeId,
1102 generation: u64,
1103 refs: Rc<Cell<usize>>,
1105 counted: bool,
1108}
1109
1110impl Core {
1111 fn from_parts(
1112 graph: Rc<RefCell<GraphCore>>,
1113 id: NodeId,
1114 generation: u64,
1115 refs: Rc<Cell<usize>>,
1116 ) -> Self {
1117 assert!(
1118 refs.get() != 0,
1119 "Core counted promotion rejected: node is externally dead"
1120 );
1121 refs.set(refs.get().checked_add(1).expect("Core refcount overflow"));
1122 Self {
1123 graph,
1124 id,
1125 generation,
1126 refs,
1127 counted: true,
1128 }
1129 }
1130
1131 fn from_borrowed_parts(
1132 graph: Rc<RefCell<GraphCore>>,
1133 id: NodeId,
1134 generation: u64,
1135 refs: Rc<Cell<usize>>,
1136 ) -> Self {
1137 Self {
1138 graph,
1139 id,
1140 generation,
1141 refs,
1142 counted: false,
1143 }
1144 }
1145
1146 fn borrowed_view(&self) -> Self {
1147 Self::from_borrowed_parts(
1148 self.graph.clone(),
1149 self.id,
1150 self.generation,
1151 self.refs.clone(),
1152 )
1153 }
1154
1155 fn key(&self) -> NodeKey {
1156 NodeKey {
1157 id: self.id,
1158 generation: self.generation,
1159 }
1160 }
1161
1162 fn arena_node_key(&self) -> ArenaNodeKey {
1163 ArenaNodeKey {
1164 graph: Rc::as_ptr(&self.graph) as usize,
1165 node: self.key(),
1166 }
1167 }
1168
1169 fn boundary_root(&self) -> BoundaryRoot {
1170 BoundaryRoot {
1171 graph_id: Rc::as_ptr(&self.graph) as usize,
1172 graph: Rc::downgrade(&self.graph),
1173 }
1174 }
1175
1176 fn borrow(&self) -> Ref<'_, NodeTopologySlot> {
1177 Ref::map(self.graph.borrow(), |g| g.get(self.key()))
1178 }
1179
1180 fn with_inner_edges_mut<R>(
1181 &self,
1182 f: impl FnOnce(&mut NodeTopologySlot, &mut DepEdges) -> R,
1183 ) -> R {
1184 let mut g = self.graph.borrow_mut();
1185 let (n, e) = g.get_node_and_edges_mut(self.key());
1186 f(n, e)
1187 }
1188
1189 fn with_inner_edges_aux_mut<R>(
1190 &self,
1191 f: impl FnOnce(&mut NodeTopologySlot, &mut DepEdges, &mut NodeAux) -> R,
1192 ) -> R {
1193 let mut g = self.graph.borrow_mut();
1194 let (n, e, a) = g.get_node_edges_aux_mut(self.key());
1195 f(n, e, a)
1196 }
1197
1198 fn with_inner_edges<R>(&self, f: impl FnOnce(&NodeTopologySlot, &DepEdges) -> R) -> R {
1199 let g = self.graph.borrow();
1200 let n = g.get(self.key());
1201 let e = g
1202 .edge_slots
1203 .get(self.id.0)
1204 .and_then(Option::as_ref)
1205 .expect("Core points at a live GraphCore edge slot");
1206 f(n, e)
1207 }
1208
1209 fn with_node_state<R>(
1210 &self,
1211 f: impl FnOnce(&NodeTopologySlot, &NodeCallSlot, &NodeConfigSlot, &NodeRunState, &DepEdges) -> R,
1212 ) -> R {
1213 let g = self.graph.borrow();
1214 let (n, c, cfg, r, e) = g.get_node_call_config_run_edges(self.key());
1215 f(n, c, cfg, r, e)
1216 }
1217
1218 fn with_node_state_mut<R>(
1219 &self,
1220 f: impl FnOnce(
1221 &mut NodeTopologySlot,
1222 &mut NodeCallSlot,
1223 &mut NodeConfigSlot,
1224 &mut NodeRunState,
1225 &mut DepEdges,
1226 ) -> R,
1227 ) -> R {
1228 let mut g = self.graph.borrow_mut();
1229 let (n, c, cfg, r, e) = g.get_node_call_config_run_edges_mut(self.key());
1230 f(n, c, cfg, r, e)
1231 }
1232
1233 fn with_node_state_aux_mut<R>(
1234 &self,
1235 f: impl FnOnce(
1236 &mut NodeTopologySlot,
1237 &mut NodeCallSlot,
1238 &mut NodeConfigSlot,
1239 &mut NodeRunState,
1240 &mut DepEdges,
1241 &mut NodeAux,
1242 ) -> R,
1243 ) -> R {
1244 let mut g = self.graph.borrow_mut();
1245 let (n, c, cfg, r, e, a) = g.get_node_call_config_run_edges_aux_mut(self.key());
1246 f(n, c, cfg, r, e, a)
1247 }
1248
1249 fn with_call<R>(&self, f: impl FnOnce(&NodeCallSlot) -> R) -> R {
1250 let g = self.graph.borrow();
1251 f(g.get_call(self.key()))
1252 }
1253
1254 fn with_call_mut<R>(&self, f: impl FnOnce(&mut NodeCallSlot) -> R) -> R {
1255 let mut g = self.graph.borrow_mut();
1256 f(g.get_call_mut(self.key()))
1257 }
1258
1259 fn with_config<R>(&self, f: impl FnOnce(&NodeConfigSlot) -> R) -> R {
1260 let g = self.graph.borrow();
1261 f(g.get_config(self.key()))
1262 }
1263
1264 fn with_aux<R>(&self, f: impl FnOnce(&NodeAux) -> R) -> R {
1265 let g = self.graph.borrow();
1266 f(g.get_aux(self.key()))
1267 }
1268
1269 fn with_aux_mut<R>(&self, f: impl FnOnce(&mut NodeAux) -> R) -> R {
1270 let mut g = self.graph.borrow_mut();
1271 assert!(
1272 g.is_live_key(self.key()),
1273 "Core points at a stale or freed GraphCore slot"
1274 );
1275 let a = g
1276 .aux_slots
1277 .get_mut(self.id.0)
1278 .and_then(Option::as_mut)
1279 .expect("Core points at a live GraphCore aux slot");
1280 f(a)
1281 }
1282
1283 fn try_with_node_state_aux_mut<R>(
1284 &self,
1285 f: impl FnOnce(
1286 &mut NodeTopologySlot,
1287 &mut NodeCallSlot,
1288 &mut NodeConfigSlot,
1289 &mut NodeRunState,
1290 &mut DepEdges,
1291 &mut NodeAux,
1292 ) -> R,
1293 ) -> Option<R> {
1294 let mut g = self.graph.try_borrow_mut().ok()?;
1295 let (n, c, cfg, r, e, a) = g.get_node_call_config_run_edges_aux_mut(self.key());
1296 Some(f(n, c, cfg, r, e, a))
1297 }
1298
1299 fn downgrade(&self) -> CoreWeak {
1300 CoreWeak {
1301 graph: Rc::downgrade(&self.graph),
1302 key: self.key(),
1303 refs: Rc::downgrade(&self.refs),
1304 }
1305 }
1306}
1307
1308impl Clone for Core {
1309 fn clone(&self) -> Self {
1310 Self::from_parts(
1311 self.graph.clone(),
1312 self.id,
1313 self.generation,
1314 self.refs.clone(),
1315 )
1316 }
1317}
1318
1319impl Drop for Core {
1320 fn drop(&mut self) {
1321 if !self.counted {
1322 return;
1323 }
1324 let refs = self.refs.get();
1325 if refs > 1 {
1326 self.refs.set(refs - 1);
1327 return;
1328 }
1329 if refs == 0 {
1330 return;
1331 }
1332 self.refs.set(0);
1333 free_slot_if_unreferenced(&self.graph, self.key(), &self.refs);
1334 }
1335}
1336
1337fn free_slot_if_unreferenced(graph_ref: &Rc<RefCell<GraphCore>>, key: NodeKey, refs: &Cell<usize>) {
1338 if refs.get() != 0 {
1339 return;
1340 }
1341 let (mut inner, _topology, mut call, _config, _run, mut edges, _version, mut aux) = {
1342 let mut graph = graph_ref.borrow_mut();
1343 if graph.pin_count(key) != 0 {
1344 return;
1345 }
1346 let Some(pair) = graph.take_live(key) else {
1347 return;
1348 };
1349 pair
1350 };
1351 struct FreeSlotOnDrop {
1352 graph: Rc<RefCell<GraphCore>>,
1353 id: usize,
1354 armed: bool,
1355 }
1356 impl Drop for FreeSlotOnDrop {
1357 fn drop(&mut self) {
1358 if self.armed {
1359 self.graph.borrow_mut().free.push(self.id);
1360 }
1361 }
1362 }
1363 let mut free_slot = FreeSlotOnDrop {
1364 graph: graph_ref.clone(),
1365 id: key.id.0,
1366 armed: true,
1367 };
1368 unregister_backend_state_contributor_key((
1369 Rc::as_ptr(graph_ref) as usize,
1370 key.id.0,
1371 key.generation,
1372 ));
1373 inner.cleanup_before_free(&mut call, &mut edges, &mut aux);
1374 free_slot.armed = false;
1375 graph_ref.borrow_mut().free.push(key.id.0);
1376}
1377
1378struct ArenaNodePin {
1379 arena_key: ArenaNodeKey,
1380 graph: Rc<RefCell<GraphCore>>,
1381 refs: Rc<Cell<usize>>,
1382}
1383
1384impl ArenaNodePin {
1385 pub(crate) fn from_core(core: &Core) -> Option<Self> {
1386 {
1387 let mut graph = core.graph.borrow_mut();
1388 if !graph.pin_live_key(core.key()) {
1389 return None;
1390 }
1391 }
1392 Some(Self {
1393 arena_key: core.arena_node_key(),
1394 graph: core.graph.clone(),
1395 refs: core.refs.clone(),
1396 })
1397 }
1398
1399 fn borrowed_core(&self) -> Option<Core> {
1400 if !self.graph.borrow().is_live_key(self.arena_key.node) {
1401 return None;
1402 }
1403 Some(Core::from_borrowed_parts(
1404 self.graph.clone(),
1405 self.arena_key.node.id,
1406 self.arena_key.node.generation,
1407 self.refs.clone(),
1408 ))
1409 }
1410
1411 fn reset_wave_flags(&self) {
1412 let mut g = self.graph.borrow_mut();
1413 if !g.is_live_key(self.arena_key.node) {
1414 return;
1415 }
1416 let (n, e) = g.get_node_and_edges_mut(self.arena_key.node);
1417 reset_wave_flags_inner(n, e);
1418 }
1419
1420 fn boundary_root(&self) -> BoundaryRoot {
1421 BoundaryRoot {
1422 graph_id: self.arena_key.graph,
1423 graph: Rc::downgrade(&self.graph),
1424 }
1425 }
1426}
1427
1428impl Drop for ArenaNodePin {
1429 fn drop(&mut self) {
1430 self.graph.borrow_mut().release_pin(self.arena_key.node);
1431 free_slot_if_unreferenced(&self.graph, self.arena_key.node, &self.refs);
1432 }
1433}
1434
1435pub(crate) struct BatchTarget {
1436 pin: ArenaNodePin,
1437}
1438
1439impl BatchTarget {
1440 pub(crate) fn from_core(core: &Core) -> Option<Self> {
1441 Some(Self {
1442 pin: ArenaNodePin::from_core(core)?,
1443 })
1444 }
1445
1446 pub(crate) fn matches(&self, core: &Core) -> bool {
1447 self.pin.arena_key == core.arena_node_key()
1448 }
1449
1450 pub(crate) fn boundary_root(&self) -> BoundaryRoot {
1451 self.pin.boundary_root()
1452 }
1453
1454 pub(crate) fn commit_batched_wave(&self, wave: Wave<AnyValue>) {
1455 if let Some(core) = self.pin.borrowed_core() {
1456 core.commit_batched_wave(wave);
1457 }
1458 }
1459
1460 pub(crate) fn rollback_batched(&self) {
1461 if let Some(core) = self.pin.borrowed_core() {
1462 core.rollback_batched();
1463 }
1464 }
1465
1466 #[cfg(test)]
1467 fn borrowed_core(&self) -> Option<Core> {
1468 self.pin.borrowed_core()
1469 }
1470}
1471
1472#[derive(Clone)]
1473struct CoreWeak {
1474 graph: Weak<RefCell<GraphCore>>,
1475 key: NodeKey,
1476 refs: Weak<Cell<usize>>,
1477}
1478
1479#[derive(Default)]
1480struct InWaveDepReceiveAction {
1481 emit_teardown_subscribers: bool,
1482 emit_dirty: bool,
1483 do_invalidate: bool,
1484 invalidate_as_invalidate_msg: bool,
1485 had_node_data_before_invalidate: bool,
1486 clear_undirty_after_invalidate: bool,
1487 defer_maybe_run: bool,
1488 maybe_run_decision: MaybeRunDecision,
1489 down_msgs: Option<Vec<Msg>>,
1490 do_settle_after_absorbed_terminal: bool,
1491 fire_demand: bool,
1492}
1493
1494impl InWaveDepReceiveAction {
1495 fn has_work(&self) -> bool {
1496 self.emit_teardown_subscribers
1497 || self.emit_dirty
1498 || self.do_invalidate
1499 || self.defer_maybe_run
1500 || !matches!(self.maybe_run_decision, MaybeRunDecision::Skip)
1501 || self.do_settle_after_absorbed_terminal
1502 || self.fire_demand
1503 || self.down_msgs.as_ref().is_some_and(|msgs| !msgs.is_empty())
1504 }
1505
1506 fn apply(self, core: &Core) {
1507 if self.emit_teardown_subscribers {
1508 core.emit_to_subs(&Message::Teardown);
1509 }
1510 if self.emit_dirty {
1511 core.emit_to_subs(&Message::Dirty);
1512 }
1513 if self.do_invalidate {
1514 if self.invalidate_as_invalidate_msg && self.had_node_data_before_invalidate {
1515 core.down(vec![Message::Invalidate]);
1516 } else {
1517 core.invalidate();
1518 }
1519 }
1520 if self.clear_undirty_after_invalidate {
1521 if self.had_node_data_before_invalidate {
1522 core.with_inner_edges_mut(|_n, e| {
1523 e.wave.emitted_dirty_this_wave = false;
1524 });
1525 } else {
1526 core.down(vec![Message::Resolved]);
1527 }
1528 }
1529 if self.defer_maybe_run {
1530 defer_run_until_delivery_boundary(core);
1531 }
1532 match self.maybe_run_decision {
1533 MaybeRunDecision::Skip => {}
1534 MaybeRunDecision::Passthrough => core.passthrough_emit(),
1535 MaybeRunDecision::Run => {
1536 core.run_wave();
1537 }
1538 }
1539 if let Some(msgs) = self.down_msgs {
1540 core.down(msgs);
1541 }
1542 if self.do_settle_after_absorbed_terminal {
1543 core.settle_after_absorbed_terminal();
1544 }
1545 if self.fire_demand {
1546 core.fire_owed_demand_if_ready();
1547 }
1548 }
1549}
1550
1551enum DownAction {
1552 Emit { msg: Msg, subs: SubscriberSnapshot },
1553 Invalidate { hooks: Vec<Rc<dyn Fn()>> },
1554}
1555
1556impl CoreWeak {
1557 #[cfg(test)]
1558 fn counted_core(&self) -> Option<Core> {
1559 let graph = self.graph.upgrade()?;
1560 let refs = self.refs.upgrade()?;
1561 if refs.get() == 0 {
1562 return None;
1563 }
1564 if !graph.borrow().is_live_key(self.key) {
1565 return None;
1566 }
1567 Some(Core::from_parts(
1568 graph,
1569 self.key.id,
1570 self.key.generation,
1571 refs,
1572 ))
1573 }
1574
1575 #[cfg(test)]
1576 fn borrowed_core(&self) -> Option<Core> {
1577 let graph = self.graph.upgrade()?;
1578 let refs = self.refs.upgrade()?;
1579 let can_borrow = {
1580 let g = graph.borrow();
1581 g.is_live_key(self.key) && (refs.get() != 0 || g.pin_count(self.key) != 0)
1582 };
1583 if !can_borrow {
1584 return None;
1585 }
1586 Some(Core::from_borrowed_parts(
1587 graph,
1588 self.key.id,
1589 self.key.generation,
1590 refs,
1591 ))
1592 }
1593
1594 fn pinned_borrowed_core(&self) -> Option<(ArenaNodePin, Core)> {
1595 let graph = self.graph.upgrade()?;
1596 let refs = self.refs.upgrade()?;
1597 let arena_key = ArenaNodeKey {
1598 graph: Rc::as_ptr(&graph) as usize,
1599 node: self.key,
1600 };
1601 {
1602 let mut g = graph.borrow_mut();
1603 if !g.pin_live_key(self.key) {
1604 return None;
1605 }
1606 }
1607 let pin = ArenaNodePin {
1608 arena_key,
1609 graph,
1610 refs,
1611 };
1612 let core = pin.borrowed_core()?;
1613 Some((pin, core))
1614 }
1615
1616 fn collect_in_wave_receive_from_dep_action(
1617 &self,
1618 idx: usize,
1619 msg: &Msg,
1620 ) -> Option<InWaveDepReceiveAction> {
1621 let graph = self.graph.upgrade()?;
1622 let refs = self.refs.upgrade()?;
1623 let arena_key = ArenaNodeKey {
1624 graph: Rc::as_ptr(&graph) as usize,
1625 node: self.key,
1626 };
1627 let registered = WAVE.with(|w| {
1628 let mut wave = w.borrow_mut();
1629 let Some(scope) = wave.as_mut() else {
1630 return false;
1631 };
1632 scope.pin_once(arena_key, &graph, &refs)
1633 });
1634 if !registered {
1635 return None;
1636 }
1637 let mut g = graph.borrow_mut();
1638 Core::collect_receive_from_dep_action_from_graph(&mut g, self.key, idx, msg)
1639 }
1640
1641 fn apply_in_wave_receive_action(&self, action: InWaveDepReceiveAction) {
1642 let graph = match self.graph.upgrade() {
1643 Some(graph) => graph,
1644 None => return,
1645 };
1646 let refs = match self.refs.upgrade() {
1647 Some(refs) => refs,
1648 None => return,
1649 };
1650 let can_apply = {
1651 let g = graph.borrow();
1652 g.is_live_key(self.key) && (refs.get() != 0 || g.pin_count(self.key) != 0)
1653 };
1654 if !can_apply {
1655 return;
1656 }
1657 let core = Core::from_borrowed_parts(graph, self.key.id, self.key.generation, refs);
1658 core.apply_receive_from_dep_action(action);
1659 }
1660
1661 fn receive_from_dep(&self, idx: usize, msg: &Msg) {
1662 if wave_in_flight() {
1669 let Some(action) = self.collect_in_wave_receive_from_dep_action(idx, msg) else {
1670 return;
1671 };
1672 if action.has_work() {
1673 self.apply_in_wave_receive_action(action);
1674 }
1675 return;
1676 }
1677 if let Some((_pin, core)) = self.pinned_borrowed_core() {
1678 core.receive_from_dep_registered(idx, msg);
1679 }
1680 }
1681}
1682
1683struct DeferredNodeAction {
1684 arena_key: ArenaNodeKey,
1685 weak: CoreWeak,
1686}
1687
1688impl DeferredNodeAction {
1689 fn from_core(core: &Core) -> Self {
1690 Self {
1691 arena_key: core.arena_node_key(),
1692 weak: core.downgrade(),
1693 }
1694 }
1695
1696 fn matches(&self, core: &Core) -> bool {
1697 self.arena_key == core.arena_node_key()
1698 }
1699
1700 fn pinned_borrowed_core(&self) -> Option<(ArenaNodePin, Core)> {
1701 self.weak.pinned_borrowed_core()
1702 }
1703
1704 fn with_live_edges_mut<R>(&self, f: impl FnOnce(&mut DepEdges) -> R) -> Option<R> {
1705 let graph = self.weak.graph.upgrade()?;
1706 let refs = self.weak.refs.upgrade()?;
1707 let mut g = graph.borrow_mut();
1708 if !g.is_live_key(self.weak.key) || (refs.get() == 0 && g.pin_count(self.weak.key) == 0) {
1709 return None;
1710 }
1711 let (_n, e) = g.get_node_and_edges_mut(self.weak.key);
1712 Some(f(e))
1713 }
1714}
1715
1716#[derive(Clone, Copy, PartialEq, Eq)]
1717enum DeferredDeliveryKind {
1718 Run,
1719 AbsorbedSettle,
1720}
1721
1722struct DeferredDeliveryAction {
1723 kind: DeferredDeliveryKind,
1724 target: DeferredNodeAction,
1725}
1726
1727impl DeferredDeliveryAction {
1728 fn from_core(kind: DeferredDeliveryKind, core: &Core) -> Self {
1729 Self {
1730 kind,
1731 target: DeferredNodeAction::from_core(core),
1732 }
1733 }
1734
1735 fn matches(&self, kind: DeferredDeliveryKind, core: &Core) -> bool {
1736 self.kind == kind && self.target.matches(core)
1737 }
1738
1739 fn core_for_drain(&self) -> Option<(ArenaNodePin, Core)> {
1740 self.target.pinned_borrowed_core()
1741 }
1742
1743 fn apply(&self) {
1744 let Some((_pin, node)) = self.core_for_drain() else {
1745 return;
1746 };
1747 if node.with_inner_edges(|_, e| e.value.terminal) {
1748 return;
1749 }
1750 match self.kind {
1751 DeferredDeliveryKind::Run => node.maybe_run(),
1752 DeferredDeliveryKind::AbsorbedSettle => node.settle_after_absorbed_terminal(),
1753 }
1754 }
1755}
1756
1757#[derive(Clone)]
1758struct CoreToken {
1759 graph_id: usize,
1760 key: NodeKey,
1761 refs: Weak<Cell<usize>>,
1762}
1763
1764impl CoreToken {
1765 fn from_core(core: &Core) -> Self {
1766 Self {
1767 graph_id: Rc::as_ptr(&core.graph) as usize,
1768 key: core.key(),
1769 refs: Rc::downgrade(&core.refs),
1770 }
1771 }
1772
1773 fn pinned_borrowed_core(&self, graph: &Rc<RefCell<GraphCore>>) -> Option<PinnedBorrowedCore> {
1774 if Rc::as_ptr(graph) as usize != self.graph_id {
1775 return None;
1776 }
1777 let refs = self.refs.upgrade()?;
1778 {
1779 let mut g = graph.borrow_mut();
1780 if !(g.is_live_key(self.key) && (refs.get() != 0 || g.pin_count(self.key) != 0)) {
1781 return None;
1782 }
1783 if !g.pin_live_key(self.key) {
1784 return None;
1785 }
1786 }
1787 let arena_key = ArenaNodeKey {
1788 graph: Rc::as_ptr(graph) as usize,
1789 node: self.key,
1790 };
1791 Some(PinnedBorrowedCore {
1792 _pin: ArenaNodePin {
1793 arena_key,
1794 graph: graph.clone(),
1795 refs: refs.clone(),
1796 },
1797 core: Core::from_borrowed_parts(graph.clone(), self.key.id, self.key.generation, refs),
1798 })
1799 }
1800}
1801
1802struct BoundaryDrainGuard {
1803 graph: Rc<RefCell<GraphCore>>,
1804 active: bool,
1805}
1806
1807impl BoundaryDrainGuard {
1808 fn enter(graph: &Rc<RefCell<GraphCore>>) -> Self {
1809 graph.borrow_mut().draining_boundary = true;
1810 Self {
1811 graph: graph.clone(),
1812 active: true,
1813 }
1814 }
1815
1816 fn finish(mut self) {
1817 self.active = false;
1818 self.graph.borrow_mut().draining_boundary = false;
1819 }
1820}
1821
1822impl Drop for BoundaryDrainGuard {
1823 fn drop(&mut self) {
1824 if self.active {
1825 self.graph.borrow_mut().draining_boundary = false;
1826 }
1827 }
1828}
1829
1830struct PinnedBorrowedCore {
1831 _pin: ArenaNodePin,
1832 core: Core,
1833}
1834
1835enum BoundaryTask {
1836 Rewire {
1837 target: CoreToken,
1838 req: RewireRequest,
1839 committed: Rc<Cell<bool>>,
1840 },
1841 ExternalRewire {
1842 target: CoreToken,
1843 req: RewireRequest,
1844 committed: Rc<Cell<bool>>,
1845 },
1846 Up {
1847 target: CoreToken,
1848 msgs: Wave<AnyValue>,
1849 toward_dep: Option<usize>,
1850 committed: Rc<Cell<bool>>,
1851 },
1852 Down {
1853 target: CoreToken,
1854 msgs: Wave<AnyValue>,
1855 committed: Rc<Cell<bool>>,
1856 },
1857}
1858
1859impl BoundaryTask {
1860 fn committed(&self) -> bool {
1861 match self {
1862 BoundaryTask::Rewire { committed, .. }
1863 | BoundaryTask::ExternalRewire { committed, .. }
1864 | BoundaryTask::Up { committed, .. }
1865 | BoundaryTask::Down { committed, .. } => committed.get(),
1866 }
1867 }
1868}
1869
1870#[derive(Clone)]
1871pub(crate) struct BoundaryRoot {
1872 graph_id: usize,
1873 graph: Weak<RefCell<GraphCore>>,
1874}
1875
1876impl BoundaryRoot {
1877 fn from_core(core: &Core) -> Self {
1878 Self {
1879 graph_id: Rc::as_ptr(&core.graph) as usize,
1880 graph: Rc::downgrade(&core.graph),
1881 }
1882 }
1883
1884 pub(crate) fn same_graph(&self, core: &Core) -> bool {
1885 self.graph_id == Rc::as_ptr(&core.graph) as usize
1886 }
1887
1888 pub(crate) fn same_root(&self, other: &Self) -> bool {
1889 self.graph_id == other.graph_id
1890 }
1891
1892 fn upgrade(&self) -> Option<Rc<RefCell<GraphCore>>> {
1893 self.graph.upgrade()
1894 }
1895}
1896
1897pub(crate) fn boundary_root_for(target: &Core) -> BoundaryRoot {
1898 BoundaryRoot::from_core(target)
1899}
1900
1901struct WaveGuard(DeferredNodeAction);
1908impl Drop for WaveGuard {
1909 fn drop(&mut self) {
1910 let _ = self.0.with_live_edges_mut(|e| {
1911 e.wave.inside_run_wave = false;
1912 });
1913 }
1914}
1915
1916struct DepMutationGuard(DeferredNodeAction);
1925impl Drop for DepMutationGuard {
1926 fn drop(&mut self) {
1927 let _ = self.0.with_live_edges_mut(|e| {
1928 e.wave.in_dep_mutation = false;
1929 });
1930 }
1931}
1932
1933struct WaveScope {
1953 owner_graph_id: usize,
1954 wave_id: u64,
1957 boundary_roots: Vec<BoundaryRoot>,
1960 touched: Vec<ArenaNodePin>,
1963 blamed: Option<ArenaNodeKey>,
1970}
1971
1972impl WaveScope {
1973 fn pin_once(
1974 &mut self,
1975 arena_key: ArenaNodeKey,
1976 graph: &Rc<RefCell<GraphCore>>,
1977 refs: &Rc<Cell<usize>>,
1978 ) -> bool {
1979 {
1980 let mut g = graph.borrow_mut();
1981 if !g.is_live_key(arena_key.node) {
1982 return false;
1983 }
1984 let id = arena_key.node.id.0;
1985 if g.touched_waves.get(id) == Some(&self.wave_id) {
1986 return true;
1987 }
1988 g.pins[id] = g.pins[id]
1989 .checked_add(1)
1990 .expect("GraphCore pin count overflow");
1991 let touched = g
1992 .touched_waves
1993 .get_mut(id)
1994 .expect("Core points at a live GraphCore touch slot");
1995 *touched = self.wave_id;
1996 }
1997 self.touched.push(ArenaNodePin {
1998 arena_key,
1999 graph: graph.clone(),
2000 refs: refs.clone(),
2001 });
2002 true
2003 }
2004}
2005
2006thread_local! {
2007 static WAVE: RefCell<Option<WaveScope>> = const { RefCell::new(None) };
2010 static NEXT_WAVE_SCOPE_ID: Cell<u64> = const { Cell::new(1) };
2011 static DELIVERY_DEPTH: Cell<usize> = const { Cell::new(0) };
2015 static NEXT_DELIVERY_ID: Cell<u64> = const { Cell::new(1) };
2016 static CURRENT_DELIVERY_ID: Cell<u64> = const { Cell::new(0) };
2017 static DEFERRED_DELIVERY_ACTIONS: RefCell<Vec<DeferredDeliveryAction>> = const { RefCell::new(Vec::new()) };
2018}
2019
2020fn wave_in_flight() -> bool {
2022 WAVE.with(|w| w.borrow().is_some())
2023}
2024
2025fn next_wave_scope_id() -> u64 {
2026 NEXT_WAVE_SCOPE_ID.with(|next| {
2027 let id = next.get();
2028 next.set(id.checked_add(1).expect("wave scope id overflow"));
2029 id
2030 })
2031}
2032
2033fn wave_register(core: &Core) {
2036 WAVE.with(|w| {
2037 if let Some(scope) = w.borrow_mut().as_mut() {
2038 let _ = scope.pin_once(core.arena_node_key(), &core.graph, &core.refs);
2039 }
2040 });
2041}
2042
2043fn wave_register_boundary_root(core: &Core) {
2044 WAVE.with(|w| {
2045 if let Some(scope) = w.borrow_mut().as_mut() {
2046 if scope.owner_graph_id == Rc::as_ptr(&core.graph) as usize
2047 || scope.boundary_roots.iter().any(|r| r.same_graph(core))
2048 {
2049 return;
2050 }
2051 scope.boundary_roots.push(BoundaryRoot::from_core(core));
2052 }
2053 });
2054}
2055
2056fn delivery_in_flight() -> bool {
2057 DELIVERY_DEPTH.with(|depth| depth.get() > 0)
2058}
2059
2060fn current_delivery_id() -> u64 {
2061 CURRENT_DELIVERY_ID.with(Cell::get)
2062}
2063
2064fn defer_run_until_delivery_boundary(core: &Core) {
2065 defer_delivery_action_until_boundary(core, DeferredDeliveryKind::Run);
2066}
2067
2068fn defer_absorbed_settle_until_delivery_boundary(core: &Core) {
2069 defer_delivery_action_until_boundary(core, DeferredDeliveryKind::AbsorbedSettle);
2070}
2071
2072fn defer_delivery_action_until_boundary(core: &Core, kind: DeferredDeliveryKind) {
2073 DEFERRED_DELIVERY_ACTIONS.with(|actions| {
2074 let mut actions = actions.borrow_mut();
2075 if !actions.iter().any(|action| action.matches(kind, core)) {
2076 actions.push(DeferredDeliveryAction::from_core(kind, core));
2077 }
2078 });
2079}
2080
2081struct DeliveryGuard {
2082 active: bool,
2083 prev_id: u64,
2084}
2085
2086impl DeliveryGuard {
2087 fn enter() -> Self {
2088 let prev_id = CURRENT_DELIVERY_ID.with(Cell::get);
2089 let id = NEXT_DELIVERY_ID.with(|next| {
2090 let id = next.get();
2091 next.set(id.wrapping_add(1).max(1));
2092 id
2093 });
2094 CURRENT_DELIVERY_ID.with(|current| current.set(id));
2095 DELIVERY_DEPTH.with(|depth| depth.set(depth.get() + 1));
2096 Self {
2097 active: true,
2098 prev_id,
2099 }
2100 }
2101
2102 fn finish(mut self) {
2103 self.active = false;
2104 CURRENT_DELIVERY_ID.with(|current| current.set(self.prev_id));
2105 let outer = DELIVERY_DEPTH.with(|depth| {
2106 let current = depth.get();
2107 debug_assert!(current > 0);
2108 depth.set(current - 1);
2109 current == 1
2110 });
2111 if outer {
2112 drain_deferred_runs();
2113 }
2114 }
2115}
2116
2117impl Drop for DeliveryGuard {
2118 fn drop(&mut self) {
2119 if self.active {
2120 CURRENT_DELIVERY_ID.with(|current| current.set(self.prev_id));
2121 DELIVERY_DEPTH.with(|depth| {
2122 let current = depth.get();
2123 if current > 0 {
2124 depth.set(current - 1);
2125 }
2126 });
2127 if !std::thread::panicking() && !delivery_in_flight() {
2128 drain_deferred_runs();
2129 }
2130 }
2131 }
2132}
2133
2134fn with_delivery_scope(body: impl FnOnce()) {
2135 let guard = DeliveryGuard::enter();
2136 body();
2137 guard.finish();
2138}
2139
2140fn drain_deferred_runs() {
2141 loop {
2142 let actions = DEFERRED_DELIVERY_ACTIONS.with(|r| std::mem::take(&mut *r.borrow_mut()));
2143 if actions.is_empty() {
2144 break;
2145 }
2146 for kind in [
2147 DeferredDeliveryKind::Run,
2148 DeferredDeliveryKind::AbsorbedSettle,
2149 ] {
2150 for action in &actions {
2151 if action.kind == kind {
2152 action.apply();
2153 }
2154 }
2155 }
2156 }
2157}
2158
2159fn clear_deferred_delivery_actions() {
2160 DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
2161}
2162
2163fn wave_set_blamed(core: &Core) {
2165 WAVE.with(|w| {
2166 if let Some(scope) = w.borrow_mut().as_mut() {
2167 scope.blamed = Some(core.arena_node_key());
2168 }
2169 });
2170}
2171
2172fn panic_to_error(payload: Box<dyn std::any::Any + Send>) -> GraphError {
2174 let msg = payload
2175 .downcast_ref::<&str>()
2176 .map(|s| (*s).to_owned())
2177 .or_else(|| payload.downcast_ref::<String>().cloned())
2178 .unwrap_or_else(|| "node fn panicked".to_owned());
2179 msg.into()
2180}
2181
2182fn with_wave_owner<R>(owner: &Core, body: impl FnOnce() -> R, on_error: impl FnOnce() -> R) -> R {
2189 if wave_in_flight() {
2190 return body();
2191 }
2192 WAVE.with(|w| {
2193 *w.borrow_mut() = Some(WaveScope {
2194 owner_graph_id: Rc::as_ptr(&owner.graph) as usize,
2195 wave_id: next_wave_scope_id(),
2196 boundary_roots: Vec::new(),
2197 touched: Vec::new(),
2198 blamed: None,
2199 });
2200 });
2201 let result = catch_unwind(AssertUnwindSafe(body));
2202 let mut scope = WAVE
2203 .with(|w| w.borrow_mut().take())
2204 .expect("wave owner installed a scope");
2205 let boundary_roots = std::mem::take(&mut scope.boundary_roots);
2206 let ret = match result {
2207 Ok(r) => r,
2208 Err(payload) => {
2209 for n in &scope.touched {
2214 n.reset_wave_flags();
2215 }
2216 clear_deferred_delivery_actions();
2221 if is_host_boundary_abort_payload(payload.as_ref()) {
2222 clear_all_deferred_boundary_root(&owner.boundary_root());
2223 for root in &boundary_roots {
2224 clear_all_deferred_boundary_root(root);
2225 }
2226 std::panic::resume_unwind(payload);
2227 }
2228 let blamed = scope
2231 .blamed
2232 .and_then(|key| {
2233 scope
2234 .touched
2235 .iter()
2236 .find(|n| n.arena_key == key)
2237 .and_then(ArenaNodePin::borrowed_core)
2238 })
2239 .unwrap_or_else(|| owner.borrowed_view());
2240 let err = panic_to_error(payload);
2241 let _ = catch_unwind(AssertUnwindSafe(move || {
2247 blamed.down(vec![Message::Error(err)]);
2248 }));
2249 clear_deferred_delivery_actions();
2250 on_error()
2251 }
2252 };
2253 drop(std::mem::take(&mut scope.touched));
2254 if !boundary_drains_blocked() {
2259 drain_committed_boundaries(std::slice::from_ref(owner), &boundary_roots);
2260 }
2261 ret
2262}
2263
2264struct UpRouteState {
2265 demand_fired: Vec<(LockId, ArenaNodeKey)>,
2266}
2267
2268impl UpRouteState {
2269 fn new() -> Self {
2270 Self {
2271 demand_fired: Vec::new(),
2272 }
2273 }
2274
2275 fn mark_demand(&mut self, lock: &LockId, holder: &Core) -> bool {
2276 let holder_key = holder.arena_node_key();
2277 if self
2278 .demand_fired
2279 .iter()
2280 .any(|(l, h)| l == lock && *h == holder_key)
2281 {
2282 return true;
2283 }
2284 self.demand_fired.push((lock.clone(), holder_key));
2285 false
2286 }
2287}
2288
2289fn defer_boundary(owner: &Core, task: BoundaryTask) {
2292 wave_register_boundary_root(owner);
2293 register_boundary_root(owner);
2294 owner.graph.borrow_mut().deferred_boundary.push_back(task);
2295}
2296
2297pub(crate) fn drain_committed_boundary(target: &Core) {
2298 drain_deferred_rewires(target);
2299}
2300
2301pub(crate) fn drain_committed_boundary_root(root: &BoundaryRoot) {
2302 if let Some(graph) = root.upgrade() {
2303 drain_deferred_rewires_for_graph(&graph);
2304 }
2305}
2306
2307pub(crate) fn drain_committed_boundaries(core_roots: &[Core], task_roots: &[BoundaryRoot]) {
2308 let mut escaped: Option<Box<dyn std::any::Any + Send>> = None;
2309 for root in core_roots {
2310 let result = catch_unwind(AssertUnwindSafe(|| drain_committed_boundary(root)));
2311 if let Err(payload) = result {
2312 if is_host_boundary_abort_payload(payload.as_ref()) {
2313 clear_all_deferred_boundaries(core_roots, task_roots);
2314 std::panic::resume_unwind(payload);
2315 }
2316 remember_first_boundary_panic(&mut escaped, Err(payload));
2317 }
2318 }
2319 for root in task_roots {
2320 let result = catch_unwind(AssertUnwindSafe(|| drain_committed_boundary_root(root)));
2321 if let Err(payload) = result {
2322 if is_host_boundary_abort_payload(payload.as_ref()) {
2323 clear_all_deferred_boundaries(core_roots, task_roots);
2324 std::panic::resume_unwind(payload);
2325 }
2326 remember_first_boundary_panic(&mut escaped, Err(payload));
2327 }
2328 }
2329 if let Some(e) = escaped {
2330 std::panic::resume_unwind(e);
2331 }
2332}
2333
2334fn clear_all_deferred_boundaries(core_roots: &[Core], task_roots: &[BoundaryRoot]) {
2335 for root in core_roots {
2336 clear_all_deferred_boundary_root(&root.boundary_root());
2337 }
2338 for root in task_roots {
2339 clear_all_deferred_boundary_root(root);
2340 }
2341}
2342
2343fn remember_first_boundary_panic(
2344 escaped: &mut Option<Box<dyn std::any::Any + Send>>,
2345 result: std::thread::Result<()>,
2346) {
2347 if let Err(e) = result {
2348 if escaped.is_none() {
2349 *escaped = Some(e);
2350 }
2351 }
2352}
2353
2354pub(crate) fn clear_deferred_boundary_root(root: &BoundaryRoot) {
2355 if let Some(graph) = root.upgrade() {
2356 graph
2357 .borrow_mut()
2358 .deferred_boundary
2359 .retain(BoundaryTask::committed);
2360 }
2361}
2362
2363pub(crate) fn clear_all_deferred_boundary_root(root: &BoundaryRoot) {
2364 if let Some(graph) = root.upgrade() {
2365 graph.borrow_mut().deferred_boundary.clear();
2366 }
2367}
2368
2369fn drain_deferred_rewires(owner: &Core) {
2380 drain_deferred_rewires_for_graph(&owner.graph);
2381}
2382
2383fn drain_deferred_rewires_for_graph(graph: &Rc<RefCell<GraphCore>>) {
2384 if graph.borrow().draining_boundary {
2385 return; }
2387 if graph.borrow().deferred_boundary.is_empty() {
2388 return; }
2390 let drain_guard = BoundaryDrainGuard::enter(graph);
2391 let mut escaped: Option<Box<dyn std::any::Any + Send>> = None;
2392 let mut blocked = 0usize;
2393 loop {
2394 let task = {
2395 let mut g = graph.borrow_mut();
2396 if g.deferred_boundary.is_empty() {
2397 None
2398 } else {
2399 g.deferred_boundary.pop_front()
2400 }
2401 };
2402 let Some(task) = task else { break };
2403 match catch_unwind(AssertUnwindSafe(|| run_boundary_task(graph, task))) {
2404 Ok(Some(task)) => {
2405 let queued = {
2406 let mut g = graph.borrow_mut();
2407 g.deferred_boundary.push_back(task);
2408 g.deferred_boundary.len()
2409 };
2410 blocked += 1;
2411 if blocked >= queued {
2412 break;
2413 }
2414 }
2415 Ok(None) => {
2416 blocked = 0;
2417 }
2418 Err(e) => {
2419 blocked = 0;
2420 if is_host_boundary_abort_payload(e.as_ref()) {
2421 graph.borrow_mut().deferred_boundary.clear();
2422 drain_guard.finish();
2423 std::panic::resume_unwind(e);
2424 }
2425 if escaped.is_none() {
2426 escaped = Some(e);
2427 }
2428 }
2429 }
2430 }
2431 drain_guard.finish();
2432 if let Some(e) = escaped {
2433 std::panic::resume_unwind(e);
2434 }
2435}
2436
2437fn run_boundary_task(graph: &Rc<RefCell<GraphCore>>, task: BoundaryTask) -> Option<BoundaryTask> {
2438 match task {
2439 BoundaryTask::Rewire {
2440 target,
2441 req,
2442 committed,
2443 } => {
2444 if !committed.get() {
2445 return None;
2446 }
2447 if let Some(node) = target.pinned_borrowed_core(graph) {
2448 if node.core.is_paused() {
2449 return Some(BoundaryTask::Rewire {
2450 target,
2451 req,
2452 committed,
2453 });
2454 }
2455 node.core.apply_rewire_next(req);
2456 }
2457 }
2458 BoundaryTask::ExternalRewire {
2459 target,
2460 req,
2461 committed,
2462 } => {
2463 if committed.get() {
2464 if let Some(node) = target.pinned_borrowed_core(graph) {
2465 node.core.apply_external_rewire(req);
2466 }
2467 }
2468 }
2469 BoundaryTask::Up {
2470 target,
2471 msgs,
2472 toward_dep,
2473 committed,
2474 } => {
2475 if !committed.get() {
2476 return None;
2477 }
2478 if let Some(node) = target.pinned_borrowed_core(graph) {
2479 if node.core.is_paused() {
2480 return Some(BoundaryTask::Up {
2481 target,
2482 msgs,
2483 toward_dep,
2484 committed,
2485 });
2486 }
2487 node.core.owned_up(msgs, toward_dep);
2488 }
2489 }
2490 BoundaryTask::Down {
2491 target,
2492 msgs,
2493 committed,
2494 } => {
2495 if committed.get() {
2496 if let Some(node) = target.pinned_borrowed_core(graph) {
2497 node.core.owned_down(msgs);
2498 }
2499 }
2500 }
2501 }
2502 None
2503}
2504
2505impl Core {
2506 pub(crate) fn ptr_eq(&self, other: &Core) -> bool {
2507 Rc::ptr_eq(&self.graph, &other.graph)
2508 && self.id == other.id
2509 && self.generation == other.generation
2510 }
2511
2512 pub(crate) fn same_graph(&self, other: &Core) -> bool {
2513 Rc::ptr_eq(&self.graph, &other.graph)
2514 }
2515
2516 pub(crate) fn same_graph_arena(&self, arena: &GraphArena) -> bool {
2517 Rc::ptr_eq(&self.graph, &arena.0)
2518 }
2519
2520 pub(crate) fn arena(&self) -> GraphArena {
2521 GraphArena(self.graph.clone())
2522 }
2523
2524 pub(crate) fn dispatcher(&self) -> Dispatcher {
2525 self.with_call(|c| c.dispatcher.clone())
2526 }
2527
2528 pub(crate) fn deps(&self) -> Vec<Core> {
2529 self.borrow().deps.clone()
2530 }
2531
2532 pub(crate) fn set_topology_deps_changed_observer(&self, observer: TopologyDepsChangedObserver) {
2533 self.with_node_state_mut(|n, _c, _cfg, _r, _e| {
2534 n.topology_deps_changed = Some(observer);
2535 });
2536 }
2537
2538 fn notify_topology_deps_changed(&self, old_deps: &[Core], new_deps: &[Core]) {
2539 let observer = self.with_inner_edges(|n, _e| n.topology_deps_changed.clone());
2540 if let Some(observer) = observer {
2541 observer(old_deps, new_deps);
2542 }
2543 }
2544
2545 pub(crate) fn cache_any(&self) -> Option<AnyValue> {
2546 self.with_inner_edges(|_n, e| e.value.cache.clone())
2547 }
2548
2549 pub(crate) fn status(&self) -> Status {
2550 self.with_inner_edges(|_n, e| e.value.status)
2551 }
2552
2553 pub(crate) fn version(&self) -> Option<NodeVersion> {
2554 self.graph.borrow().get_version(self.key()).value.clone()
2555 }
2556
2557 pub(crate) fn versioning_policy(&self) -> ResolvedNodeVersioningPolicy {
2558 self.graph.borrow().get_version(self.key()).policy.clone()
2559 }
2560
2561 pub(crate) fn handle(&self) -> Option<Handle> {
2562 self.with_call(|c| c.handle)
2563 }
2564
2565 pub(crate) fn runtime_is_quiescent_for_release(&self) -> bool {
2566 if wave_in_flight() || delivery_in_flight() {
2567 return false;
2568 }
2569 let g = self.graph.borrow();
2570 let key = self.key();
2571 if !g.is_live_key(key) {
2572 return true;
2573 }
2574 if g.pin_count(key) != 0 || g.draining_boundary || !g.deferred_boundary.is_empty() {
2575 return false;
2576 }
2577 let e = g
2578 .edge_slots
2579 .get(key.id.0)
2580 .and_then(Option::as_ref)
2581 .expect("Core points at a live GraphCore edge slot");
2582 let a = g.get_aux(key);
2583 e.value.status != Status::Dirty
2584 && e.value.status != Status::Pending
2585 && e.state.pending == 0
2586 && !e.wave.inside_run_wave
2587 && !e.wave.in_dep_mutation
2588 && !e.wave.rewire_run_pending
2589 && !e.wave.batch_dirty_owed
2590 && a.pull_demand_owed.is_none()
2591 && a.active_pull.is_none()
2592 && a.pause_buffer.is_empty()
2593 && a.pause_lockset.is_empty()
2594 }
2595
2596 pub(crate) fn is_quiescent_for_release(&self) -> bool {
2597 self.runtime_is_quiescent_for_release() && self.subscriber_count() == 0
2598 }
2599
2600 pub(crate) fn release_runtime_for_graph(&self) -> bool {
2601 if !self.is_quiescent_for_release() {
2602 return false;
2603 }
2604 let key = self.key();
2605 let slot = {
2606 let mut graph = self.graph.borrow_mut();
2607 if graph.pin_count(key) != 0 {
2608 return false;
2609 }
2610 let Some(slot) = graph.take_live(key) else {
2611 self.refs.set(0);
2612 return true;
2613 };
2614 slot
2615 };
2616 self.refs.set(0);
2617 struct FreeSlotOnDrop {
2618 graph: Rc<RefCell<GraphCore>>,
2619 id: usize,
2620 armed: bool,
2621 }
2622 impl Drop for FreeSlotOnDrop {
2623 fn drop(&mut self) {
2624 if self.armed {
2625 self.graph.borrow_mut().free.push(self.id);
2626 }
2627 }
2628 }
2629 let mut free_slot = FreeSlotOnDrop {
2630 graph: self.graph.clone(),
2631 id: key.id.0,
2632 armed: true,
2633 };
2634 unregister_backend_state_contributor(self);
2635 let (mut inner, _topology, mut call, _config, _run, mut edges, _version, mut aux) = slot;
2636 inner.cleanup_before_free(&mut call, &mut edges, &mut aux);
2637 free_slot.armed = false;
2638 self.graph.borrow_mut().free.push(key.id.0);
2639 true
2640 }
2641
2642 pub(crate) fn checkpoint_runtime(&self) -> NodeCheckpointRuntime {
2643 let version = self.version();
2644 self.with_node_state_aux_mut(|_n, _c, _cfg, r, e, a| NodeCheckpointRuntime {
2645 cache: e.value.cache.clone(),
2646 has_data: e.value.has_data,
2647 version,
2648 status: e.value.status,
2649 terminal: e.value.terminal,
2650 activated: a.activated,
2651 has_called_fn_once: r.has_called_fn_once,
2652 ctx_state: a.state.clone(),
2653 ctx_state_persist: a.state_persist,
2654 })
2655 }
2656
2657 pub(crate) fn restore_runtime(&self, state: NodeRestoreRuntime) {
2658 let should_activate = state.activated;
2659 let deps = self.with_node_state_aux_mut(|n, _c, _cfg, r, e, a| {
2660 e.value.cache = state.cache;
2661 e.value.has_data = state.has_data;
2662 e.value.status = state.status;
2663 e.value.terminal = state.terminal;
2664 e.wave = NodeWaveState::new();
2665 e.state = DepState::new(e.unsubs.len());
2666 e.restored_activation_handshake =
2667 vec![state.has_data || state.has_called_fn_once; e.unsubs.len()];
2668 r.has_called_fn_once = state.has_called_fn_once;
2669 a.activated = should_activate;
2670 a.state = state.ctx_state;
2671 a.state_persist = state.ctx_state_persist;
2672 a.on_deactivation.clear();
2673 a.on_invalidate.clear();
2674 a.pull_demand_owed = None;
2675 a.active_pull = None;
2676 a.in_deliver_demand = false;
2677 a.pull_dirty_owed = false;
2678 a.pause_lockset.clear();
2679 a.paused_dep_wave_occurred = false;
2680 a.pause_buffer.clear();
2681 n.deps.clone()
2682 });
2683 if should_activate {
2684 for (idx, dep) in deps.iter().enumerate() {
2685 self.subscribe_dep(idx, dep);
2686 }
2687 }
2688 let mut graph = self.graph.borrow_mut();
2689 let version_state = graph.get_version_mut(self.key());
2690 match state.version {
2691 RestoredNodeVersion::Disabled => {
2692 version_state.policy = ResolvedNodeVersioningPolicy::Disabled;
2693 version_state.value = None;
2694 }
2695 RestoredNodeVersion::Version(version) => {
2696 if matches!(version, NodeVersion::V0 { .. }) {
2697 version_state.policy = ResolvedNodeVersioningPolicy::Level0;
2698 }
2699 version_state.value = Some(version);
2700 }
2701 }
2702 }
2703
2704 pub(crate) fn local_async_driver(
2705 &self,
2706 ) -> Option<Rc<dyn crate::async_driver::LocalAsyncDriver>> {
2707 self.with_call(|c| c.environment.local_async_driver())
2708 }
2709
2710 pub(crate) fn environment(&self) -> EnvironmentDrivers {
2711 self.with_call(|c| c.environment.clone())
2712 }
2713
2714 pub(crate) fn set_environment(&self, environment: EnvironmentDrivers) {
2715 self.with_call_mut(|c| {
2716 c.environment = environment;
2717 });
2718 }
2719
2720 pub(crate) fn factory(&self) -> Option<String> {
2721 self.with_call(|c| c.factory.clone())
2722 }
2723
2724 pub(crate) fn identity_key(&self) -> (usize, usize, u64) {
2725 (Rc::as_ptr(&self.graph) as usize, self.id.0, self.generation)
2726 }
2727
2728 fn new_in_arena(
2729 arena: &GraphArena,
2730 deps: Vec<Core>,
2731 handle: Option<Handle>,
2732 dispatcher: Dispatcher,
2733 initial: Option<AnyValue>,
2734 opts: NodeOpts,
2735 ) -> Core {
2736 for dep in &deps {
2737 assert!(
2738 dep.same_graph_arena(arena),
2739 "node construction: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
2740 );
2741 }
2742 let topology = NodeTopologySlot {
2743 deps,
2744 topology_deps_changed: None,
2745 };
2746 let inner = NodeInner;
2747 let mut environment = EnvironmentDrivers::default();
2748 environment.set_local_async_driver(dispatcher.local_async_driver());
2749 let call = NodeCallSlot {
2750 factory: None,
2751 handle,
2752 dispatcher,
2753 environment,
2754 };
2755 let config = NodeConfigSlot::from_opts(&opts);
2756 let version = NodeVersionState::new(config.versioning.clone(), initial.as_ref());
2757 {
2758 let mut g = arena.0.borrow_mut();
2759 let id = g.alloc(inner, topology, call, config, version);
2760 let generation = g.key_for(id).generation;
2761 if let Some(v) = initial {
2763 let (_n, e) = g.get_node_and_edges_mut(NodeKey { id, generation });
2764 e.value.cache = Some(v);
2765 e.value.has_data = true;
2766 e.value.status = Status::Settled;
2767 }
2768 Core {
2769 graph: arena.0.clone(),
2770 id,
2771 generation,
2772 refs: Rc::new(Cell::new(1)),
2773 counted: true,
2774 }
2775 }
2776 }
2777
2778 fn configure_pull(&self, pull_id: Option<LockId>) {
2779 if let Some(id) = pull_id {
2780 self.with_node_state_aux_mut(|_n, _c, cfg, _r, _e, a| {
2781 cfg.pull_id = Some(id);
2782 a.pull_demand_owed = None;
2783 a.active_pull = None;
2784 });
2785 }
2786 }
2787
2788 fn subscribe_recording_id(&self, sink: Sink, id_out: &Cell<Option<u64>>) -> Unsub {
2797 self.subscribe_recording_id_with_kind(sink, SubscriberKind::External, id_out)
2798 }
2799
2800 fn subscribe_recording_id_with_kind(
2801 &self,
2802 sink: Sink,
2803 kind: SubscriberKind,
2804 id_out: &Cell<Option<u64>>,
2805 ) -> Unsub {
2806 let (id, push) = {
2807 self.with_node_state_aux_mut(|_n, _c, cfg, _r, e, a| {
2808 let id = a.next_sub_id;
2809 a.next_sub_id += 1;
2810 a.subscribers.push(SubscriberEntry {
2811 id,
2812 kind,
2813 sink: sink.clone(),
2814 });
2815 let push = if cfg.pull_id.is_some() {
2816 None
2817 } else if e.value.has_data {
2818 Some(Message::Data(
2819 e.value.cache.clone().expect("has_data ⇒ cache present"),
2820 ))
2821 } else if e.value.status == Status::Dirty {
2822 Some(Message::Dirty)
2823 } else {
2824 None
2825 };
2826 (id, push)
2827 })
2828 };
2829 id_out.set(Some(id));
2830 sink(&Message::Start);
2831 if let Some(m) = push {
2832 sink(&m);
2833 }
2834 if !self.with_aux(|a| a.activated) {
2835 self.activate();
2836 }
2837 let core = self.clone();
2838 Box::new(move || core.unsubscribe(id))
2839 }
2840
2841 fn unsubscribe(&self, id: u64) {
2842 if !self.graph.borrow().is_live_key(self.key()) {
2843 return;
2844 }
2845 let became_empty = self.with_aux_mut(|a| {
2846 let before = a.subscribers.len();
2847 a.subscribers.retain(|entry| entry.id != id);
2848 a.subscribers.len() < before && a.subscribers.is_empty()
2849 });
2850 if became_empty {
2851 self.deactivate();
2852 }
2853 }
2854
2855 pub(crate) fn external_subscriber_count_for_release(&self) -> usize {
2856 self.with_aux(|a| {
2857 a.subscribers
2858 .iter()
2859 .filter(|entry| entry.kind != SubscriberKind::GraphObserver)
2860 .count()
2861 })
2862 }
2863
2864 pub(crate) fn detach_graph_observer_subscribers_for_release(&self) -> usize {
2865 if !self.graph.borrow().is_live_key(self.key()) {
2866 return 0;
2867 }
2868 self.with_aux_mut(|a| {
2869 let before = a.subscribers.len();
2870 a.subscribers
2871 .retain(|entry| entry.kind != SubscriberKind::GraphObserver);
2872 before - a.subscribers.len()
2873 })
2874 }
2875
2876 fn activate(&self) {
2879 let deps = self.with_node_state_aux_mut(|n, _c, _cfg, _r, e, a| {
2880 a.activated = true;
2881 e.unsubs = (0..n.deps.len()).map(|_| None).collect();
2882 e.idx_boxes = (0..n.deps.len())
2885 .map(|_| Rc::new(Cell::new(-1i64)))
2886 .collect();
2887 n.deps.clone()
2888 });
2889 for (idx, dep) in deps.iter().enumerate() {
2890 self.subscribe_dep(idx, dep);
2891 }
2892 let run = self.with_node_state(|n, c, _cfg, r, _e| {
2894 n.deps.is_empty() && c.handle.is_some() && !r.has_called_fn_once
2895 });
2896 if run {
2897 self.run_wave();
2898 }
2899 }
2900
2901 fn subscribe_dep(&self, idx: usize, dep: &Core) {
2902 let weak = self.downgrade();
2903 let idx_box: Rc<Cell<i64>> = Rc::new(Cell::new(idx as i64));
2906 let cb_box = idx_box.clone();
2907 let sink: Sink = Rc::new(move |msg: &Msg| {
2908 let i = cb_box.get();
2909 if i < 0 {
2910 return; }
2912 weak.receive_from_dep(i as usize, msg);
2913 });
2914 let dep_sub_id = Cell::new(None);
2917 let subscribed = catch_unwind(AssertUnwindSafe(|| {
2918 dep.subscribe_recording_id(sink, &dep_sub_id)
2919 }));
2920 let unsub = match subscribed {
2921 Ok(unsub) => unsub,
2922 Err(payload) => {
2923 if let Some(id) = dep_sub_id.get() {
2924 if self.with_inner_edges(|_, e| e.wave.in_dep_mutation) {
2925 let dep = dep.clone();
2926 let _ = catch_unwind(AssertUnwindSafe(move || dep.unsubscribe(id)));
2927 }
2928 }
2929 std::panic::resume_unwind(payload);
2930 }
2931 };
2932 self.with_inner_edges_mut(|_, e| {
2933 e.unsubs[idx] = Some(unsub);
2934 e.idx_boxes[idx] = idx_box;
2935 e.restored_activation_handshake[idx] = false;
2936 });
2937 }
2938
2939 fn deactivate(&self) {
2940 let (unsubs, hooks) = self.with_node_state_aux_mut(|n, c, _cfg, r, e, a| {
2941 a.activated = false;
2942 let unsubs: Vec<Unsub> = e.unsubs.drain(..).flatten().collect();
2943 e.idx_boxes.clear();
2944 let hooks: Vec<Box<dyn FnOnce()>> = std::mem::take(&mut a.on_deactivation);
2945 let is_compute = c.handle.is_some() || !n.deps.is_empty();
2948 if is_compute {
2949 e.value.cache = None;
2950 e.value.has_data = false;
2951 e.value.status = Status::Sentinel;
2952 }
2953 for i in 0..n.deps.len() {
2954 e.state.batch[i] = None;
2955 e.state.batch_waves[i].clear();
2956 e.state.batch_wave_id[i] = None;
2957 e.state.prev[i] = None;
2958 e.state.has_data[i] = false;
2959 e.state.dirty[i] = false;
2960 e.state.tier[i] = 0;
2961 e.state.terminal[i] = None;
2962 e.state.terminal_wave[i] = None;
2963 }
2964 e.restored_activation_handshake.fill(false);
2965 e.state.pending = 0;
2966 e.wave.emitted_dirty_this_wave = false;
2967 r.has_called_fn_once = false;
2968 a.on_invalidate.clear();
2971 a.pause_lockset.clear();
2972 a.pull_demand_owed = None;
2973 a.active_pull = None;
2974 a.in_deliver_demand = false;
2975 a.pull_dirty_owed = false;
2976 a.paused_dep_wave_occurred = false;
2977 a.pause_buffer.clear();
2978 if !a.state_persist {
2979 a.state = None;
2980 }
2981 (unsubs, hooks)
2982 });
2983 for u in unsubs {
2984 u();
2985 }
2986 for h in hooks {
2987 h();
2988 }
2989 }
2990
2991 #[cfg(test)]
2994 fn receive_from_dep(&self, idx: usize, msg: &Msg) {
2995 wave_register(self);
3000 self.receive_from_dep_registered(idx, msg);
3001 }
3002
3003 fn receive_from_dep_registered(&self, idx: usize, msg: &Msg) {
3004 let Some(action) = self.collect_receive_from_dep_action(idx, msg) else {
3005 return;
3006 };
3007 if action.has_work() {
3008 self.apply_receive_from_dep_action(action);
3009 }
3010 }
3011
3012 fn collect_receive_from_dep_action(
3013 &self,
3014 idx: usize,
3015 msg: &Msg,
3016 ) -> Option<InWaveDepReceiveAction> {
3017 let mut g = self.graph.borrow_mut();
3018 Self::collect_receive_from_dep_action_from_graph(&mut g, self.key(), idx, msg)
3019 }
3020
3021 fn decide_maybe_run_from_graph(g: &mut GraphCore, key: NodeKey) -> MaybeRunDecision {
3022 let (_n, c, cfg, r, e, a) = g.get_node_call_config_run_edges_aux_mut(key);
3023
3024 if e.wave.in_dep_mutation {
3027 e.wave.rewire_run_pending = true;
3028 return MaybeRunDecision::Skip;
3029 }
3030
3031 let pull_quiet = cfg.pull_id.is_some() && a.active_pull.is_none();
3036 if matches!(cfg.pausable, Pausable::True) && (!a.pause_lockset.is_empty() || pull_quiet) {
3037 a.paused_dep_wave_occurred = true;
3038 return MaybeRunDecision::Skip;
3039 }
3040 if e.state.pending > 0 {
3041 return MaybeRunDecision::Skip;
3042 }
3043 if c.handle.is_none() {
3044 return MaybeRunDecision::Passthrough;
3045 }
3046 if !(r.has_called_fn_once || cfg.partial || cfg.all_deps_settled(&e.state)) {
3047 return MaybeRunDecision::Skip;
3048 }
3049
3050 MaybeRunDecision::Run
3051 }
3052
3053 fn set_maybe_run_action(g: &mut GraphCore, key: NodeKey, action: &mut InWaveDepReceiveAction) {
3054 if delivery_in_flight() {
3055 action.defer_maybe_run = true;
3056 } else {
3057 action.maybe_run_decision = Self::decide_maybe_run_from_graph(g, key);
3058 }
3059 }
3060
3061 fn apply_receive_from_dep_action(&self, action: InWaveDepReceiveAction) {
3062 action.apply(self);
3063 }
3064
3065 fn collect_receive_from_dep_action_from_graph(
3066 g: &mut GraphCore,
3067 key: NodeKey,
3068 idx: usize,
3069 msg: &Msg,
3070 ) -> Option<InWaveDepReceiveAction> {
3071 let (pausable, pull_id, auto_error, auto_complete, terminal_as_real_input) = {
3072 let cfg = g.get_config(key);
3073 (
3074 cfg.pausable,
3075 cfg.pull_id.clone(),
3076 cfg.error_when_deps_error,
3077 cfg.complete_when_deps_complete,
3078 cfg.terminal_as_real_input,
3079 )
3080 };
3081 if g.get_node_and_edges_mut(key).1.value.terminal {
3082 if matches!(msg, Message::Teardown) {
3083 return Some(InWaveDepReceiveAction {
3084 emit_teardown_subscribers: true,
3085 ..Default::default()
3086 });
3087 }
3088 return None;
3089 }
3090
3091 let mut action = InWaveDepReceiveAction::default();
3092 match msg {
3093 Message::Start => {}
3094 Message::Invalidate => {
3095 let (had_data, undirty_no_settle, paused) = {
3096 let (_n, e, a) = g.get_node_edges_aux_mut(key);
3097 let pull_quiet = pull_id.is_some() && a.active_pull.is_none();
3098 e.state.prev[idx] = None;
3099 e.state.has_data[idx] = false;
3100 e.state.batch[idx] = None;
3101 let had_current_projection = e.state.record_wave_sentinel(idx);
3102 if !had_current_projection && !e.state.has_run_triggering_projection() {
3103 e.state.clear_wave_projection(idx);
3104 }
3105 if e.state.dirty[idx] {
3106 e.state.dirty[idx] = false;
3107 e.state.pending -= 1;
3108 }
3109 if a.paused_dep_wave_occurred && e.state.batch.iter().all(|b| b.is_none()) {
3110 a.paused_dep_wave_occurred = false;
3111 }
3112 (
3113 e.value.has_data,
3114 e.state.pending == 0 && e.wave.emitted_dirty_this_wave,
3115 !a.pause_lockset.is_empty() || pull_quiet,
3116 )
3117 };
3118 let buffer_own_invalidate = paused && matches!(pausable, Pausable::ResumeAll);
3119 action.had_node_data_before_invalidate = had_data;
3120 action.do_invalidate = true;
3121 action.invalidate_as_invalidate_msg = buffer_own_invalidate && had_data;
3122 if undirty_no_settle {
3123 if !had_data {
3124 action.down_msgs = Some(vec![Message::Resolved]);
3125 } else {
3126 action.clear_undirty_after_invalidate = true;
3127 }
3128 }
3129 action.fire_demand = true;
3130 }
3131 Message::Dirty => {
3132 action.emit_dirty = {
3133 let (_n, e, a) = g.get_node_edges_aux_mut(key);
3134 if e.state.dirty[idx] {
3135 false
3136 } else {
3137 e.state.dirty[idx] = true;
3138 e.state.pending += 1;
3139 e.state.tier[idx] = 2;
3140 let pull_quiet = pull_id.is_some() && a.active_pull.is_none();
3141 if pull_quiet || e.wave.emitted_dirty_this_wave {
3142 false
3143 } else {
3144 e.wave.emitted_dirty_this_wave = true;
3145 e.value.status = Status::Dirty;
3146 true
3147 }
3148 }
3149 };
3150 }
3151 Message::Data(v) => {
3152 if g.get_node_and_edges_mut(key)
3153 .1
3154 .restored_activation_handshake[idx]
3155 {
3156 let (_n, e) = g.get_node_and_edges_mut(key);
3157 e.state.prev[idx] = Some(v.clone());
3158 e.state.has_data[idx] = true;
3159 e.state.tier[idx] = 3;
3160 e.state.batch[idx] = None;
3161 return None;
3162 }
3163 let pending_drained = {
3164 let (_n, e, a) = g.get_node_edges_aux_mut(key);
3165 match &mut e.state.batch[idx] {
3166 Some(b) => b.push(v.clone()),
3167 none => *none = Some(vec![v.clone()]),
3168 }
3169 e.state.record_wave_data(idx, v.clone());
3170 e.state.has_data[idx] = true;
3171 e.state.tier[idx] = 3;
3172 if e.state.dirty[idx] {
3173 e.state.dirty[idx] = false;
3174 e.state.pending -= 1;
3175 }
3176 let pending_drained = e.state.pending == 0;
3177 if !pending_drained
3178 && matches!(pausable, Pausable::True)
3179 && !e.wave.in_dep_mutation
3180 && (!a.pause_lockset.is_empty()
3181 || (pull_id.is_some() && a.active_pull.is_none()))
3182 {
3183 a.paused_dep_wave_occurred = true;
3184 }
3185 pending_drained
3186 };
3187 if pending_drained {
3188 Self::set_maybe_run_action(g, key, &mut action);
3189 action.fire_demand = true;
3190 }
3191 }
3192 Message::Resolved => {
3193 let pending_drained = {
3194 let (_n, e, a) = g.get_node_edges_aux_mut(key);
3195 e.state.record_resolved_wave(idx);
3196 e.state.tier[idx] = 3;
3197 if e.state.dirty[idx] {
3198 e.state.dirty[idx] = false;
3199 e.state.pending -= 1;
3200 }
3201 let pending_drained = e.state.pending == 0;
3202 if !pending_drained
3203 && matches!(pausable, Pausable::True)
3204 && !e.wave.in_dep_mutation
3205 && (!a.pause_lockset.is_empty()
3206 || (pull_id.is_some() && a.active_pull.is_none()))
3207 {
3208 a.paused_dep_wave_occurred = true;
3209 }
3210 pending_drained
3211 };
3212 if pending_drained {
3213 Self::set_maybe_run_action(g, key, &mut action);
3214 action.fire_demand = true;
3215 }
3216 }
3217 m if m.tier() == Tier::Terminal => {
3218 let dep_term = match m {
3221 Message::Error(e) => DepTerminal::Error(format!("{e}").into()),
3222 _ => DepTerminal::Complete,
3223 };
3224 let is_error = matches!(dep_term, DepTerminal::Error(_));
3225 let all_deps_terminal = {
3226 let (n, e) = g.get_node_and_edges_mut(key);
3227 e.state.terminal[idx] = Some(dep_term.clone());
3228 e.state.terminal_wave[idx] = Some(dep_term.clone());
3229 if e.state.dirty[idx] {
3230 e.state.dirty[idx] = false;
3231 e.state.pending -= 1;
3232 }
3233 !n.deps.is_empty() && e.state.terminal.iter().all(|t| t.is_some())
3234 };
3235 if is_error && auto_error {
3236 if let DepTerminal::Error(s) = &dep_term {
3237 action.down_msgs = Some(vec![Message::Error(s.to_string().into())]);
3238 }
3239 } else if terminal_as_real_input {
3240 Self::set_maybe_run_action(g, key, &mut action);
3241 } else if auto_complete && all_deps_terminal {
3242 action.down_msgs = Some(vec![Message::Complete]);
3243 } else {
3244 action.do_settle_after_absorbed_terminal = true;
3245 }
3246 action.fire_demand = true;
3247 }
3248 Message::Teardown => {
3249 action.down_msgs = Some(vec![Message::Complete, Message::Teardown]);
3250 }
3251 _ => {}
3254 }
3255 if action.has_work() {
3256 Some(action)
3257 } else {
3258 None
3259 }
3260 }
3261
3262 fn settle_after_absorbed_terminal(&self) {
3270 if delivery_in_flight() {
3271 defer_absorbed_settle_until_delivery_boundary(self);
3272 return;
3273 }
3274 if !self.with_inner_edges(|_, e| e.state.pending == 0 && e.wave.emitted_dirty_this_wave) {
3275 return;
3276 }
3277 let saw_data = self.with_inner_edges(|_n, e| {
3281 e.state
3282 .batch
3283 .iter()
3284 .any(|b| b.as_ref().is_some_and(|v| !v.is_empty()))
3285 });
3286 if saw_data {
3287 self.maybe_run();
3288 }
3289 let still_owes = self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave);
3297 if still_owes {
3298 self.down(vec![Message::Resolved]);
3299 }
3300 }
3301
3302 fn mark_dirty(&self) {
3303 let subs = self.with_inner_edges_aux_mut(|_n, e, a| {
3304 e.value.status = Status::Dirty;
3305 if !e.wave.emitted_dirty_this_wave {
3306 e.wave.emitted_dirty_this_wave = true;
3307 snapshot_subscribers(a)
3308 } else {
3309 SubscriberSnapshot::Empty
3310 }
3311 });
3312 deliver_subscriber_snapshot(subs, &Message::Dirty);
3313 }
3314
3315 fn maybe_run(&self) {
3316 if delivery_in_flight() {
3317 defer_run_until_delivery_boundary(self);
3318 return;
3319 }
3320 let decision = {
3321 let mut g = self.graph.borrow_mut();
3322 Self::decide_maybe_run_from_graph(&mut g, self.key())
3323 };
3324 match decision {
3325 MaybeRunDecision::Skip => {}
3326 MaybeRunDecision::Passthrough => self.passthrough_emit(),
3327 MaybeRunDecision::Run => self.run_wave(),
3328 }
3329 }
3330
3331 fn try_run(&self) {
3332 let (pending, has_handle, has_called, partial, all_settled) =
3333 self.with_node_state(|_n, c, cfg, r, e| {
3334 (
3335 e.state.pending,
3336 c.handle.is_some(),
3337 r.has_called_fn_once,
3338 cfg.partial,
3339 cfg.all_deps_settled(&e.state),
3340 )
3341 });
3342 if pending > 0 {
3343 return;
3344 }
3345 if !has_handle {
3346 self.passthrough_emit();
3347 return;
3348 }
3349 if !has_called {
3350 if partial || all_settled {
3352 self.run_wave();
3353 }
3354 return;
3355 }
3356 self.run_wave();
3357 }
3358
3359 fn passthrough_emit(&self) {
3360 let v = self.with_inner_edges_mut(|_n, e| {
3362 let v = e.state.batch[0].as_ref().and_then(|b| b.last().cloned());
3363 if let Some(v) = &v {
3364 e.state.prev[0] = Some(v.clone());
3365 }
3366 e.state.batch[0] = None;
3367 e.state.batch_waves[0].clear();
3368 e.state.batch_wave_id[0] = None;
3369 v
3370 });
3371 if let Some(v) = v {
3372 self.down(vec![Message::Data(v)]);
3373 }
3374 }
3375
3376 pub(crate) fn rewire(&self, new_deps: Vec<Core>, fn_: NodeFn) {
3387 self.external_rewire(RewireRequest::Set(new_deps, fn_));
3388 }
3389
3390 fn external_rewire(&self, req: RewireRequest) {
3391 req.validate_deps_same_graph(self);
3392 if let Some(committed) = committed_after_batch_for_target(self) {
3393 defer_boundary(
3394 self,
3395 BoundaryTask::ExternalRewire {
3396 target: CoreToken::from_core(self),
3397 req,
3398 committed,
3399 },
3400 );
3401 return;
3402 }
3403 self.apply_external_rewire(req);
3404 }
3405
3406 fn apply_external_rewire(&self, req: RewireRequest) {
3407 let current = self.deps();
3408 let (new_deps, fn_) = req.into_deps_and_fn(current);
3409 self.rewire_inner(new_deps, fn_, false);
3410 }
3411
3412 fn project_pending_external_rewire_deps(&self) -> Vec<Core> {
3413 let target = CoreToken::from_core(self);
3414 let mut deps = self.deps();
3415 let graph = self.graph.borrow();
3416 for task in &graph.deferred_boundary {
3417 let BoundaryTask::ExternalRewire {
3418 target: queued_target,
3419 req,
3420 ..
3421 } = task
3422 else {
3423 continue;
3424 };
3425 if queued_target.graph_id == target.graph_id && queued_target.key == target.key {
3426 deps = req.project_deps(deps);
3427 }
3428 }
3429 deps
3430 }
3431
3432 fn rewire_inner(&self, new_deps: Vec<Core>, fn_: NodeFn, allow_terminal_owner: bool) {
3433 let new_deps = dedup_cores(new_deps);
3434 for dep in &new_deps {
3435 assert!(
3436 dep.same_graph(self),
3437 "rewire: dep belongs to a different graph; cross-graph deps require a wire bridge (D22/R-graph-domain)"
3438 );
3439 }
3440 if !allow_terminal_owner {
3441 if let Some(committed) = committed_after_batch_for_target(self) {
3442 defer_boundary(
3443 self,
3444 BoundaryTask::ExternalRewire {
3445 target: CoreToken::from_core(self),
3446 req: RewireRequest::Set(new_deps, fn_),
3447 committed,
3448 },
3449 );
3450 return;
3451 }
3452 }
3453 {
3454 let (terminal, inside_run_wave, in_dep_mutation) = self.with_inner_edges(|_n, e| {
3455 (
3456 e.value.terminal,
3457 e.wave.inside_run_wave,
3458 e.wave.in_dep_mutation,
3459 )
3460 });
3461 assert!(
3462 allow_terminal_owner || !terminal,
3463 "rewire: node is terminal (completed/errored) — cannot rewire (R-rewire / D42)"
3464 );
3465 assert!(
3466 !inside_run_wave,
3467 "rewire: mid-fn topology mutation — a fn mutating its own deps mid-wave is the feedback cycle (R-rewire / D37)"
3468 );
3469 assert!(
3470 !in_dep_mutation,
3471 "rewire: reentrant dep mutation — another replace_deps/subscribe_dep/unsubscribe_dep is in flight (R-rewire)"
3472 );
3473 }
3474 assert!(
3475 !new_deps.iter().any(|d| d.ptr_eq(self)),
3476 "rewire: self-dependency rejected (R-rewire / D42)"
3477 );
3478 let old_deps = self.borrow().deps.clone();
3479 for d in new_deps
3480 .iter()
3481 .filter(|d| !old_deps.iter().any(|o| o.ptr_eq(d)))
3482 {
3483 assert!(
3484 !reachable_upstream(d, self),
3485 "rewire: would create a cycle — dep already transitively depends on this node (R-rewire / D42)"
3486 );
3487 assert!(
3490 !d.with_inner_edges(|_n, e| e.value.terminal),
3491 "rewire: cannot add a non-resubscribable terminal dep — would wedge (R-rewire / D42)"
3492 );
3493 }
3494 with_wave_owner(self, || self.rewire_apply(new_deps, fn_), || {});
3495 }
3496
3497 pub(crate) fn request_rewire_next(&self, req: RewireRequest) {
3504 defer_boundary(
3505 self,
3506 BoundaryTask::Rewire {
3507 target: CoreToken::from_core(self),
3508 req,
3509 committed: active_batch_committed_token()
3510 .unwrap_or_else(|| Rc::new(Cell::new(true))),
3511 },
3512 );
3513 }
3514
3515 pub(crate) fn request_up_next(&self, msgs: Wave<AnyValue>, toward_dep: Option<usize>) {
3516 defer_boundary(
3517 self,
3518 BoundaryTask::Up {
3519 target: CoreToken::from_core(self),
3520 msgs,
3521 toward_dep,
3522 committed: active_batch_committed_token()
3523 .unwrap_or_else(|| Rc::new(Cell::new(true))),
3524 },
3525 );
3526 }
3527
3528 pub(crate) fn request_down_next(&self, msgs: Wave<AnyValue>) {
3529 defer_boundary(
3530 self,
3531 BoundaryTask::Down {
3532 target: CoreToken::from_core(self),
3533 msgs,
3534 committed: active_batch_committed_token()
3535 .unwrap_or_else(|| Rc::new(Cell::new(true))),
3536 },
3537 );
3538 }
3539
3540 fn apply_rewire_next(&self, req: RewireRequest) {
3546 let (new_deps, fn_) = match req {
3547 RewireRequest::Set(deps, f) => (deps, f),
3548 RewireRequest::Add(dep, f) => {
3549 let mut next = self.deps();
3550 if !next.iter().any(|d| d.ptr_eq(&dep)) {
3551 next.push(dep);
3552 }
3553 (next, f)
3554 }
3555 RewireRequest::Remove(dep, f) => {
3556 let current = self.borrow().deps.clone();
3557 if !current.iter().any(|d| dep.matches(d)) {
3558 return;
3559 }
3560 let next: Vec<Core> = current.into_iter().filter(|d| !dep.matches(d)).collect();
3561 (next, f)
3562 }
3563 };
3564 let outcome = catch_unwind(AssertUnwindSafe(|| self.rewire_inner(new_deps, fn_, true)));
3569 if let Err(payload) = outcome {
3570 if is_host_boundary_abort_payload(payload.as_ref()) {
3571 resume_unwind(payload);
3572 }
3573 let err = panic_to_error(payload);
3574 self.owned_down(vec![Message::Error(err)]);
3575 }
3576 }
3577
3578 fn rewire_apply(&self, new_deps: Vec<Core>, fn_: NodeFn) {
3584 let old_deps = self.borrow().deps.clone();
3585 if same_ordered_deps(&old_deps, &new_deps) {
3586 self.with_inner_edges_mut(|_, e| {
3587 e.wave.in_dep_mutation = true;
3588 e.wave.rewire_run_pending = false;
3589 });
3590 let _mut_guard = DepMutationGuard(DeferredNodeAction::from_core(self));
3591 self.swap_fn_preserving_pool(fn_);
3592 self.with_inner_edges_mut(|_, e| {
3593 e.wave.in_dep_mutation = false;
3594 });
3595 return;
3596 }
3597 if old_deps.len() == new_deps.len() && Self::same_graph_no_overlap(&old_deps, &new_deps) {
3598 let n = new_deps.len();
3602 self.with_inner_edges_mut(|_, e| {
3603 e.wave.in_dep_mutation = true;
3604 e.wave.rewire_run_pending = false;
3605 });
3606 let _mut_guard = DepMutationGuard(DeferredNodeAction::from_core(self));
3607 let activated = self.with_aux(|a| a.activated);
3608 let mut removed_dirty_contributor = false;
3609
3610 self.swap_fn_preserving_pool(fn_);
3611
3612 for old_idx in 0..old_deps.len() {
3613 let unsub = {
3614 let mut graph = self.graph.borrow_mut();
3615 let (_nn, edges) = graph.get_node_and_edges_mut(self.key());
3616 if edges.state.dirty[old_idx] {
3617 removed_dirty_contributor = true;
3618 edges.state.pending -= 1;
3619 }
3620 if let Some(box_ref) = edges.idx_boxes.get(old_idx) {
3621 box_ref.set(-1);
3622 }
3623 edges.unsubs.get_mut(old_idx).and_then(|u| u.take())
3624 };
3625 if let Some(unsub) = unsub {
3626 unsub();
3627 }
3628 }
3629
3630 let new_batch: Vec<Option<Vec<AnyValue>>> = (0..n).map(|_| None).collect();
3631 let new_batch_waves: Vec<Vec<Vec<WaveData>>> = (0..n).map(|_| Vec::new()).collect();
3632 let new_batch_wave_id: Vec<Option<u64>> = (0..n).map(|_| None).collect();
3633 let new_prev: Vec<Option<AnyValue>> = (0..n).map(|_| None).collect();
3634 let new_tier: Vec<u8> = (0..n).map(|_| 0).collect();
3635 let new_terminal: Vec<Option<DepTerminal>> = (0..n).map(|_| None).collect();
3636 let new_terminal_wave: Vec<Option<DepTerminal>> = (0..n).map(|_| None).collect();
3637 let new_has: Vec<bool> = (0..n).map(|_| false).collect();
3638 let new_dirty: Vec<bool> = (0..n).map(|_| false).collect();
3639 let new_unsubs: Vec<Option<Unsub>> = (0..n).map(|_| None).collect();
3640 let new_boxes: Vec<Rc<Cell<i64>>> = (0..n).map(|_| Rc::new(Cell::new(-1i64))).collect();
3641 let new_restored_handshake: Vec<bool> = vec![false; n];
3642 self.with_inner_edges_mut(|nn, e| {
3643 nn.deps = new_deps.clone();
3644 e.state.batch = new_batch;
3645 e.state.batch_waves = new_batch_waves;
3646 e.state.batch_wave_id = new_batch_wave_id;
3647 e.state.prev = new_prev;
3648 e.state.has_data = new_has;
3649 e.state.dirty = new_dirty;
3650 e.state.tier = new_tier;
3651 e.state.terminal = new_terminal;
3652 e.state.terminal_wave = new_terminal_wave;
3653 e.unsubs = new_unsubs;
3654 e.idx_boxes = new_boxes;
3655 e.restored_activation_handshake = new_restored_handshake;
3656 });
3657
3658 if activated {
3659 for (idx, dep) in new_deps.iter().enumerate() {
3660 self.subscribe_dep(idx, dep);
3661 }
3662 }
3663 self.notify_topology_deps_changed(&old_deps, &new_deps);
3664
3665 if self.with_inner_edges(|_, e| e.value.terminal) {
3666 return;
3667 }
3668
3669 let should_rewire_settle = self.with_inner_edges(|_, e| {
3670 removed_dirty_contributor && e.state.pending == 0 && e.value.status == Status::Dirty
3671 });
3672 let mut zero_dep_undirty = false;
3673 if should_rewire_settle {
3674 if n == 0 {
3675 zero_dep_undirty = true;
3676 } else {
3677 self.with_inner_edges_mut(|_, e| {
3678 e.wave.rewire_run_pending = true;
3679 });
3680 }
3681 }
3682
3683 self.with_inner_edges_mut(|_, e| {
3684 e.wave.in_dep_mutation = false;
3685 });
3686
3687 let run_pending =
3688 self.with_inner_edges_mut(|_, e| std::mem::take(&mut e.wave.rewire_run_pending));
3689 if run_pending {
3690 self.settle_rewire();
3691 } else if zero_dep_undirty {
3692 let emitted = self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave);
3693 if emitted {
3694 self.down(vec![Message::Resolved]);
3695 } else {
3696 self.with_inner_edges_mut(|_n, e| {
3697 e.value.status = if e.value.has_data {
3698 Status::Settled
3699 } else {
3700 Status::Sentinel
3701 };
3702 });
3703 }
3704 }
3705 return;
3706 }
3707 let n = new_deps.len();
3708 let mut old_to_new: Vec<Option<usize>> = vec![None; old_deps.len()];
3709 let mut new_to_old: Vec<Option<usize>> = vec![None; n];
3710 if old_deps.len().saturating_mul(n) <= 64 {
3711 for (old_idx, old_dep) in old_deps.iter().enumerate() {
3712 for (new_idx, new_dep) in new_deps.iter().enumerate() {
3713 if old_dep.ptr_eq(new_dep) && new_to_old[new_idx].is_none() {
3714 old_to_new[old_idx] = Some(new_idx);
3715 new_to_old[new_idx] = Some(old_idx);
3716 break;
3717 }
3718 }
3719 }
3720 } else {
3721 let mut first_old_by_key: HashMap<ArenaNodeKey, usize> =
3722 HashMap::with_capacity(old_deps.len());
3723 for (old_idx, old_dep) in old_deps.iter().enumerate() {
3724 first_old_by_key
3725 .entry(old_dep.arena_node_key())
3726 .or_insert(old_idx);
3727 }
3728 for (new_idx, new_dep) in new_deps.iter().enumerate() {
3729 if let Some(old_idx) = first_old_by_key.remove(&new_dep.arena_node_key()) {
3730 old_to_new[old_idx] = Some(new_idx);
3731 new_to_old[new_idx] = Some(old_idx);
3732 }
3733 }
3734 }
3735 let added: Vec<(usize, Core)> = new_deps
3736 .iter()
3737 .enumerate()
3738 .filter(|(idx, _)| new_to_old[*idx].is_none())
3739 .map(|(idx, dep)| (idx, dep.clone()))
3740 .collect();
3741 let removed: Vec<usize> = old_deps
3742 .iter()
3743 .enumerate()
3744 .filter(|(idx, _)| old_to_new[*idx].is_none())
3745 .map(|(idx, _)| idx)
3746 .collect();
3747
3748 self.with_inner_edges_mut(|_, e| {
3749 e.wave.in_dep_mutation = true;
3750 e.wave.rewire_run_pending = false;
3751 });
3752 let _mut_guard = DepMutationGuard(DeferredNodeAction::from_core(self));
3761 let activated = self.with_aux(|a| a.activated);
3762 let mut zero_dep_undirty = false;
3763
3764 self.swap_fn_preserving_pool(fn_);
3768
3769 let mut removed_dirty_contributor = false;
3771 for old_idx in &removed {
3772 let old_idx = *old_idx;
3773 let unsub = {
3774 let mut graph = self.graph.borrow_mut();
3775 let (_nn, edges) = graph.get_node_and_edges_mut(self.key());
3776 if edges.state.dirty[old_idx] {
3777 removed_dirty_contributor = true;
3778 edges.state.pending -= 1;
3779 }
3780 if let Some(b) = edges.idx_boxes.get(old_idx) {
3781 b.set(-1); }
3783 edges.unsubs.get_mut(old_idx).and_then(|u| u.take())
3784 };
3785 if let Some(u) = unsub {
3786 u(); }
3788 }
3789
3790 {
3793 let mut graph = self.graph.borrow_mut();
3794 let (nn, edges) = graph.get_node_and_edges_mut(self.key());
3795 let mut new_batch: Vec<Option<Vec<AnyValue>>> = vec![None; n];
3796 let mut new_batch_waves: Vec<Vec<Vec<WaveData>>> = vec![Vec::new(); n];
3797 let mut new_batch_wave_id: Vec<Option<u64>> = vec![None; n];
3798 let mut new_prev: Vec<Option<AnyValue>> = vec![None; n];
3799 let mut new_has = vec![false; n];
3800 let mut new_dirty = vec![false; n];
3801 let mut new_tier = vec![0u8; n];
3802 let mut new_terminal: Vec<Option<DepTerminal>> = vec![None; n];
3803 let mut new_terminal_wave: Vec<Option<DepTerminal>> = vec![None; n];
3804 let mut new_unsubs: Vec<Option<Unsub>> = (0..n).map(|_| None).collect();
3805 let mut new_boxes: Vec<Rc<Cell<i64>>> =
3806 (0..n).map(|_| Rc::new(Cell::new(-1i64))).collect();
3807 for (j, old_idx) in new_to_old.iter().enumerate() {
3808 if let Some(old_idx) = *old_idx {
3809 new_batch[j] = edges.state.batch[old_idx].take();
3810 new_batch_waves[j] = std::mem::take(&mut edges.state.batch_waves[old_idx]);
3811 new_batch_wave_id[j] = edges.state.batch_wave_id[old_idx].take();
3812 new_prev[j] = edges.state.prev[old_idx].clone();
3813 new_has[j] = edges.state.has_data[old_idx];
3814 new_dirty[j] = edges.state.dirty[old_idx];
3815 new_tier[j] = edges.state.tier[old_idx];
3816 new_terminal[j] = edges.state.terminal[old_idx].clone();
3817 new_terminal_wave[j] = edges.state.terminal_wave[old_idx].clone();
3818 if let Some(slot) = edges.unsubs.get_mut(old_idx) {
3819 new_unsubs[j] = slot.take();
3820 }
3821 if let Some(b) = edges.idx_boxes.get(old_idx) {
3822 b.set(j as i64); new_boxes[j] = b.clone();
3824 }
3825 }
3826 }
3827 nn.deps = new_deps.clone();
3828 edges.state.batch = new_batch;
3829 edges.state.batch_waves = new_batch_waves;
3830 edges.state.batch_wave_id = new_batch_wave_id;
3831 edges.state.prev = new_prev;
3832 edges.state.has_data = new_has;
3833 edges.state.dirty = new_dirty;
3834 edges.state.tier = new_tier;
3835 edges.state.terminal = new_terminal;
3836 edges.state.terminal_wave = new_terminal_wave;
3837 edges.unsubs = new_unsubs;
3838 edges.idx_boxes = new_boxes;
3839 edges.restored_activation_handshake = vec![false; n];
3840 }
3841
3842 if activated {
3846 for (idx, d) in &added {
3847 self.subscribe_dep(*idx, d);
3848 }
3849 }
3850 self.notify_topology_deps_changed(&old_deps, &new_deps);
3851
3852 if self.with_inner_edges(|_, e| e.value.terminal) {
3857 return;
3858 }
3859
3860 {
3864 let should_rewire_settle = self.with_inner_edges(|_n, e| {
3865 removed_dirty_contributor && e.state.pending == 0 && e.value.status == Status::Dirty
3866 });
3867 if should_rewire_settle {
3868 if new_deps.is_empty() {
3869 zero_dep_undirty = true;
3870 } else {
3871 self.with_inner_edges_mut(|_, e| {
3872 e.wave.rewire_run_pending = true;
3873 });
3874 }
3875 }
3876 }
3877
3878 self.with_inner_edges_mut(|_, e| {
3879 e.wave.in_dep_mutation = false;
3880 });
3881
3882 let run_pending =
3885 self.with_inner_edges_mut(|_, e| std::mem::take(&mut e.wave.rewire_run_pending));
3886 if run_pending {
3887 self.settle_rewire();
3888 } else if zero_dep_undirty {
3889 let emitted = self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave);
3890 if emitted {
3891 self.down(vec![Message::Resolved]); } else {
3893 self.with_inner_edges_mut(|_n, e| {
3894 e.value.status = if e.value.has_data {
3895 Status::Settled
3896 } else {
3897 Status::Sentinel
3898 };
3899 });
3900 }
3901 }
3902 }
3903
3904 fn same_graph_no_overlap(old_deps: &[Core], new_deps: &[Core]) -> bool {
3905 let mut old_ids = HashSet::with_capacity(old_deps.len());
3906 for dep in old_deps {
3907 old_ids.insert(dep.arena_node_key());
3908 }
3909 !new_deps
3910 .iter()
3911 .any(|dep| old_ids.contains(&dep.arena_node_key()))
3912 }
3913
3914 fn settle_rewire(&self) {
3919 if self.with_config(|cfg| matches!(cfg.pausable, Pausable::True)) && self.is_paused() {
3920 self.with_aux_mut(|a| a.paused_dep_wave_occurred = true);
3921 return;
3922 }
3923 let (pending, has_handle, has_called, partial, all_settled) =
3924 self.with_node_state(|_n, c, cfg, r, e| {
3925 (
3926 e.state.pending,
3927 c.handle.is_some(),
3928 r.has_called_fn_once,
3929 cfg.partial,
3930 cfg.all_deps_settled(&e.state),
3931 )
3932 });
3933 if pending > 0 {
3934 return;
3935 }
3936 if !has_handle {
3937 self.passthrough_emit();
3938 return;
3939 }
3940 if !(has_called || partial || all_settled) {
3941 return; }
3943 self.mark_dirty(); self.run_wave(); }
3946
3947 fn swap_fn_preserving_pool(&self, fn_: NodeFn) {
3948 let (old_handle, disp) = { self.with_call(|call| (call.handle, call.dispatcher.clone())) };
3949 let kind = old_handle
3950 .map(|h| disp.pool_kind(h.pool_id))
3951 .unwrap_or(PoolKind::Sync);
3952 let new_handle = register_with(&disp, kind, fn_);
3953 self.with_node_state_mut(|_n, call, _cfg, _r, _e| {
3954 call.handle = Some(new_handle);
3955 });
3956 if let Some(oh) = old_handle {
3957 disp.unregister(oh);
3958 }
3959 }
3960
3961 fn run_wave(&self) {
3962 wave_register(self);
3965 if self.with_inner_edges(|_, e| e.wave.inside_run_wave) {
3966 panic!(
3972 "synchronous feedback cycle: node fn re-entered its own wave (R-reentrancy / D37)"
3973 );
3974 }
3975 let (handle, dispatcher, is_async, ctx, old_on_invalidate, old_on_deactivation) = self
3978 .with_node_state_aux_mut(|n, c, _cfg, r, e, a| {
3979 let dep_records: Vec<DepRecord> = (0..n.deps.len())
3980 .map(|i| {
3981 let latest = e.state.batch[i]
3982 .as_ref()
3983 .and_then(|b| b.last().cloned())
3984 .or_else(|| e.state.prev[i].clone());
3985 let wave_data = std::mem::take(&mut e.state.batch_waves[i]);
3986 e.state.batch_wave_id[i] = None;
3987 DepRecord {
3988 wave_data,
3989 prev_data: e.state.prev[i].clone(),
3990 latest,
3991 terminal: e.state.terminal_wave[i].take(),
3992 }
3993 })
3994 .collect();
3995 r.has_called_fn_once = true;
3996 e.wave.inside_run_wave = true;
3997 e.wave.emitted_tier3_this_wave = false;
3998 (
3999 c.handle.expect("run_wave ⇒ handle present"),
4000 c.dispatcher.clone(),
4001 c.handle
4002 .map(|h| c.dispatcher.pool_kind(h.pool_id) == PoolKind::Async)
4003 .unwrap_or(false),
4004 Ctx::new(self.borrowed_view(), dep_records, a.active_pull.clone()),
4005 std::mem::take(&mut a.on_invalidate),
4006 std::mem::take(&mut a.on_deactivation),
4007 )
4008 });
4009 drop(old_on_invalidate);
4012 drop(old_on_deactivation);
4013 wave_set_blamed(self);
4016 let _guard = WaveGuard(DeferredNodeAction::from_core(self));
4017 dispatcher.invoke(handle, &ctx);
4019 drop(_guard); let undirty = self.with_inner_edges(|_n, e| {
4030 e.wave.emitted_dirty_this_wave
4036 && !e.wave.emitted_tier3_this_wave
4037 && !e.value.terminal
4038 && !is_async
4039 });
4040 if undirty {
4041 self.down(vec![Message::Resolved]);
4044 }
4045 let keep_dirty_for_deferred_undirty = undirty
4046 && self.with_inner_edges(|_n, e| {
4047 e.wave.batch_dirty_owed || e.wave.emitted_tier3_this_wave
4048 });
4049
4050 self.with_inner_edges_mut(|_n, e| {
4053 for (i, b) in e.state.batch.iter_mut().enumerate() {
4054 if let Some(last) = b.as_ref().and_then(|batch| batch.last()).cloned() {
4055 e.state.prev[i] = Some(last);
4056 }
4057 *b = None;
4058 }
4059 if !keep_dirty_for_deferred_undirty {
4060 e.wave.emitted_dirty_this_wave = false;
4061 e.wave.emitted_tier3_this_wave = false;
4062 }
4063 });
4064 }
4065
4066 pub(crate) fn down(&self, msgs: Vec<Msg>) {
4077 let (terminal, inside) =
4081 self.with_inner_edges(|_n, e| (e.value.terminal, e.wave.inside_run_wave));
4082 if terminal {
4083 return;
4084 }
4085 wave_register(self);
4087 let mut sorted = msgs;
4088 sorted.sort_by_key(|m| m.tier().as_u8()); if sorted
4093 .iter()
4094 .filter(|m| matches!(m, Message::Invalidate))
4095 .count()
4096 > 1
4097 {
4098 let mut seen = false;
4099 sorted.retain(|m| {
4100 if matches!(m, Message::Invalidate) {
4101 let keep = !seen;
4102 seen = true;
4103 keep
4104 } else {
4105 true
4106 }
4107 });
4108 }
4109
4110 let versioning_policy = self.versioning_policy();
4111 for m in &sorted {
4112 if let Message::Data(value) = m {
4113 assert_node_version_data_compatible(&versioning_policy, value)
4114 .unwrap_or_else(|err| panic!("{err}"));
4115 }
4116 }
4117
4118 if !inside && collecting_batch() {
4119 let (deferred, rest): (Vec<Msg>, Vec<Msg>) = sorted
4120 .into_iter()
4121 .partition(|m| m.tier().is_batch_deferred());
4122 if !deferred.is_empty() && defer_to_batch(self, deferred) {
4123 if !self.with_inner_edges(|_, e| e.wave.emitted_dirty_this_wave) {
4124 self.emit_dirty_once();
4125 }
4126 self.with_inner_edges_mut(|_, e| {
4127 e.wave.batch_dirty_owed = true;
4128 });
4129 return;
4130 }
4131 sorted = rest;
4132 if sorted.is_empty() {
4133 return;
4134 }
4135 }
4136
4137 if self.should_buffer_on_pause() {
4144 let (buffered, rest): (Vec<Msg>, Vec<Msg>) = sorted
4145 .into_iter()
4146 .partition(|m| m.tier().is_pause_buffered());
4147 if !buffered.is_empty() {
4148 self.with_inner_edges_aux_mut(|_n, e, a| {
4149 e.wave.emitted_tier3_this_wave = true;
4156 a.pause_buffer.push(buffered);
4157 });
4158 }
4159 sorted = rest;
4160 if sorted.is_empty() {
4161 return;
4162 }
4163 }
4164
4165 let mut data_count = 0usize;
4166 let mut has_resolved = false;
4167 let mut has_tier3 = false;
4168 for m in &sorted {
4169 match m {
4170 Message::Data(_) => {
4171 data_count += 1;
4172 has_tier3 = true;
4173 }
4174 Message::Resolved => {
4175 has_resolved = true;
4176 has_tier3 = true;
4177 }
4178 _ => {}
4179 }
4180 }
4181 assert!(
4184 !(data_count >= 1 && has_resolved),
4185 "down: a wave cannot mix DATA and RESOLVED (tier-3 exclusivity, R-resolved-undirty / D49)"
4186 );
4187
4188 let pull_dirty_owed = self.with_aux(|a| a.pull_dirty_owed);
4191 if has_tier3 && (!inside || pull_dirty_owed) {
4192 self.emit_dirty_once();
4193 }
4194
4195 with_delivery_scope(|| {
4196 for m in sorted {
4197 if let Some(action) = self.collect_down_action(m) {
4198 self.apply_down_action(action);
4199 }
4200 }
4201 });
4202
4203 if !inside {
4204 self.with_inner_edges_mut(|_, e| {
4205 e.wave.emitted_dirty_this_wave = false;
4206 e.wave.emitted_tier3_this_wave = false;
4207 });
4208 }
4209 }
4210
4211 fn collect_down_action(&self, msg: Msg) -> Option<DownAction> {
4212 let next_version = if let Message::Data(value) = &msg {
4213 let (policy, current) = {
4214 let graph = self.graph.borrow();
4215 let version = graph.get_version(self.key());
4216 (version.policy.clone(), version.value.clone())
4217 };
4218 Some(
4219 advance_node_version(current.as_ref(), &policy, value)
4220 .unwrap_or_else(|err| panic!("{err}")),
4221 )
4222 } else {
4223 None
4224 };
4225 let mut g = self.graph.borrow_mut();
4226 Self::collect_down_action_from_graph(&mut g, self.key(), msg, next_version)
4227 }
4228
4229 fn apply_down_action(&self, action: DownAction) {
4230 match action {
4231 DownAction::Emit { msg, subs } => deliver_subscriber_snapshot(subs, &msg),
4232 DownAction::Invalidate { hooks } => {
4233 for f in hooks {
4236 f();
4237 }
4238 self.emit_to_subs(&Message::Invalidate);
4239 }
4240 }
4241 }
4242
4243 fn collect_down_action_from_graph(
4244 g: &mut GraphCore,
4245 key: NodeKey,
4246 msg: Msg,
4247 next_version: Option<Option<NodeVersion>>,
4248 ) -> Option<DownAction> {
4249 match msg {
4250 Message::Dirty => {
4251 let (_n, e, a) = g.get_node_edges_aux_mut(key);
4252 if e.wave.emitted_dirty_this_wave {
4253 return None;
4254 }
4255 e.wave.emitted_dirty_this_wave = true;
4256 e.value.status = Status::Dirty;
4257 Some(DownAction::Emit {
4258 msg: Message::Dirty,
4259 subs: snapshot_subscribers(a),
4260 })
4261 }
4262 Message::Data(v) => {
4263 {
4265 let version = g.get_version_mut(key);
4266 version.value =
4267 next_version.expect("DATA precomputed next node runtime version");
4268 }
4269 let (_n, e, a) = g.get_node_edges_aux_mut(key);
4270 e.value.cache = Some(v.clone());
4271 e.value.has_data = true;
4272 e.value.status = Status::Settled;
4273 e.wave.emitted_tier3_this_wave = true;
4274 Some(DownAction::Emit {
4275 msg: Message::Data(v),
4276 subs: snapshot_subscribers(a),
4277 })
4278 }
4279 Message::Resolved => {
4280 let (_n, e, a) = g.get_node_edges_aux_mut(key);
4285 e.value.status = if e.value.has_data {
4286 Status::Resolved
4287 } else {
4288 Status::Sentinel
4289 };
4290 e.wave.emitted_tier3_this_wave = true;
4291 Some(DownAction::Emit {
4292 msg: Message::Resolved,
4293 subs: snapshot_subscribers(a),
4294 })
4295 }
4296 Message::Invalidate => {
4297 let (_n, e, a) = g.get_node_edges_aux_mut(key);
4300 if !e.value.has_data {
4301 return None;
4302 }
4303 e.value.cache = None;
4304 e.value.has_data = false;
4305 e.value.status = Status::Sentinel;
4306 Some(DownAction::Invalidate {
4307 hooks: a.on_invalidate.clone(),
4308 })
4309 }
4310 Message::Complete => {
4311 let (_n, e, a) = g.get_node_edges_aux_mut(key);
4312 if e.value.terminal {
4313 return None;
4314 }
4315 e.value.terminal = true;
4316 e.value.status = Status::Completed;
4317 a.pull_demand_owed = None;
4318 a.active_pull = None;
4319 a.in_deliver_demand = false;
4320 a.pull_dirty_owed = false;
4321 a.paused_dep_wave_occurred = false;
4322 a.pause_buffer.clear();
4323 Some(DownAction::Emit {
4324 msg: Message::Complete,
4325 subs: snapshot_subscribers(a),
4326 })
4327 }
4328 Message::Error(e) => {
4329 let (_n, edges, a) = g.get_node_edges_aux_mut(key);
4330 if edges.value.terminal {
4331 return None;
4332 }
4333 edges.value.terminal = true;
4334 edges.value.status = Status::Errored;
4335 a.pull_demand_owed = None;
4336 a.active_pull = None;
4337 a.in_deliver_demand = false;
4338 a.pull_dirty_owed = false;
4339 a.paused_dep_wave_occurred = false;
4340 a.pause_buffer.clear();
4341 Some(DownAction::Emit {
4342 msg: Message::Error(e),
4343 subs: snapshot_subscribers(a),
4344 })
4345 }
4346 Message::Teardown => Some(DownAction::Emit {
4347 msg: Message::Teardown,
4348 subs: snapshot_subscribers(g.get_aux(key)),
4349 }),
4350 _ => None,
4352 }
4353 }
4354
4355 pub(crate) fn owned_down(&self, msgs: Wave<AnyValue>) {
4362 with_wave_owner(self, || self.down(msgs), || {});
4363 }
4364
4365 pub(crate) fn owned_up(&self, msgs: Wave<AnyValue>, toward_dep: Option<usize>) {
4366 with_wave_owner(self, || self.up(msgs, toward_dep), || {});
4367 }
4368
4369 pub(crate) fn commit_batched_wave(&self, wave: Wave<AnyValue>) {
4370 self.with_inner_edges_mut(|_, e| {
4371 e.wave.batch_dirty_owed = false;
4372 });
4373 self.owned_down(wave);
4374 }
4375
4376 pub(crate) fn rollback_batched(&self) {
4377 let should_balance = self.with_inner_edges_mut(|_, e| {
4378 if e.wave.batch_dirty_owed {
4379 e.wave.batch_dirty_owed = false;
4380 true
4381 } else {
4382 false
4383 }
4384 });
4385 if should_balance {
4386 self.owned_down(vec![Message::Resolved]);
4387 }
4388 }
4389
4390 fn emit_dirty_once(&self) {
4391 let subs = self.with_inner_edges_aux_mut(|_n, e, a| {
4392 if !e.wave.emitted_dirty_this_wave {
4393 e.wave.emitted_dirty_this_wave = true;
4394 e.value.status = Status::Dirty;
4395 snapshot_subscribers(a)
4396 } else {
4397 SubscriberSnapshot::Empty
4398 }
4399 });
4400 deliver_subscriber_snapshot(subs, &Message::Dirty);
4401 }
4402
4403 fn emit_to_subs(&self, msg: &Msg) {
4404 let subs = self.with_aux(snapshot_subscribers);
4408 deliver_subscriber_snapshot(subs, msg);
4409 }
4410
4411 pub(crate) fn up(&self, msgs: Vec<Msg>, toward_dep: Option<usize>) {
4424 let mut route = UpRouteState::new();
4425 self.up_route(msgs, toward_dep, &mut route);
4426 }
4427
4428 fn up_route(&self, msgs: Vec<Msg>, toward_dep: Option<usize>, route: &mut UpRouteState) {
4429 for m in &msgs {
4430 assert!(
4431 m.is_up_allowed(),
4432 "ctx.up: {m:?} is down-only; up carries control tiers only (R-ctx-up)"
4433 );
4434 }
4435 for m in msgs {
4436 self.up_msg(m, toward_dep, route);
4437 }
4438 }
4439
4440 fn up_msg(&self, m: Msg, toward_dep: Option<usize>, route: &mut UpRouteState) {
4441 let is_depless = self.borrow().deps.is_empty();
4442 match m {
4443 Message::Pause(lock) => self.pause_acquire(lock),
4444 Message::Resume(lock) => {
4445 if self.with_aux(|a| a.pause_lockset.contains(&lock)) {
4446 self.pause_release(lock);
4447 } else {
4448 self.forward_up(Message::Resume(lock), toward_dep, route);
4449 }
4450 }
4451 Message::Pull(demand) => {
4452 let is_pull_holder =
4453 self.with_config(|cfg| cfg.pull_id.as_ref() == Some(&demand.pull_id));
4454 if is_pull_holder {
4455 if !route.mark_demand(&demand.pull_id, self) {
4456 self.on_demand(demand);
4457 }
4458 } else {
4459 self.forward_up(Message::Pull(demand), toward_dep, route);
4460 }
4461 }
4462 other if is_depless => {
4464 if matches!(other, Message::Invalidate) {
4466 self.down(vec![Message::Invalidate]);
4467 }
4468 }
4469 other => {
4470 self.forward_up(other, toward_dep, route);
4474 }
4475 }
4476 }
4477
4478 fn forward_up(&self, m: Msg, toward_dep: Option<usize>, route: &mut UpRouteState) {
4479 let deps = self.with_inner_edges(|n, _| {
4484 n.deps
4485 .iter()
4486 .map(|dep| (dep.id, dep.generation, dep.refs.clone()))
4487 .collect::<Vec<_>>()
4488 });
4489 let mut to_target = |target: &(NodeId, u64, Rc<Cell<usize>>), msg: Msg| {
4490 let (id, generation, refs) = target;
4491 let key = NodeKey {
4492 id: *id,
4493 generation: *generation,
4494 };
4495 let arena_key = ArenaNodeKey {
4496 graph: Rc::as_ptr(&self.graph) as usize,
4497 node: key,
4498 };
4499 {
4500 let mut graph = self.graph.borrow_mut();
4501 if !graph.is_live_key(key) || (refs.get() == 0 && graph.pin_count(key) == 0) {
4502 return;
4503 }
4504 if !graph.pin_live_key(key) {
4505 return;
4506 }
4507 }
4508 let pin = ArenaNodePin {
4509 arena_key,
4510 graph: self.graph.clone(),
4511 refs: refs.clone(),
4512 };
4513 let Some(dep_core) = pin.borrowed_core() else {
4514 return;
4515 };
4516 dep_core.up_msg(msg, None, route);
4517 };
4518 if let Some(i) = toward_dep {
4519 if let Some(dep) = deps.get(i) {
4520 to_target(dep, m);
4521 }
4522 return;
4523 }
4524 for dep in deps {
4525 to_target(&dep, dup_control(&m));
4526 }
4527 }
4528
4529 fn is_paused(&self) -> bool {
4532 !self.with_aux(|a| a.pause_lockset.is_empty())
4533 }
4534
4535 fn pause_acquire(&self, lock: LockId) {
4536 self.with_aux_mut(|a| {
4538 a.pause_lockset.insert(lock);
4539 });
4540 }
4541
4542 fn pause_release(&self, lock: LockId) {
4543 let Some(resumed) = self.with_aux_mut(|a| {
4544 if !a.pause_lockset.remove(&lock) {
4545 return None; }
4547 Some(a.pause_lockset.is_empty()) }) else {
4549 return;
4550 };
4551 if resumed && self.with_config(|cfg| cfg.pull_id.is_none()) {
4552 self.on_resume();
4553 if boundary_drains_blocked() {
4554 register_boundary_root(self);
4555 }
4556 } else if resumed && boundary_drains_blocked() {
4557 register_boundary_root(self);
4558 }
4559 self.fire_owed_demand_if_ready();
4560 }
4561
4562 fn can_fire_demand(&self) -> bool {
4563 self.with_node_state_aux_mut(|_n, c, cfg, r, e, a| {
4564 if cfg.pull_id.is_none()
4565 || e.value.terminal
4566 || e.state.pending != 0
4567 || !a.pause_lockset.is_empty()
4568 {
4569 return false;
4570 }
4571 if c.handle.is_some()
4572 && !r.has_called_fn_once
4573 && !cfg.partial
4574 && !cfg.all_deps_settled(&e.state)
4575 {
4576 return false;
4577 }
4578 true
4579 })
4580 }
4581
4582 fn on_demand(&self, demand: PullDemand) {
4583 let should_deliver = self.with_node_state_aux_mut(|_n, _c, cfg, _r, _e, a| {
4584 if cfg.pull_id.is_none() || a.in_deliver_demand {
4585 return false;
4586 }
4587 a.pull_demand_owed = Some(demand);
4588 true
4589 });
4590 if should_deliver {
4591 self.fire_owed_demand_if_ready();
4592 }
4593 }
4594
4595 fn fire_owed_demand_if_ready(&self) {
4596 if self.with_aux(|a| a.in_deliver_demand) || !self.can_fire_demand() {
4597 return;
4598 }
4599 let Some(demand) = self.with_aux_mut(|a| a.pull_demand_owed.take()) else {
4600 return;
4601 };
4602 self.deliver_pull_demand(demand);
4603 }
4604
4605 fn deliver_pull_demand(&self, demand: PullDemand) {
4606 self.with_aux_mut(|a| {
4607 a.active_pull = Some(demand);
4608 a.in_deliver_demand = true;
4609 });
4610 struct PullDelivery<'a> {
4611 core: &'a Core,
4612 }
4613 impl Drop for PullDelivery<'_> {
4614 fn drop(&mut self) {
4615 let _ = self
4616 .core
4617 .try_with_node_state_aux_mut(|_n, _c, _cfg, _r, _e, a| {
4618 a.active_pull = None;
4619 a.in_deliver_demand = false;
4620 a.pull_dirty_owed = false;
4621 });
4622 }
4623 }
4624 let _guard = PullDelivery { core: self };
4625 self.fire_pull_demand();
4626 }
4627
4628 fn fire_pull_demand(&self) {
4629 let (mut buf, pausable) = self.with_node_state_aux_mut(|_n, _c, cfg, _r, _e, a| {
4630 (std::mem::take(&mut a.pause_buffer), cfg.pausable)
4631 });
4632 if !buf.is_empty() {
4633 if matches!(pausable, Pausable::True) {
4634 if let Some(wave) = buf.pop() {
4635 self.down(wave);
4636 }
4637 } else {
4638 for wave in buf {
4639 self.down(wave);
4640 }
4641 }
4642 return;
4643 }
4644
4645 let should_run_paused_dep = self.with_aux(|a| a.paused_dep_wave_occurred);
4646 if should_run_paused_dep {
4647 let gated = self.with_node_state(|_n, c, cfg, r, e| {
4648 e.state.pending != 0
4649 || (c.handle.is_some()
4650 && !r.has_called_fn_once
4651 && !cfg.partial
4652 && !cfg.all_deps_settled(&e.state))
4653 });
4654 if gated {
4655 return;
4656 }
4657 self.with_node_state_aux_mut(|_n, _c, _cfg, _r, e, a| {
4658 a.paused_dep_wave_occurred = false;
4659 a.pull_dirty_owed = true;
4660 e.wave.emitted_dirty_this_wave = false;
4661 });
4662 self.try_run();
4663 return;
4664 }
4665
4666 let has_handle = self.with_call(|c| c.handle.is_some());
4667 if has_handle {
4668 self.with_inner_edges_aux_mut(|_n, e, a| {
4669 a.pull_dirty_owed = true;
4670 e.wave.emitted_dirty_this_wave = false;
4671 });
4672 self.try_run();
4673 }
4674 }
4675
4676 fn on_resume(&self) {
4677 if self.with_inner_edges(|_n, e| e.value.terminal) {
4680 self.with_aux_mut(|a| {
4681 a.pull_demand_owed = None;
4682 a.active_pull = None;
4683 a.in_deliver_demand = false;
4684 a.pull_dirty_owed = false;
4685 a.paused_dep_wave_occurred = false;
4686 a.pause_buffer.clear();
4687 });
4688 return;
4689 }
4690 let buf = { self.with_aux_mut(|a| std::mem::take(&mut a.pause_buffer)) };
4695 for wave in buf {
4696 self.down(wave);
4697 }
4698 let fire = { self.with_aux_mut(|a| std::mem::take(&mut a.paused_dep_wave_occurred)) };
4701 if fire {
4702 self.try_run();
4703 }
4704 }
4705
4706 fn call_is_async(c: &NodeCallSlot) -> bool {
4709 c.handle
4710 .map(|h| c.dispatcher.pool_kind(h.pool_id) == PoolKind::Async)
4711 .unwrap_or(false)
4712 }
4713
4714 fn should_buffer_on_pause(&self) -> bool {
4717 self.with_node_state(|n, c, cfg, _r, e| match cfg.pausable {
4718 Pausable::False => false,
4720 _ if self.with_aux(|a| {
4721 a.pause_lockset.is_empty() && !(cfg.pull_id.is_some() && a.active_pull.is_none())
4722 }) =>
4723 {
4724 false
4725 }
4726 Pausable::ResumeAll => true,
4728 Pausable::True => {
4733 let pull_quiet =
4734 cfg.pull_id.is_some() && self.with_aux(|a| a.active_pull.is_none());
4735 (pull_quiet && !e.wave.inside_run_wave && n.deps.is_empty())
4736 || (!e.wave.inside_run_wave && Self::call_is_async(c) && !n.deps.is_empty())
4737 }
4738 })
4739 }
4740
4741 fn invalidate(&self) {
4745 let hooks = {
4746 self.with_inner_edges_aux_mut(|_n, e, a| {
4747 if !e.value.has_data {
4748 return None; }
4750 e.value.cache = None;
4751 e.value.has_data = false;
4752 e.value.status = Status::Sentinel;
4753 Some(a.on_invalidate.clone()) })
4755 };
4756 let Some(hooks) = hooks else {
4757 return;
4758 };
4759 for f in hooks {
4760 f();
4761 }
4762 self.emit_to_subs(&Message::Invalidate);
4763 }
4764
4765 pub(crate) fn get_state(&self) -> Option<AnyValue> {
4768 self.with_aux(|a| a.state.clone())
4769 }
4770 pub(crate) fn set_state(&self, v: AnyValue) {
4771 self.with_aux_mut(|a| a.state = Some(v));
4772 }
4773 pub(crate) fn set_state_persist(&self, on: bool) {
4774 self.with_aux_mut(|a| a.state_persist = on);
4775 }
4776 pub(crate) fn register_on_deactivation(&self, f: Box<dyn FnOnce()>) {
4777 self.with_aux_mut(|a| a.on_deactivation.push(f));
4778 }
4779 pub(crate) fn register_on_invalidate(&self, f: Rc<dyn Fn()>) {
4780 self.with_aux_mut(|a| a.on_invalidate.push(f));
4781 }
4782
4783 pub(crate) fn subscriber_count(&self) -> usize {
4784 self.with_aux(|a| a.subscribers.len())
4785 }
4786
4787 pub(crate) fn is_active(&self) -> bool {
4788 self.with_aux(|a| a.activated)
4789 }
4790}
4791
4792fn reset_wave_flags_inner(n: &mut NodeTopologySlot, e: &mut DepEdges) {
4793 e.wave.inside_run_wave = false;
4794 e.wave.emitted_dirty_this_wave = false;
4795 e.wave.emitted_tier3_this_wave = false;
4796 e.state.pending = 0;
4797 for i in 0..n.deps.len() {
4798 e.state.batch[i] = None;
4799 e.state.batch_waves[i].clear();
4800 e.state.batch_wave_id[i] = None;
4801 e.state.terminal_wave[i] = None;
4802 e.state.dirty[i] = false;
4803 }
4804 }
4809
4810fn dup_control(m: &Msg) -> Msg {
4816 match m {
4817 Message::Pause(lock) => Message::Pause(lock.clone()),
4818 Message::Resume(lock) => Message::Resume(lock.clone()),
4819 Message::Pull(demand) => Message::Pull(demand.clone()),
4820 Message::Dirty => Message::Dirty,
4821 Message::Invalidate => Message::Invalidate,
4822 Message::Teardown => Message::Teardown,
4823 other => unreachable!("up forwards only control messages, got {other:?}"),
4824 }
4825}
4826
4827fn reachable_upstream(from: &Core, target: &Core) -> bool {
4830 if !from.same_graph(target) {
4831 return false;
4832 }
4833 let target_key = target.key();
4834 let mut seen: HashSet<NodeKey> = HashSet::new();
4835 let mut stack: Vec<NodeKey> = vec![from.key()];
4836 let graph = from.graph.borrow();
4837 if !graph.is_live_key(target_key) {
4838 return false;
4839 }
4840 while let Some(key) = stack.pop() {
4841 if !seen.insert(key) {
4842 continue;
4843 }
4844 if !graph.is_live_key(key) {
4845 continue;
4846 }
4847 if key == target_key {
4848 return true;
4849 }
4850 let topology = graph.get(key);
4851 for dep in &topology.deps {
4852 stack.push(dep.key());
4853 }
4854 }
4855 false
4856}
4857
4858fn dedup_cores(deps: Vec<Core>) -> Vec<Core> {
4861 let mut out: Vec<Core> = Vec::with_capacity(deps.len());
4862 let mut seen: HashSet<ArenaNodeKey> = HashSet::with_capacity(deps.len());
4863 for d in deps {
4864 if seen.insert(d.arena_node_key()) {
4865 out.push(d);
4866 }
4867 }
4868 out
4869}
4870
4871fn same_ordered_deps(old_deps: &[Core], new_deps: &[Core]) -> bool {
4872 old_deps.len() == new_deps.len()
4873 && old_deps
4874 .iter()
4875 .zip(new_deps)
4876 .all(|(old_dep, new_dep)| old_dep.ptr_eq(new_dep))
4877}
4878
4879fn register_with(disp: &Dispatcher, pool: PoolKind, f: NodeFn) -> Handle {
4883 match pool {
4884 PoolKind::Sync => disp.register(f),
4885 PoolKind::Async => disp.register_async(f),
4886 }
4887}
4888
4889pub struct Node<T> {
4895 core: Core,
4896 _t: PhantomData<T>,
4897}
4898
4899impl<T: 'static> Node<T> {
4900 pub fn state(initial: T) -> Node<T> {
4903 Self::state_in_arena(&GraphArena::default(), initial)
4904 }
4905
4906 pub(crate) fn state_in_arena(arena: &GraphArena, initial: T) -> Node<T> {
4907 Self::state_in_arena_with_dispatcher(arena, default_dispatcher(), initial)
4908 }
4909
4910 pub(crate) fn state_in_arena_with_dispatcher(
4911 arena: &GraphArena,
4912 dispatcher: Dispatcher,
4913 initial: T,
4914 ) -> Node<T> {
4915 Self::state_opts_in_arena_with_dispatcher(arena, dispatcher, initial, NodeOpts::default())
4916 }
4917
4918 pub(crate) fn state_opts_in_arena_with_dispatcher(
4919 arena: &GraphArena,
4920 dispatcher: Dispatcher,
4921 initial: T,
4922 opts: NodeOpts,
4923 ) -> Node<T> {
4924 Node {
4925 core: Core::new_in_arena(
4926 arena,
4927 vec![],
4928 None,
4929 dispatcher,
4930 Some(Rc::new(initial)),
4931 opts,
4932 ),
4933 _t: PhantomData,
4934 }
4935 }
4936
4937 pub fn state_empty() -> Node<T> {
4939 Self::state_empty_in_arena(&GraphArena::default())
4940 }
4941
4942 pub(crate) fn state_empty_in_arena(arena: &GraphArena) -> Node<T> {
4943 Self::state_empty_in_arena_with_dispatcher(arena, default_dispatcher())
4944 }
4945
4946 pub(crate) fn state_empty_in_arena_with_dispatcher(
4947 arena: &GraphArena,
4948 dispatcher: Dispatcher,
4949 ) -> Node<T> {
4950 Self::state_empty_opts_in_arena_with_dispatcher(arena, dispatcher, NodeOpts::default())
4951 }
4952
4953 pub(crate) fn state_empty_opts_in_arena_with_dispatcher(
4954 arena: &GraphArena,
4955 dispatcher: Dispatcher,
4956 opts: NodeOpts,
4957 ) -> Node<T> {
4958 Node {
4959 core: Core::new_in_arena(arena, vec![], None, dispatcher, None, opts),
4960 _t: PhantomData,
4961 }
4962 }
4963
4964 pub fn producer<F: Fn(&Ctx) + 'static>(f: F) -> Node<T> {
4966 Self::producer_opts(NodeOpts::default(), f)
4967 }
4968
4969 pub fn producer_async<F: Fn(&Ctx) + 'static>(f: F) -> Node<T> {
4977 Self::producer_opts(
4978 NodeOpts {
4979 factory: None,
4980 pool: PoolKind::Async,
4981 pausable: Pausable::True,
4982 ..NodeOpts::default()
4983 },
4984 f,
4985 )
4986 }
4987
4988 pub fn producer_opts<F: Fn(&Ctx) + 'static>(opts: NodeOpts, f: F) -> Node<T> {
4990 Self::producer_opts_in_arena(&GraphArena::default(), opts, f)
4991 }
4992
4993 pub(crate) fn producer_opts_in_arena<F: Fn(&Ctx) + 'static>(
4994 arena: &GraphArena,
4995 opts: NodeOpts,
4996 f: F,
4997 ) -> Node<T> {
4998 Self::producer_opts_in_arena_with_dispatcher(arena, default_dispatcher(), opts, f)
4999 }
5000
5001 pub(crate) fn producer_opts_in_arena_with_dispatcher<F: Fn(&Ctx) + 'static>(
5002 arena: &GraphArena,
5003 dispatcher: Dispatcher,
5004 opts: NodeOpts,
5005 f: F,
5006 ) -> Node<T> {
5007 assert!(
5008 !(opts.pull_id.is_some() && matches!(opts.pausable, Pausable::False)),
5009 "pullId + pausable:false is invalid (R-pull / R-pause-modes)"
5010 );
5011 let factory = opts.factory.clone();
5012 let handle = register_with(&dispatcher, opts.pool, Rc::new(f));
5013 let node = Node {
5014 core: Core::new_in_arena(arena, vec![], Some(handle), dispatcher, None, opts.clone()),
5015 _t: PhantomData,
5016 };
5017 node.core.with_node_state_mut(|_n, call, _cfg, _r, _e| {
5018 call.factory = factory;
5019 });
5020 node.core.configure_pull(opts.pull_id);
5021 node
5022 }
5023
5024 pub fn derived<F: Fn(&Ctx) + 'static>(deps: Vec<Core>, f: F) -> Node<T> {
5030 Self::derived_opts(deps, NodeOpts::default(), f)
5031 }
5032
5033 pub fn derived_async<F: Fn(&Ctx) + 'static>(deps: Vec<Core>, f: F) -> Node<T> {
5039 Self::derived_opts(
5040 deps,
5041 NodeOpts {
5042 factory: None,
5043 pool: PoolKind::Async,
5044 pausable: Pausable::True,
5045 ..NodeOpts::default()
5046 },
5047 f,
5048 )
5049 }
5050
5051 pub fn derived_opts<F: Fn(&Ctx) + 'static>(deps: Vec<Core>, opts: NodeOpts, f: F) -> Node<T> {
5053 Self::derived_opts_in_arena(&GraphArena::default(), deps, opts, f)
5054 }
5055
5056 pub(crate) fn derived_opts_in_arena<F: Fn(&Ctx) + 'static>(
5057 arena: &GraphArena,
5058 deps: Vec<Core>,
5059 opts: NodeOpts,
5060 f: F,
5061 ) -> Node<T> {
5062 Self::derived_opts_in_arena_with_dispatcher(arena, default_dispatcher(), deps, opts, f)
5063 }
5064
5065 pub(crate) fn derived_opts_in_arena_with_dispatcher<F: Fn(&Ctx) + 'static>(
5066 arena: &GraphArena,
5067 dispatcher: Dispatcher,
5068 deps: Vec<Core>,
5069 opts: NodeOpts,
5070 f: F,
5071 ) -> Node<T> {
5072 Self::derived_opts_initial_in_arena_with_dispatcher(arena, dispatcher, deps, opts, None, f)
5073 }
5074
5075 pub(crate) fn derived_opts_initial_in_arena_with_dispatcher<F: Fn(&Ctx) + 'static>(
5076 arena: &GraphArena,
5077 dispatcher: Dispatcher,
5078 deps: Vec<Core>,
5079 opts: NodeOpts,
5080 initial: Option<T>,
5081 f: F,
5082 ) -> Node<T> {
5083 assert!(
5084 !(opts.pull_id.is_some() && matches!(opts.pausable, Pausable::False)),
5085 "pullId + pausable:false is invalid (R-pull / R-pause-modes)"
5086 );
5087 let factory = opts.factory.clone();
5088 let handle = register_with(&dispatcher, opts.pool, Rc::new(f));
5089 let initial = initial.map(|value| Rc::new(value) as AnyValue);
5090 let node = Node {
5091 core: Core::new_in_arena(arena, deps, Some(handle), dispatcher, initial, opts.clone()),
5092 _t: PhantomData,
5093 };
5094 {
5095 node.core.with_node_state_mut(|_n, call, cfg, _r, _e| {
5098 cfg.partial = opts.partial;
5099 cfg.complete_when_deps_complete = opts.complete_when_deps_complete;
5100 cfg.error_when_deps_error = opts.error_when_deps_error;
5101 cfg.terminal_as_real_input = opts.terminal_as_real_input;
5102 cfg.versioning = opts.versioning;
5103 call.factory = factory;
5104 });
5105 }
5106 node.core.configure_pull(opts.pull_id);
5107 node
5108 }
5109
5110 pub fn set(&self, v: T) {
5113 self.down(vec![Message::Data(Rc::new(v))]);
5114 }
5115
5116 pub fn down(&self, msgs: Wave<AnyValue>) {
5119 with_wave_owner(&self.core, || self.core.down(msgs), || {});
5120 }
5121
5122 pub fn up(&self, msgs: Wave<AnyValue>) {
5125 with_wave_owner(&self.core, || self.core.up(msgs, None), || {});
5126 }
5127
5128 pub fn up_toward(&self, toward_dep: usize, msgs: Wave<AnyValue>) {
5130 with_wave_owner(&self.core, || self.core.up(msgs, Some(toward_dep)), || {});
5131 }
5132
5133 pub fn cache(&self) -> Option<T>
5135 where
5136 T: Clone,
5137 {
5138 self.core
5139 .with_inner_edges(|_n, e| e.value.cache.clone())
5140 .as_ref()
5141 .and_then(|a| a.downcast_ref::<T>().cloned())
5142 }
5143
5144 pub fn status(&self) -> Status {
5146 self.core.with_inner_edges(|_n, e| e.value.status)
5147 }
5148
5149 pub fn version(&self) -> Option<NodeVersion> {
5151 self.core.version()
5152 }
5153
5154 pub fn pull_id(&self) -> Option<LockId> {
5156 self.core.with_config(|cfg| cfg.pull_id.clone())
5157 }
5158
5159 pub fn subscribe(&self, sink: impl Fn(&Msg) + 'static) -> Unsub {
5172 self.subscribe_with_kind(sink, SubscriberKind::External)
5173 }
5174
5175 pub(crate) fn subscribe_graph_observer(&self, sink: impl Fn(&Msg) + 'static) -> Unsub {
5176 self.subscribe_with_kind(sink, SubscriberKind::GraphObserver)
5177 }
5178
5179 fn subscribe_with_kind(&self, sink: impl Fn(&Msg) + 'static, kind: SubscriberKind) -> Unsub {
5180 assert!(
5181 !self.core.with_inner_edges(|_n, e| e.value.terminal),
5182 "subscribe: node is terminal and non-resubscribable — the stream is permanently over (R-terminal / D17)"
5183 );
5184 let sink = Rc::new(sink);
5185 let id_cell: Rc<Cell<Option<u64>>> = Rc::new(Cell::new(None));
5188 let body_cell = id_cell.clone();
5189 let abort_cell = id_cell.clone();
5190 let result = catch_unwind(AssertUnwindSafe(|| {
5191 with_wave_owner(
5192 &self.core,
5193 || {
5194 self.core
5195 .subscribe_recording_id_with_kind(sink, kind, &body_cell)
5196 },
5197 move || match id_cell.get() {
5198 Some(id) => {
5199 let err_core = self.core.clone();
5200 Box::new(move || err_core.unsubscribe(id)) as Unsub
5201 }
5202 None => Box::new(|| {}) as Unsub,
5203 },
5204 )
5205 }));
5206 match result {
5207 Ok(unsub) => unsub,
5208 Err(payload) => {
5209 if is_host_boundary_abort_payload(payload.as_ref()) {
5210 if let Some(id) = abort_cell.get() {
5211 self.core.unsubscribe(id);
5212 }
5213 }
5214 resume_unwind(payload);
5215 }
5216 }
5217 }
5218
5219 pub fn replace_deps<F: Fn(&Ctx) + 'static>(&self, new_deps: Vec<Core>, f: F) {
5230 self.core.rewire(new_deps, Rc::new(f));
5231 }
5232
5233 pub fn subscribe_dep<F: Fn(&Ctx) + 'static>(&self, dep: Core, f: F) -> usize {
5235 let mut next = self.core.project_pending_external_rewire_deps();
5236 if !next.iter().any(|d| d.ptr_eq(&dep)) {
5237 next.push(dep.clone());
5238 }
5239 let index = next
5240 .iter()
5241 .position(|d| d.ptr_eq(&dep))
5242 .expect("subscribe_dep next shape contains dep");
5243 self.core
5244 .external_rewire(RewireRequest::Add(dep, Rc::new(f)));
5245 index
5246 }
5247
5248 pub fn unsubscribe_dep<F: Fn(&Ctx) + 'static>(&self, dep: Core, f: F) {
5251 self.core
5252 .external_rewire(RewireRequest::remove(&dep, Rc::new(f)));
5253 }
5254
5255 pub fn erased(&self) -> Core {
5257 self.core.clone()
5258 }
5259
5260 pub(crate) fn from_core(core: Core) -> Node<T> {
5261 Node {
5262 core,
5263 _t: PhantomData,
5264 }
5265 }
5266}
5267
5268impl<T> Clone for Node<T> {
5272 fn clone(&self) -> Self {
5273 Node {
5274 core: self.core.clone(),
5275 _t: PhantomData,
5276 }
5277 }
5278}
5279
5280#[cfg(test)]
5281mod tests {
5282 use super::*;
5283 use std::cell::Cell;
5284
5285 use serde_json::json;
5286
5287 fn recorder() -> (Rc<RefCell<Vec<String>>>, impl Fn(&Msg) + 'static) {
5289 let log = Rc::new(RefCell::new(Vec::new()));
5290 let l2 = log.clone();
5291 (log, move |m: &Msg| l2.borrow_mut().push(format!("{m:?}")))
5292 }
5293
5294 #[test]
5295 fn status_freshness_and_terminality() {
5296 assert!(Status::Settled.is_fresh());
5297 assert!(Status::Resolved.is_fresh());
5298 assert!(!Status::Dirty.is_fresh());
5299 assert!(Status::Completed.is_terminal());
5300 assert!(!Status::Settled.is_terminal());
5301 }
5302
5303 #[test]
5304 fn state_node_push_on_subscribe_and_set() {
5305 let s = Node::<i32>::state(1);
5306 let (log, sink) = recorder();
5307 let _u = s.subscribe(sink);
5308 assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5310 assert_eq!(s.cache(), Some(1));
5311
5312 s.set(2);
5313 assert_eq!(*log.borrow(), vec!["START", "DATA", "DIRTY", "DATA"]);
5315 assert_eq!(s.cache(), Some(2));
5316 assert_eq!(s.status(), Status::Settled);
5317 }
5318
5319 #[test]
5320 fn batch_commits_last_settle_and_rollback_balances_dirty() {
5321 let s = Node::<i32>::state(0);
5322 let (log, sink) = recorder();
5323 let _u = s.subscribe(sink);
5324 log.borrow_mut().clear();
5325
5326 crate::batch::batch(|_| {
5327 s.set(1);
5328 s.set(2);
5329 assert_eq!(*log.borrow(), vec!["DIRTY"]);
5330 assert_eq!(s.cache(), Some(0));
5331 });
5332 assert_eq!(*log.borrow(), vec!["DIRTY", "DATA"]);
5333 assert_eq!(s.cache(), Some(2));
5334
5335 log.borrow_mut().clear();
5336 crate::batch::batch(|bctx| {
5337 s.set(3);
5338 bctx.rollback();
5339 assert_eq!(*log.borrow(), vec!["DIRTY"]);
5340 });
5341 assert_eq!(*log.borrow(), vec!["DIRTY", "RESOLVED"]);
5342 assert_eq!(s.cache(), Some(2));
5343 }
5344
5345 #[test]
5346 fn batch_rollback_resolved_respects_resumeall_pause() {
5347 let n = Node::<i32>::producer_opts(
5348 NodeOpts {
5349 pausable: Pausable::ResumeAll,
5350 ..NodeOpts::default()
5351 },
5352 |_| {},
5353 );
5354 let (log, sink) = recorder();
5355 let _u = n.subscribe(sink);
5356 log.borrow_mut().clear();
5357
5358 let pause = LockId::new("rollback");
5359 n.up(vec![Message::Pause(pause.clone())]);
5360 crate::batch::batch(|bctx| {
5361 n.down(vec![Message::Data(Rc::new(1i32))]);
5362 bctx.rollback();
5363 assert_eq!(*log.borrow(), vec!["DIRTY"]);
5364 });
5365
5366 assert_eq!(*log.borrow(), vec!["DIRTY"]);
5367 assert_eq!(n.cache(), None);
5368 n.up(vec![Message::Resume(pause)]);
5369 assert_eq!(*log.borrow(), vec!["DIRTY", "RESOLVED"]);
5370 assert_eq!(n.status(), Status::Sentinel);
5371 }
5372
5373 #[test]
5374 fn d49_repeated_equal_value_stays_data() {
5375 let s = Node::<i32>::state(5);
5378 let (log, sink) = recorder();
5379 let _u = s.subscribe(sink);
5380 s.set(5); assert_eq!(*log.borrow(), vec!["START", "DATA", "DIRTY", "DATA"]);
5382 assert_eq!(s.status(), Status::Settled);
5383 s.set(5); assert_eq!(
5385 *log.borrow(),
5386 vec!["START", "DATA", "DIRTY", "DATA", "DIRTY", "DATA"]
5387 );
5388 assert_eq!(s.status(), Status::Settled);
5389 }
5390
5391 #[test]
5392 fn d49_filter_no_emit_synthesizes_undirty_resolved() {
5393 let a = Node::<i32>::state_empty();
5397 let evens: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5398 let v = *ctx.data::<i32>(0).unwrap();
5399 if v % 2 == 0 {
5400 ctx.emit(v);
5401 } });
5403 let (log, sink) = recorder();
5404 let seen_refs = Rc::new(RefCell::new(Vec::<usize>::new()));
5405 let evens_refs = evens.core.refs.clone();
5406 let seen_ref_count = seen_refs.clone();
5407 let _u = evens.subscribe({
5408 move |m| {
5409 if matches!(m, Message::Resolved) {
5410 seen_ref_count.borrow_mut().push(evens_refs.get());
5411 }
5412 sink(m)
5413 }
5414 });
5415 let base_refs = evens.core.refs.get();
5416 assert_eq!(*log.borrow(), vec!["START"]); a.set(3); assert_eq!(*log.borrow(), vec!["START", "DIRTY", "RESOLVED"]);
5420 assert_eq!(evens.cache(), None); assert_eq!(evens.status(), Status::Sentinel);
5422 assert!(
5423 seen_refs.borrow().iter().all(|count| *count == base_refs),
5424 "post-run RESOLVED must not promote a counted owner Core"
5425 );
5426 let (prev, batch) = evens.core.with_inner_edges(|_n, e| {
5427 (
5428 e.state.prev[0]
5429 .as_ref()
5430 .and_then(|v| v.downcast_ref::<i32>().copied()),
5431 e.state.batch[0].is_none(),
5432 )
5433 });
5434 assert_eq!(prev, Some(3));
5435 assert!(batch);
5436
5437 a.set(4); assert_eq!(
5439 *log.borrow(),
5440 vec!["START", "DIRTY", "RESOLVED", "DIRTY", "DATA"]
5441 );
5442 assert_eq!(evens.cache(), Some(4));
5443 assert_eq!(evens.status(), Status::Settled);
5444 let prev = evens.core.with_inner_edges(|_n, e| {
5445 e.state.prev[0]
5446 .as_ref()
5447 .and_then(|v| v.downcast_ref::<i32>().copied())
5448 });
5449 assert_eq!(prev, Some(4));
5450
5451 a.set(5); assert_eq!(
5453 *log.borrow(),
5454 vec!["START", "DIRTY", "RESOLVED", "DIRTY", "DATA", "DIRTY", "RESOLVED"]
5455 );
5456 assert_eq!(evens.cache(), Some(4));
5457 assert_eq!(evens.status(), Status::Resolved);
5458 let (seen_refs, prev_batch) = evens.core.with_inner_edges(|_n, e| {
5459 (
5460 seen_refs.borrow().clone(),
5461 e.state.prev[0]
5462 .as_ref()
5463 .and_then(|v| v.downcast_ref::<i32>().copied()),
5464 )
5465 });
5466 assert_eq!(
5467 seen_refs,
5468 vec![base_refs, base_refs],
5469 "RESOLVED callbacks should not increment counted Core refs"
5470 );
5471 assert_eq!(prev_batch, Some(5));
5472 }
5473
5474 #[test]
5475 fn run_wave_post_dispatch_resolved_is_borrow_free_and_rolls_dep_state() {
5476 let arena = GraphArena::new();
5480 let source = Node::<i32>::state_in_arena(&arena, 2);
5481 type SeenRuns = Rc<RefCell<Vec<(Option<i32>, Vec<i32>)>>>;
5482 let seen: SeenRuns = Rc::new(RefCell::new(Vec::new()));
5483 let evens: Node<i32> =
5484 Node::derived_opts_in_arena(&arena, vec![source.erased()], NodeOpts::default(), {
5485 let seen = seen.clone();
5486 move |ctx| {
5487 let prev = ctx.dep_records()[0]
5488 .prev_data
5489 .as_ref()
5490 .and_then(|v| v.downcast_ref::<i32>().copied());
5491 let batch = ctx
5492 .batch::<i32>(0)
5493 .into_iter()
5494 .map(|v| *v)
5495 .collect::<Vec<_>>();
5496 seen.borrow_mut().push((prev, batch));
5497 let v = *ctx.data::<i32>(0).unwrap();
5498 if v % 2 == 0 {
5499 ctx.emit(v);
5500 }
5501 }
5502 });
5503 let held: Rc<RefCell<Option<Node<i32>>>> =
5504 Rc::new(RefCell::new(Some(Node::<i32>::state_in_arena(&arena, 99))));
5505 let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
5506 let resolved_refs = Rc::new(Cell::new(usize::MAX));
5507 let _u = evens.subscribe({
5508 let held = held.clone();
5509 let refs_slot = refs_slot.clone();
5510 let resolved_refs = resolved_refs.clone();
5511 move |msg| {
5512 if matches!(msg, Message::Resolved) {
5513 if let Some(refs) = refs_slot.borrow().as_ref() {
5514 resolved_refs.set(refs.get());
5515 }
5516 let _ = held.borrow_mut().take();
5517 }
5518 }
5519 });
5520 *refs_slot.borrow_mut() = Some(evens.core.refs.clone());
5521 let refs_before = evens.core.refs.get();
5522
5523 assert_eq!(
5524 &*seen.borrow(),
5525 &[(None, vec![2])],
5526 "activation run sees the cached source wave once"
5527 );
5528 source.set(3);
5529
5530 assert!(held.borrow().is_none(), "RESOLVED callback ran borrow-free");
5531 assert_eq!(
5532 resolved_refs.get(),
5533 refs_before,
5534 "post-run RESOLVED delivery must not temporarily promote the owner Core"
5535 );
5536 assert_eq!(evens.cache(), Some(2));
5537 assert_eq!(evens.status(), Status::Resolved);
5538
5539 source.set(4);
5540 assert_eq!(
5541 &*seen.borrow(),
5542 &[(None, vec![2]), (Some(2), vec![3]), (Some(3), vec![4])],
5543 "the rejected odd wave must still roll dep prev_data before the next run"
5544 );
5545 assert_eq!(evens.cache(), Some(4));
5546 let (res_refs, rolled_prev) = evens.core.with_inner_edges(|_n, e| {
5547 (
5548 resolved_refs.get(),
5549 e.state.prev[0]
5550 .as_ref()
5551 .and_then(|v| v.downcast_ref::<i32>().copied()),
5552 )
5553 });
5554 assert_eq!(
5555 rolled_prev,
5556 Some(4),
5557 "run_wave should carry latest settled data forward in prev"
5558 );
5559 assert!(
5560 evens
5561 .core
5562 .with_inner_edges(|_n, e| e.state.batch[0].is_none()),
5563 "post-wave state must clear batch"
5564 );
5565 assert_eq!(
5566 res_refs, refs_before,
5567 "post-run RESOLVED delivery should remain borrow-free and ref-neutral"
5568 );
5569 }
5570
5571 #[test]
5572 fn run_wave_post_dispatch_panic_keeps_wave_guard() {
5573 let source = Node::<i32>::state_empty();
5576 let resolved = Rc::new(Cell::new(false));
5577 let resolved_flag = resolved.clone();
5578 let derived: Node<i32> = Node::derived(vec![source.erased()], |ctx| {
5579 let _ = ctx.data::<i32>(0).unwrap();
5580 });
5581 let _u = derived.subscribe(move |msg| {
5582 if matches!(msg, Message::Resolved) {
5583 resolved_flag.set(true);
5584 panic!("resolved callback panic");
5585 }
5586 });
5587
5588 source.set(1);
5589
5590 assert!(
5591 resolved.get(),
5592 "post-run RESOLVED should still be delivered before callback-panic recovery"
5593 );
5594 assert_eq!(
5595 derived.status(),
5596 Status::Errored,
5597 "panic during callback should land ERROR"
5598 );
5599 assert!(
5600 !derived.core.with_inner_edges(|_, e| e.wave.inside_run_wave),
5601 "WaveGuard must clear inside_run_wave during unwind"
5602 );
5603 }
5604
5605 #[test]
5606 fn unarmed_host_boundary_abort_is_ordinary_graph_error() {
5607 let source = Node::<i32>::state(1);
5608 let bad: Node<i32> = Node::derived(vec![source.erased()], |_| {
5609 crate::host_boundary::abort_host_boundary();
5610 });
5611
5612 let _u = bad.subscribe(|_| {});
5613
5614 assert_eq!(
5615 bad.status(),
5616 Status::Errored,
5617 "D431 marker helper is active only inside native host-boundary guards"
5618 );
5619 }
5620
5621 #[test]
5622 fn d1_drop_releases_upstream_subscription_and_runs_cleanup() {
5623 let deactivated = Rc::new(Cell::new(false));
5626 let a = Node::<i32>::state(1);
5627 {
5628 let flag = deactivated.clone();
5629 let mid: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
5630 let f = flag.clone();
5631 ctx.on_deactivation(move || f.set(true));
5632 ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
5633 });
5634 let _sub = mid.subscribe(|_| {}); assert!(!deactivated.get());
5636 }
5638 assert!(
5639 deactivated.get(),
5640 "dropping an active node fires its on_deactivation + detaches from deps (D1)"
5641 );
5642 let (log, sink) = recorder();
5644 let _u = a.subscribe(sink);
5645 assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5646 }
5647
5648 #[test]
5649 fn producer_runs_once_on_activation() {
5650 let p = Node::<i32>::producer(|ctx| ctx.emit(42i32));
5651 let (log, sink) = recorder();
5652 let _u = p.subscribe(sink);
5653 assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5655 assert_eq!(p.cache(), Some(42));
5656 }
5657
5658 #[test]
5659 fn derived_first_run_gate_then_recompute() {
5660 let a = Node::<i32>::state_empty();
5661 let b = Node::<i32>::state_empty();
5662 let runs = Rc::new(Cell::new(0usize));
5663 let r2 = runs.clone();
5664 let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5665 r2.set(r2.get() + 1);
5666 let x = *ctx.data::<i32>(0).unwrap();
5667 let y = *ctx.data::<i32>(1).unwrap();
5668 ctx.emit(x + y);
5669 });
5670 let (log, sink) = recorder();
5671 let _u = sum.subscribe(sink);
5672 assert_eq!(runs.get(), 0);
5674 assert_eq!(*log.borrow(), vec!["START"]);
5675
5676 a.set(3); assert_eq!(runs.get(), 0);
5678
5679 b.set(4); assert_eq!(runs.get(), 1);
5681 assert_eq!(sum.cache(), Some(7));
5682 assert_eq!(*log.borrow(), vec!["START", "DIRTY", "DATA"]);
5683
5684 a.set(10); assert_eq!(runs.get(), 2);
5686 assert_eq!(sum.cache(), Some(14));
5687 }
5688
5689 #[test]
5690 fn data_waits_while_another_dep_is_pending() {
5691 let a = Node::<i32>::state_empty();
5692 let b = Node::<i32>::state_empty();
5693 let runs = Rc::new(Cell::new(0usize));
5694 let r2 = runs.clone();
5695 let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5696 r2.set(r2.get() + 1);
5697 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
5698 });
5699 let (log, sink) = recorder();
5700 let _u = sum.subscribe(sink);
5701 assert_eq!(*log.borrow(), vec!["START"]);
5702
5703 with_wave_owner(
5704 &a.core,
5705 || sum.core.receive_from_dep(0, &Message::Dirty),
5706 || {},
5707 );
5708 with_wave_owner(
5709 &b.core,
5710 || sum.core.receive_from_dep(1, &Message::Dirty),
5711 || {},
5712 );
5713 assert_eq!(*log.borrow(), vec!["START", "DIRTY"]);
5714
5715 with_wave_owner(
5716 &a.core,
5717 || sum.core.receive_from_dep(0, &Message::Data(Rc::new(3i32))),
5718 || {},
5719 );
5720 assert_eq!(runs.get(), 0, "dep 1 is still pending");
5721
5722 with_wave_owner(
5723 &b.core,
5724 || sum.core.receive_from_dep(1, &Message::Data(Rc::new(4i32))),
5725 || {},
5726 );
5727 assert_eq!(runs.get(), 1);
5728 assert_eq!(sum.cache(), Some(7));
5729 assert_eq!(*log.borrow(), vec!["START", "DIRTY", "DATA"]);
5730 }
5731
5732 #[test]
5733 fn data_without_prior_dirty_still_satisfies_first_run_gate() {
5734 let a = Node::<i32>::state_empty();
5735 let b = Node::<i32>::state_empty();
5736 let runs = Rc::new(Cell::new(0usize));
5737 let r2 = runs.clone();
5738 let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5739 r2.set(r2.get() + 1);
5740 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
5741 });
5742 let (log, sink) = recorder();
5743 let _u = sum.subscribe(sink);
5744 assert_eq!(*log.borrow(), vec!["START"]);
5745
5746 with_wave_owner(
5747 &a.core,
5748 || sum.core.receive_from_dep(0, &Message::Data(Rc::new(3i32))),
5749 || {},
5750 );
5751 assert_eq!(runs.get(), 0, "first-run gate still waits for dep 1");
5752
5753 with_wave_owner(
5754 &b.core,
5755 || sum.core.receive_from_dep(1, &Message::Data(Rc::new(4i32))),
5756 || {},
5757 );
5758 assert_eq!(runs.get(), 1);
5759 assert_eq!(sum.cache(), Some(7));
5760 assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5761 }
5762
5763 #[test]
5764 fn paused_data_survives_until_later_invalidate_drains_pending() {
5765 let a = Node::<i32>::state(1);
5766 let b = Node::<i32>::state(10);
5767 let runs = Rc::new(Cell::new(0usize));
5768 let r2 = runs.clone();
5769 let sum: Node<i32> = Node::derived(vec![a.erased(), b.erased()], move |ctx| {
5770 r2.set(r2.get() + 1);
5771 let x = ctx.data::<i32>(0).map(|v| *v).unwrap_or(0);
5772 let y = ctx.data::<i32>(1).map(|v| *v).unwrap_or(0);
5773 ctx.emit(x + y);
5774 });
5775 let (log, sink) = recorder();
5776 let _u = sum.subscribe(sink);
5777 assert_eq!(runs.get(), 1);
5778 assert_eq!(sum.cache(), Some(11));
5779 log.borrow_mut().clear();
5780
5781 let pause = LockId::new("coalesce");
5782 sum.up(vec![Message::Pause(pause.clone())]);
5783 a.down(vec![Message::Dirty]);
5784 b.down(vec![Message::Dirty]);
5785 a.down(vec![Message::Data(Rc::new(2i32))]);
5786 assert_eq!(runs.get(), 1, "dep 1 is still pending while paused");
5787
5788 b.down(vec![Message::Invalidate]);
5789 assert_eq!(runs.get(), 1, "still paused after pending drains");
5790 sum.up(vec![Message::Resume(pause)]);
5791
5792 assert_eq!(runs.get(), 2);
5793 assert_eq!(sum.cache(), Some(2));
5794 assert_eq!(*log.borrow(), vec!["DIRTY", "INVALIDATE", "DATA"]);
5795 }
5796
5797 #[test]
5798 fn diamond_recomputes_exactly_once_two_phase() {
5799 let a = Node::<i32>::state_empty();
5801 let b: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5802 ctx.emit(*ctx.data::<i32>(0).unwrap() + 1)
5803 });
5804 let c: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5805 ctx.emit(*ctx.data::<i32>(0).unwrap() * 10)
5806 });
5807 let d_runs = Rc::new(Cell::new(0usize));
5808 let dr = d_runs.clone();
5809 let d: Node<i32> = Node::derived(vec![b.erased(), c.erased()], move |ctx| {
5810 dr.set(dr.get() + 1);
5811 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
5812 });
5813 let (log, sink) = recorder();
5814 let _u = d.subscribe(sink);
5815
5816 a.set(2); assert_eq!(d_runs.get(), 1);
5818 assert_eq!(d.cache(), Some(23));
5819 assert_eq!(*log.borrow(), vec!["START", "DIRTY", "DATA"]);
5821
5822 a.set(5); assert_eq!(d_runs.get(), 2);
5824 assert_eq!(d.cache(), Some(56));
5825 }
5826
5827 #[test]
5828 fn rom_ram_deactivation_clears_compute_cache() {
5829 let a = Node::<i32>::state(7); let dbl: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5831 ctx.emit(*ctx.data::<i32>(0).unwrap() * 2)
5832 });
5833 let (_log, sink) = recorder();
5834 let u = dbl.subscribe(sink);
5835 assert_eq!(dbl.cache(), Some(14));
5836 assert_eq!(dbl.status(), Status::Settled);
5837
5838 u(); assert_eq!(dbl.cache(), None);
5841 assert_eq!(dbl.status(), Status::Sentinel);
5842 assert_eq!(a.cache(), Some(7));
5843 }
5844
5845 #[test]
5846 #[should_panic(expected = "terminal")]
5847 fn subscribe_to_terminal_node_is_rejected() {
5848 let s = Node::<i32>::state(1);
5851 let _u = s.subscribe(|_| {}); s.down(vec![Message::Complete]); assert_eq!(s.status(), Status::Completed);
5854 let _u2 = s.subscribe(|_| {}); }
5856
5857 #[test]
5858 fn set_on_terminal_node_is_a_noop() {
5859 let s = Node::<i32>::state(1);
5862 let (log, sink) = recorder();
5863 let _u = s.subscribe(sink);
5864 assert_eq!(*log.borrow(), vec!["START", "DATA"]);
5865
5866 s.down(vec![Message::Complete]); assert_eq!(s.status(), Status::Completed);
5868 assert_eq!(*log.borrow(), vec!["START", "DATA", "COMPLETE"]);
5869
5870 s.set(5); assert_eq!(*log.borrow(), vec!["START", "DATA", "COMPLETE"]);
5872 assert_eq!(s.cache(), Some(1));
5873 assert_eq!(s.status(), Status::Completed);
5874 }
5875
5876 #[test]
5877 fn b32_dropped_node_unregisters_its_fn_releasing_captures() {
5878 struct DropFlag(Rc<Cell<bool>>);
5883 impl Drop for DropFlag {
5884 fn drop(&mut self) {
5885 self.0.set(true);
5886 }
5887 }
5888 let fn_dropped = Rc::new(Cell::new(false));
5889 {
5890 let guard = DropFlag(fn_dropped.clone());
5891 let p = Node::<i32>::producer(move |ctx| {
5892 let _hold = &guard; ctx.emit(1i32);
5894 });
5895 let _u = p.subscribe(|_| {});
5896 assert!(
5897 !fn_dropped.get(),
5898 "the fn (and its capture) is live while the node is"
5899 );
5900 }
5903 assert!(
5904 fn_dropped.get(),
5905 "a dropped node unregisters its fn from the pool, releasing the fn's captures (B32)"
5906 );
5907 }
5908
5909 #[test]
5910 fn run_wave_hook_clear_drops_captured_core_borrow_free() {
5911 let a = Node::<i32>::state(1);
5915 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
5916 let held = Node::<i32>::state(99);
5917 ctx.on_invalidate(move || {
5918 let _ = held.status();
5919 });
5920 ctx.emit(*ctx.data::<i32>(0).unwrap());
5921 });
5922 let _u = d.subscribe(|_| {});
5923
5924 a.set(2);
5925 assert_eq!(d.cache(), Some(2));
5926 }
5927
5928 #[test]
5929 fn on_invalidate_unsubscribe_affects_current_broadcast_snapshot() {
5930 let src = Node::<i32>::state(1);
5935 let victim_unsub: Rc<RefCell<Option<Unsub>>> = Rc::new(RefCell::new(None));
5936 let d: Node<i32> = {
5937 let victim_unsub = victim_unsub.clone();
5938 Node::derived(vec![src.erased()], move |ctx| {
5939 let victim_unsub = victim_unsub.clone();
5940 ctx.on_invalidate(move || {
5941 if let Some(unsub) = victim_unsub.borrow_mut().take() {
5942 unsub();
5943 }
5944 });
5945 ctx.emit(*ctx.data::<i32>(0).unwrap());
5946 })
5947 };
5948 let _keepalive = d.subscribe(|_| {});
5949 let (log, sink) = recorder();
5950 let victim = d.subscribe(sink);
5951 *victim_unsub.borrow_mut() = Some(victim);
5952 log.borrow_mut().clear();
5953
5954 src.down(vec![Message::Invalidate]);
5955
5956 assert_eq!(
5957 log.borrow().iter().filter(|m| *m == "INVALIDATE").count(),
5958 0,
5959 "a subscriber detached by onInvalidate must not receive the same INVALIDATE"
5960 );
5961 }
5962
5963 #[test]
5964 fn subscriber_callback_drops_handle_borrow_free() {
5965 let held: Rc<RefCell<Option<Node<i32>>>> =
5970 Rc::new(RefCell::new(Some(Node::<i32>::producer(|_| {}))));
5971 let source = Node::<i32>::state_empty();
5972 let h = held.clone();
5973 let _u = source.subscribe(move |m| {
5974 if matches!(m, Message::Data(_)) {
5975 let _ = h.borrow_mut().take();
5976 }
5977 });
5978
5979 source.set(1);
5980
5981 assert!(held.borrow().is_none());
5982 assert_eq!(source.cache(), Some(1));
5983 }
5984
5985 #[test]
5986 fn drop_returns_arena_slot_when_cleanup_hook_panics() {
5987 let free_before = DEFAULT_GRAPH_CORE.with(|g| g.borrow().free.len());
5991 let result = catch_unwind(AssertUnwindSafe(|| {
5992 let p = Node::<i32>::producer(|ctx| {
5993 ctx.on_deactivation(|| panic!("cleanup boom"));
5994 ctx.emit(1i32);
5995 });
5996 let _u = p.subscribe(|_| {});
5997 }));
5998 assert!(result.is_err());
5999 let free_after = DEFAULT_GRAPH_CORE.with(|g| g.borrow().free.len());
6000 assert!(
6001 free_after > free_before,
6002 "panicking cleanup still returns the arena slot to the free list"
6003 );
6004 }
6005
6006 #[test]
6007 fn drop_unregisters_fn_even_when_cleanup_hook_panics() {
6008 struct DropFlag(Rc<Cell<bool>>);
6012 impl Drop for DropFlag {
6013 fn drop(&mut self) {
6014 self.0.set(true);
6015 }
6016 }
6017
6018 let fn_dropped = Rc::new(Cell::new(false));
6019 let result = catch_unwind(AssertUnwindSafe({
6020 let fn_dropped = fn_dropped.clone();
6021 move || {
6022 let guard = DropFlag(fn_dropped);
6023 let p = Node::<i32>::producer(move |ctx| {
6024 let _keep = &guard;
6025 ctx.on_deactivation(|| panic!("cleanup boom"));
6026 ctx.emit(1i32);
6027 });
6028 let _u = p.subscribe(|_| {});
6029 }
6030 }));
6031
6032 assert!(result.is_err());
6033 assert!(
6034 fn_dropped.get(),
6035 "dispatcher unregister must drop fn captures even if user cleanup panics"
6036 );
6037 }
6038
6039 #[test]
6040 fn identity_key_changes_when_arena_slot_is_reused() {
6041 let first = Node::<i32>::producer_opts(
6042 NodeOpts {
6043 factory: Some("first".to_owned()),
6044 ..NodeOpts::default()
6045 },
6046 |_| {},
6047 );
6048 let first_key = first.erased().identity_key();
6049 drop(first);
6050
6051 let second = Node::<i32>::producer_opts(
6052 NodeOpts {
6053 factory: Some("second".to_owned()),
6054 ..NodeOpts::default()
6055 },
6056 |_| {},
6057 );
6058 let second_key = second.erased().identity_key();
6059
6060 assert_ne!(
6061 first_key, second_key,
6062 "D51 synthetic describe ids must not inherit across reused arena slots"
6063 );
6064 }
6065
6066 #[test]
6067 fn arena_generation_key_rejects_reused_slot() {
6068 let arena = GraphArena::new();
6071 let first = Node::<i32>::state_in_arena(&arena, 1);
6072 let old_key = first.core.key();
6073 let old_weak = first.core.downgrade();
6074 assert!(old_weak.borrowed_core().is_some());
6075 drop(first);
6076
6077 let second = Node::<i32>::state_in_arena(&arena, 2);
6078 let new_key = second.core.key();
6079 assert_eq!(
6080 old_key.id, new_key.id,
6081 "isolated arena should reuse the just-freed slot"
6082 );
6083 assert_ne!(old_key.generation, new_key.generation);
6084 assert!(
6085 !arena.0.borrow().is_live_key(old_key),
6086 "the old generation key is stale after slot reuse"
6087 );
6088
6089 old_weak.receive_from_dep(0, &Message::Start);
6091 assert_eq!(second.cache(), Some(2));
6092 assert_eq!(second.status(), Status::Settled);
6093 }
6094
6095 #[test]
6096 fn deferred_delivery_actions_do_not_pin_core_while_queued() {
6097 let source = Node::<i32>::state(1);
6101 let derived: Node<i32> = Node::derived(vec![source.erased()], |ctx| {
6102 ctx.emit(*ctx.data::<i32>(0).unwrap() + 1)
6103 });
6104 let _u = derived.subscribe(|_| {});
6105 let refs_before = derived.core.refs.get();
6106
6107 with_delivery_scope(|| {
6108 defer_run_until_delivery_boundary(&derived.core);
6109 defer_run_until_delivery_boundary(&derived.core);
6110 defer_absorbed_settle_until_delivery_boundary(&derived.core);
6111 defer_absorbed_settle_until_delivery_boundary(&derived.core);
6112
6113 assert_eq!(
6114 derived.core.refs.get(),
6115 refs_before,
6116 "queued delivery-boundary actions must not hold counted Core refs"
6117 );
6118 DEFERRED_DELIVERY_ACTIONS.with(|actions| {
6119 let actions = actions.borrow();
6120 assert_eq!(
6121 actions.len(),
6122 2,
6123 "delivery-boundary actions dedupe by arena key and action kind"
6124 );
6125 assert_eq!(
6126 actions
6127 .iter()
6128 .filter(|action| action.kind == DeferredDeliveryKind::Run)
6129 .count(),
6130 1,
6131 "run actions dedupe by arena key"
6132 );
6133 assert_eq!(
6134 actions
6135 .iter()
6136 .filter(|action| action.kind == DeferredDeliveryKind::AbsorbedSettle)
6137 .count(),
6138 1,
6139 "absorbed-settle actions dedupe by arena key"
6140 );
6141 });
6142 });
6143
6144 assert_eq!(
6145 derived.core.refs.get(),
6146 refs_before,
6147 "delivery-boundary drain does not retain queued counted Core refs"
6148 );
6149 }
6150
6151 #[test]
6152 fn active_wave_deferred_delivery_drain_uses_borrowed_core() {
6153 let source = Node::<i32>::state_empty();
6157 let observed_refs = Rc::new(Cell::new(usize::MAX));
6158 let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
6159 let derived: Node<i32> = Node::derived(vec![source.erased()], {
6160 let observed_refs = observed_refs.clone();
6161 let refs_slot = refs_slot.clone();
6162 move |ctx| {
6163 if let Some(refs) = refs_slot.borrow().as_ref() {
6164 observed_refs.set(refs.get());
6165 }
6166 ctx.emit(*ctx.data::<i32>(0).unwrap());
6167 }
6168 });
6169 *refs_slot.borrow_mut() = Some(derived.core.refs.clone());
6170 let _u = derived.subscribe(|_| {});
6171 let refs_before = derived.core.refs.get();
6172
6173 source.set(1);
6174
6175 assert_eq!(
6176 observed_refs.get(),
6177 refs_before,
6178 "active-wave delivery-boundary drain should run through a borrowed arena view"
6179 );
6180 assert_eq!(derived.core.refs.get(), refs_before);
6181 }
6182
6183 #[test]
6184 fn deferred_delivery_drains_run_before_absorbed_settle() {
6185 let events = Rc::new(RefCell::new(Vec::<&'static str>::new()));
6188 let derived: Node<i32> = Node::derived(vec![], {
6189 let events = events.clone();
6190 move |_ctx| {
6191 events.borrow_mut().push("run");
6192 }
6193 });
6194 let _u = derived.subscribe({
6195 let events = events.clone();
6196 move |msg| {
6197 if let Message::Resolved = msg {
6198 events.borrow_mut().push("resolved");
6199 }
6200 }
6201 });
6202 events.borrow_mut().clear();
6203
6204 derived
6206 .core
6207 .with_inner_edges_mut(|_n, e| e.wave.emitted_dirty_this_wave = true);
6208
6209 with_delivery_scope(|| {
6210 defer_run_until_delivery_boundary(&derived.core);
6211 defer_absorbed_settle_until_delivery_boundary(&derived.core);
6212 });
6213
6214 assert_eq!(
6215 &*events.borrow(),
6216 &["run", "resolved"],
6217 "deferred Run must drain before AbsorbedSettle"
6218 );
6219 }
6220
6221 #[test]
6222 fn deferred_delivery_outside_wave_run_does_not_increment_refs() {
6223 let source = Node::<i32>::state(1);
6226 let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
6227 let observed_refs = Rc::new(Cell::new(usize::MAX));
6228 let run_count = Rc::new(Cell::new(0usize));
6229
6230 let derived: Node<i32> = Node::derived_opts(
6231 vec![source.erased()],
6232 NodeOpts {
6233 partial: true,
6234 ..NodeOpts::default()
6235 },
6236 {
6237 let refs_slot = refs_slot.clone();
6238 let observed_refs = observed_refs.clone();
6239 let run_count = run_count.clone();
6240 move |_ctx| {
6241 if let Some(refs) = refs_slot.borrow().as_ref() {
6242 observed_refs.set(refs.get());
6243 }
6244 run_count.set(run_count.get() + 1);
6245 }
6246 },
6247 );
6248
6249 *refs_slot.borrow_mut() = Some(derived.core.refs.clone());
6250 let before = derived.core.refs.get();
6251
6252 with_delivery_scope(|| {
6253 defer_run_until_delivery_boundary(&derived.core);
6254 });
6255
6256 assert_eq!(
6257 run_count.get(),
6258 1,
6259 "queued delivery-boundary run must execute"
6260 );
6261 assert_eq!(
6262 observed_refs.get(),
6263 before,
6264 "outside-wave delivery-boundary run must execute from a borrowed pin"
6265 );
6266 }
6267
6268 #[test]
6269 fn stale_deferred_delivery_action_does_not_run_after_slot_free() {
6270 DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
6273
6274 let arena = GraphArena::new();
6275 let old = Node::<i32>::state_in_arena(&arena, 1);
6276 let old_key = old.core.key();
6277 let weak = old.core.downgrade();
6278 let stale_run = DeferredDeliveryAction::from_core(DeferredDeliveryKind::Run, &old.core);
6279 let stale_settle =
6280 DeferredDeliveryAction::from_core(DeferredDeliveryKind::AbsorbedSettle, &old.core);
6281
6282 drop(old);
6283
6284 assert!(
6285 weak.borrowed_core().is_none(),
6286 "old slot is not live after drop"
6287 );
6288 assert_eq!(weak.key.generation, old_key.generation);
6289
6290 DEFERRED_DELIVERY_ACTIONS.with(|actions| {
6291 let mut actions = actions.borrow_mut();
6292 actions.push(stale_run);
6293 actions.push(stale_settle);
6294 });
6295 drain_deferred_runs();
6296
6297 assert!(
6298 weak.borrowed_core().is_none(),
6299 "freed slot remains out-of-scope for stale deferred work"
6300 );
6301 }
6302
6303 #[test]
6304 fn weak_borrowed_core_uses_arena_pin_after_external_death() {
6305 let arena = GraphArena::new();
6308 let graph = arena.0.clone();
6309 let victim = Node::<i32>::state_in_arena(&arena, 1);
6310 let key = victim.core.key();
6311 let weak = victim.core.downgrade();
6312 let refs = victim.core.refs.clone();
6313 let pin = ArenaNodePin::from_core(&victim.core).expect("live slot pins");
6314 drop(victim);
6315
6316 assert_eq!(refs.get(), 0);
6317 assert!(graph.borrow().is_live_key(key));
6318 assert!(
6319 weak.borrowed_core().is_some(),
6320 "active arena pin should allow borrowed weak rehydration without reviving refs"
6321 );
6322 drop(pin);
6323 assert!(!graph.borrow().is_live_key(key));
6324 assert!(
6325 weak.borrowed_core().is_none(),
6326 "borrowed weak rehydration must fail once the pin releases"
6327 );
6328 }
6329
6330 #[test]
6331 fn deferred_delivery_run_skips_same_wave_terminalized_target() {
6332 let source = Node::<i32>::state_empty();
6336 let runs = Rc::new(Cell::new(0usize));
6337 let derived: Node<i32> = Node::derived(vec![source.erased()], {
6338 let runs = runs.clone();
6339 move |ctx| {
6340 runs.set(runs.get() + 1);
6341 ctx.emit(*ctx.data::<i32>(0).unwrap());
6342 }
6343 });
6344 let _u = derived.subscribe(|_| {});
6345
6346 source.down(vec![Message::Data(Rc::new(1i32)), Message::Complete]);
6347
6348 assert_eq!(
6349 runs.get(),
6350 0,
6351 "queued delivery-boundary run must not execute after same-wave terminal"
6352 );
6353 assert_eq!(derived.status(), Status::Completed);
6354 }
6355
6356 #[test]
6357 fn stale_deferred_delivery_action_does_not_target_reused_slot() {
6358 DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
6362
6363 let arena = GraphArena::new();
6364 let old = Node::<i32>::state_in_arena(&arena, 1);
6365 let stale_run = DeferredDeliveryAction::from_core(DeferredDeliveryKind::Run, &old.core);
6366 let stale_settle =
6367 DeferredDeliveryAction::from_core(DeferredDeliveryKind::AbsorbedSettle, &old.core);
6368 let old_key = old.core.key();
6369 drop(old);
6370
6371 let reused = Node::<i32>::state_in_arena(&arena, 2);
6372 assert_eq!(old_key.id, reused.core.key().id);
6373 assert_ne!(old_key.generation, reused.core.key().generation);
6374
6375 DEFERRED_DELIVERY_ACTIONS.with(|actions| {
6376 let mut actions = actions.borrow_mut();
6377 actions.push(stale_run);
6378 actions.push(stale_settle);
6379 });
6380 drain_deferred_runs();
6381
6382 assert_eq!(reused.cache(), Some(2));
6383 assert_eq!(
6384 reused.status(),
6385 Status::Settled,
6386 "stale deferred generation must not disturb the reused slot occupant"
6387 );
6388 }
6389
6390 #[test]
6391 fn panic_aborted_delivery_clears_deferred_delivery_actions() {
6392 DEFERRED_DELIVERY_ACTIONS.with(|actions| actions.borrow_mut().clear());
6397
6398 let source = Node::<i32>::state_empty();
6399 let runs = Rc::new(Cell::new(0usize));
6400 let d: Node<i32> = Node::derived(vec![source.erased()], {
6401 let runs = runs.clone();
6402 move |_ctx| {
6403 runs.set(runs.get() + 1);
6404 }
6405 });
6406 let _d_sub = d.subscribe(|_| {});
6407 let _panic_sub = source.subscribe(|m| {
6408 if matches!(m, Message::Data(_)) {
6409 panic!("subscriber aborts delivery after dep callback queued run");
6410 }
6411 });
6412
6413 source.set(1);
6414
6415 assert_eq!(runs.get(), 0, "aborted delivery must not run the queued fn");
6416 DEFERRED_DELIVERY_ACTIONS.with(|queued| {
6417 assert!(
6418 queued.borrow().is_empty(),
6419 "aborted wave must clear queued deferred delivery actions"
6420 );
6421 });
6422
6423 let unrelated = Node::<i32>::state_empty();
6424 let _u = unrelated.subscribe(|_| {});
6425 unrelated.set(1);
6426 assert_eq!(
6427 runs.get(),
6428 0,
6429 "later unrelated delivery must not drain stale work from the aborted wave"
6430 );
6431 }
6432
6433 #[test]
6434 fn internal_dep_callback_entry_does_not_increment_core_refcount() {
6435 let arena = GraphArena::new();
6440 let source = Node::<i32>::state_in_arena(&arena, 1);
6441 let derived: Node<i32> = Node::derived_opts_in_arena(
6442 &arena,
6443 vec![source.erased()],
6444 NodeOpts::default(),
6445 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6446 );
6447 let weak = derived.core.downgrade();
6448 let refs_before = derived.core.refs.get();
6449
6450 let borrowed = with_wave_owner(&source.core, || weak.borrowed_core(), || None)
6451 .expect("borrowed core should exist while the node is live");
6452 assert_eq!(
6453 derived.core.refs.get(),
6454 refs_before,
6455 "borrowed weak dep entry should not increment Core refs while held"
6456 );
6457 drop(borrowed);
6458
6459 assert_eq!(
6460 derived.core.refs.get(),
6461 refs_before,
6462 "internal weak dep entry should borrow by arena key, not create a counted Core"
6463 );
6464 }
6465
6466 #[test]
6467 fn internal_dep_callback_entry_uses_borrowed_pin_outside_wave() {
6468 let arena = GraphArena::new();
6471 let source = Node::<i32>::state_in_arena(&arena, 1);
6472 let derived: Node<i32> = Node::derived_opts_in_arena(
6473 &arena,
6474 vec![source.erased()],
6475 NodeOpts::default(),
6476 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6477 );
6478 let weak = derived.core.downgrade();
6479 let refs_before = derived.core.refs.get();
6480
6481 weak.receive_from_dep(0, &Message::Start);
6482 assert_eq!(
6483 derived.core.refs.get(),
6484 refs_before,
6485 "outside a wave, weak dep entry must not promote a counted Core"
6486 );
6487 }
6488
6489 #[test]
6490 fn internal_dep_callback_entry_outside_wave_uses_pinned_borrow_when_refcount_dead() {
6491 let arena = GraphArena::new();
6495 let source = Node::<i32>::state_in_arena(&arena, 1);
6496 let derived: Node<i32> = Node::derived_opts_in_arena(
6497 &arena,
6498 vec![source.erased()],
6499 NodeOpts::default(),
6500 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6501 );
6502 let weak = derived.core.downgrade();
6503 let borrowed = derived.core.borrowed_view();
6504 let pin = ArenaNodePin::from_core(&derived.core).expect("node must be pinnable while live");
6505 let refs = derived.core.refs.clone();
6506
6507 drop(derived);
6508 assert_eq!(
6509 refs.get(),
6510 0,
6511 "setup drops the final public node handle so receive path is stress-tested with no counted refs"
6512 );
6513
6514 weak.receive_from_dep(0, &Message::Data(Rc::new(4i32)));
6515 assert_eq!(
6516 refs.get(),
6517 0,
6518 "pinned receive should not promote a counted Core when the slot is already externally dead"
6519 );
6520 assert_eq!(
6521 borrowed
6522 .cache_any()
6523 .and_then(|v| v.downcast_ref::<i32>().copied()),
6524 Some(4),
6525 "borrowed owner execution should still run the dep callback while pinned"
6526 );
6527
6528 with_wave_owner(
6529 &source.core,
6530 || weak.receive_from_dep(0, &Message::Data(Rc::new(5i32))),
6531 || {},
6532 );
6533 assert_eq!(
6534 refs.get(),
6535 0,
6536 "in-wave pinned receive should also avoid counted Core promotion"
6537 );
6538 assert_eq!(
6539 borrowed
6540 .cache_any()
6541 .and_then(|v| v.downcast_ref::<i32>().copied()),
6542 Some(5),
6543 "in-wave collect/apply must not half-apply dep bookkeeping then skip the action"
6544 );
6545 drop(pin);
6546 }
6547
6548 #[test]
6549 fn internal_dep_callback_entry_run_decision_executes_from_borrowed_action() {
6550 let arena = GraphArena::new();
6553 let source = Node::<i32>::state_empty_in_arena(&arena);
6554 let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
6555 let observed_refs_in_fn = Rc::new(Cell::new(usize::MAX));
6556 let run_count = Rc::new(Cell::new(0usize));
6557
6558 let derived = Node::<i32>::derived_opts_in_arena(
6559 &arena,
6560 vec![source.erased()],
6561 NodeOpts::default(),
6562 {
6563 let refs_slot = refs_slot.clone();
6564 let observed_refs_in_fn = observed_refs_in_fn.clone();
6565 let run_count = run_count.clone();
6566 move |ctx| {
6567 run_count.set(run_count.get() + 1);
6568 let refs = refs_slot
6569 .borrow()
6570 .as_ref()
6571 .expect("test installed ref counter before run")
6572 .clone();
6573 observed_refs_in_fn.set(refs.get());
6574 ctx.emit(*ctx.data::<i32>(0).unwrap());
6575 }
6576 },
6577 );
6578 *refs_slot.borrow_mut() = Some(derived.core.refs.clone());
6579 let _u = derived.subscribe(|_| {});
6580 let baseline_refs = derived.core.refs.get();
6581 assert_eq!(
6582 run_count.get(),
6583 0,
6584 "first-run gate should hold before source DATA"
6585 );
6586
6587 let weak = derived.core.downgrade();
6588 let action = with_wave_owner(
6589 &source.core,
6590 || {
6591 weak.collect_in_wave_receive_from_dep_action(0, &Message::Data(Rc::new(4i32)))
6592 .expect("in-wave receive should collect a runnable action")
6593 },
6594 InWaveDepReceiveAction::default,
6595 );
6596
6597 assert!(
6598 matches!(action.maybe_run_decision, MaybeRunDecision::Run),
6599 "first in-wave settle for a ready dep should materialize as Run"
6600 );
6601
6602 weak.apply_in_wave_receive_action(action);
6603
6604 assert_eq!(
6605 run_count.get(),
6606 1,
6607 "borrowed in-wave action must still execute one fn run"
6608 );
6609 assert_eq!(
6610 observed_refs_in_fn.get(),
6611 baseline_refs,
6612 "fn execution from borrowed action must not re-count owner refs"
6613 );
6614 assert_eq!(derived.cache(), Some(4));
6615 }
6616
6617 #[test]
6618 fn stale_in_wave_receive_action_is_generation_checked() {
6619 let arena = GraphArena::new();
6622 let source = Node::<i32>::state_in_arena(&arena, 1);
6623 let run_count = Rc::new(Cell::new(0usize));
6624
6625 let (weak, old_key, action) = {
6626 let derived = Node::<i32>::derived_opts_in_arena(
6627 &arena,
6628 vec![source.erased()],
6629 NodeOpts::default(),
6630 {
6631 let run_count = run_count.clone();
6632 move |ctx| {
6633 run_count.set(run_count.get() + 1);
6634 ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
6635 }
6636 },
6637 );
6638 let old_key = derived.core.key();
6639 let weak = derived.core.downgrade();
6640 let action = with_wave_owner(
6641 &source.core,
6642 || {
6643 weak.collect_in_wave_receive_from_dep_action(0, &Message::Data(Rc::new(9i32)))
6644 .expect("in-wave receive should collect a runnable action")
6645 },
6646 InWaveDepReceiveAction::default,
6647 );
6648 (weak, old_key, action)
6649 };
6650
6651 let replacement = Node::<i32>::state_in_arena(&arena, 2);
6652 assert_eq!(old_key.id, replacement.core.key().id);
6653 assert_ne!(old_key.generation, replacement.core.key().generation);
6654
6655 weak.apply_in_wave_receive_action(action);
6656
6657 assert_eq!(
6658 run_count.get(),
6659 0,
6660 "stale in-wave action must not execute after generation drift"
6661 );
6662 assert_eq!(
6663 replacement.cache(),
6664 Some(2),
6665 "reused slot should keep its own initial cache when action is stale"
6666 );
6667 }
6668
6669 #[test]
6670 fn in_flight_passthrough_decision_waits_for_delivery_boundary() {
6671 let arena = GraphArena::new();
6676 let source = Node::<i32>::state_empty_in_arena(&arena);
6677 let wire = Node::<i32>::from_core(Core::new_in_arena(
6678 &arena,
6679 vec![source.erased()],
6680 None,
6681 default_dispatcher(),
6682 None,
6683 NodeOpts::default(),
6684 ));
6685 let seen = Rc::new(RefCell::new(Vec::<i32>::new()));
6686 let seen_sink = seen.clone();
6687 let _u = wire.subscribe(move |msg| {
6688 if let Message::Data(v) = msg {
6689 seen_sink
6690 .borrow_mut()
6691 .push(*v.downcast_ref::<i32>().expect("wire emits i32"));
6692 }
6693 });
6694
6695 source.down(vec![
6696 Message::Data(Rc::new(1i32)),
6697 Message::Data(Rc::new(2i32)),
6698 ]);
6699
6700 assert_eq!(
6701 seen.borrow().as_slice(),
6702 &[2],
6703 "passthrough must coalesce one upstream wave to its latest DATA"
6704 );
6705 assert_eq!(wire.cache(), Some(2));
6706 }
6707
6708 #[test]
6709 fn in_flight_pause_resume_preserves_single_wave_projection() {
6710 let arena = GraphArena::new();
6716 let source = Node::<i32>::state_empty_in_arena(&arena);
6717 let lock = LockId::from("qa-in-flight-resume");
6718 let batches_seen = Rc::new(RefCell::new(Vec::<Vec<i32>>::new()));
6719 let runs = Rc::new(Cell::new(0usize));
6720 let derived: Node<i32> =
6721 Node::derived_opts_in_arena(&arena, vec![source.erased()], NodeOpts::default(), {
6722 let batches_seen = batches_seen.clone();
6723 let runs = runs.clone();
6724 move |ctx| {
6725 runs.set(runs.get() + 1);
6726 batches_seen
6727 .borrow_mut()
6728 .push(ctx.batch::<i32>(0).into_iter().map(|v| *v).collect());
6729 ctx.emit(*ctx.data::<i32>(0).expect("latest source DATA"));
6730 }
6731 });
6732 let _derived_sub = derived.subscribe(|_| {});
6733 derived.up(vec![Message::Pause(lock.clone())]);
6734 let derived_for_resume = derived.clone();
6735 let lock_for_resume = lock.clone();
6736 let _controller = source.subscribe(move |msg| {
6737 if matches!(msg, Message::Data(_)) {
6738 derived_for_resume.up(vec![Message::Resume(lock_for_resume.clone())]);
6739 }
6740 });
6741
6742 source.down(vec![
6743 Message::Data(Rc::new(1i32)),
6744 Message::Data(Rc::new(2i32)),
6745 ]);
6746
6747 assert_eq!(runs.get(), 1, "receiver fn should run once at boundary");
6748 assert_eq!(
6749 batches_seen.borrow().as_slice(),
6750 &[vec![1, 2]],
6751 "receiver must see one upstream wave projection, not two split runs"
6752 );
6753 assert_eq!(derived.cache(), Some(2));
6754 }
6755
6756 #[test]
6757 fn internal_dep_callback_entry_out_of_wave_does_not_inflate_live_refcount() {
6758 let arena = GraphArena::new();
6761 let source = Node::<i32>::state_empty_in_arena(&arena);
6762 let observed_refs = Rc::new(Cell::new(0usize));
6763 let derived: Node<i32> = Node::derived_opts_in_arena(
6764 &arena,
6765 vec![source.erased()],
6766 NodeOpts::default(),
6767 |ctx| {
6768 if let Some(v) = ctx.data::<i32>(0) {
6769 ctx.emit(*v);
6770 }
6771 },
6772 );
6773 let weak = derived.core.downgrade();
6774
6775 let _sub = derived.subscribe({
6776 let observed_refs = observed_refs.clone();
6777 let live_refs = derived.core.refs.clone();
6778 move |msg| {
6779 if matches!(msg, Message::Data(_)) {
6780 observed_refs.set(live_refs.get());
6781 }
6782 }
6783 });
6784 let live_refs = derived.core.refs.clone();
6785 let baseline = live_refs.get();
6786
6787 weak.receive_from_dep(0, &Message::Data(Rc::new(7i32)));
6788
6789 assert_eq!(
6790 observed_refs.get(),
6791 baseline,
6792 "out-of-wave receive should not promote/ref-bump while still borrowed by arena pin"
6793 );
6794 assert_eq!(derived.cache(), Some(7));
6795 assert_eq!(
6796 derived.core.refs.get(),
6797 baseline,
6798 "out-of-wave borrowed execution should preserve live counted handle count"
6799 );
6800 }
6801
6802 #[test]
6803 fn internal_dep_callback_entry_out_of_wave_refcount_dead_pin_stays_fail_closed() {
6804 let arena = GraphArena::new();
6807 let graph = arena.0.clone();
6808 let source = Node::<i32>::state_in_arena(&arena, 1);
6809 let derived: Node<i32> = Node::derived_opts_in_arena(
6810 &arena,
6811 vec![source.erased()],
6812 NodeOpts::default(),
6813 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
6814 );
6815 let weak = derived.core.downgrade();
6816 let key = derived.core.key();
6817 let refs = derived.core.refs.clone();
6818 let borrowed = derived.core.borrowed_view();
6819 let pin = ArenaNodePin::from_core(&derived.core).expect("node must be pinnable while live");
6820
6821 drop(derived);
6822
6823 assert_eq!(
6824 refs.get(),
6825 0,
6826 "setup leaves no counted owner so this only exercises fail-closed pin behavior"
6827 );
6828 weak.receive_from_dep(0, &Message::Data(Rc::new(4i32)));
6829 assert_eq!(
6830 borrowed
6831 .cache_any()
6832 .and_then(|v| v.downcast_ref::<i32>().copied()),
6833 Some(4),
6834 "pinned borrowed receive should still execute while the slot is temporarily alive"
6835 );
6836
6837 drop(pin);
6838 assert!(
6839 !graph.borrow().is_live_key(key),
6840 "pin release should allow slot cleanup"
6841 );
6842 assert!(
6843 weak.counted_core().is_none(),
6844 "fail-closed should not revive a dead counted handle after pin release"
6845 );
6846 assert!(
6847 weak.borrowed_core().is_none(),
6848 "fail-closed should not rehydrate from dead refs with no pin"
6849 );
6850 assert!(
6851 weak.pinned_borrowed_core().is_none(),
6852 "fail-closed should not construct a borrowed pin path after release"
6853 );
6854 weak.receive_from_dep(0, &Message::Data(Rc::new(5i32)));
6855 assert_eq!(
6856 refs.get(),
6857 0,
6858 "receive from a dead slot must not resurrect refs"
6859 );
6860 }
6861
6862 #[test]
6863 fn internal_up_route_broadcast_uses_borrowed_deps() {
6864 let arena = GraphArena::new();
6868 let a = Node::<i32>::state_in_arena(&arena, 1);
6869 let b = Node::<i32>::state_in_arena(&arena, 2);
6870 let a_refs = a.core.refs.clone();
6871 let b_refs = b.core.refs.clone();
6872 let graph = arena.0.clone();
6873 let a_key = a.core.key();
6874 let b_key = b.core.key();
6875 let observed: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6876 let observed_pins: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6877 let _ua = a.subscribe({
6878 let observed = observed.clone();
6879 let observed_pins = observed_pins.clone();
6880 let a_refs = a_refs.clone();
6881 let graph = graph.clone();
6882 move |msg| {
6883 if matches!(msg, Message::Invalidate) {
6884 observed.borrow_mut().push(a_refs.get());
6885 observed_pins
6886 .borrow_mut()
6887 .push(graph.borrow().pin_count(a_key));
6888 }
6889 }
6890 });
6891 let _ub = b.subscribe({
6892 let observed = observed.clone();
6893 let observed_pins = observed_pins.clone();
6894 let b_refs = b_refs.clone();
6895 let graph = graph.clone();
6896 move |msg| {
6897 if matches!(msg, Message::Invalidate) {
6898 observed.borrow_mut().push(b_refs.get());
6899 observed_pins
6900 .borrow_mut()
6901 .push(graph.borrow().pin_count(b_key));
6902 }
6903 }
6904 });
6905
6906 let parent = Node::<i32>::derived_opts_in_arena(
6907 &arena,
6908 vec![a.erased(), b.erased()],
6909 NodeOpts::default(),
6910 |_| {},
6911 );
6912 let baseline = (a_refs.get(), b_refs.get());
6913 parent.up(vec![Message::Invalidate]);
6914
6915 let observed = observed.borrow();
6916 assert_eq!(
6917 observed.len(),
6918 2,
6919 "both deps should receive one in-wave INVALIDATE control wave"
6920 );
6921 assert_eq!(
6922 observed[0], baseline.0,
6923 "dep a must not pick up a temporary counted up-route ref bump"
6924 );
6925 assert_eq!(
6926 observed[1], baseline.1,
6927 "dep b must not pick up a temporary counted up-route ref bump"
6928 );
6929 assert_eq!(
6930 *observed_pins.borrow(),
6931 vec![2, 2],
6932 "in-wave borrowed up-route should keep the wave pin and add a scoped route pin around each dep callback"
6933 );
6934 }
6935
6936 #[test]
6937 fn out_of_wave_up_route_broadcast_uses_borrowed_deps() {
6938 let arena = GraphArena::new();
6941 let a = Node::<i32>::state_in_arena(&arena, 1);
6942 let b = Node::<i32>::state_in_arena(&arena, 2);
6943 let a_refs = a.core.refs.clone();
6944 let b_refs = b.core.refs.clone();
6945 let graph = arena.0.clone();
6946 let a_key = a.core.key();
6947 let b_key = b.core.key();
6948 let observed: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6949 let observed_pins: Rc<RefCell<Vec<usize>>> = Rc::new(RefCell::new(Vec::new()));
6950 let _ua = a.subscribe({
6951 let observed = observed.clone();
6952 let observed_pins = observed_pins.clone();
6953 let a_refs = a_refs.clone();
6954 let graph = graph.clone();
6955 move |msg| {
6956 if matches!(msg, Message::Invalidate) {
6957 observed.borrow_mut().push(a_refs.get());
6958 observed_pins
6959 .borrow_mut()
6960 .push(graph.borrow().pin_count(a_key));
6961 }
6962 }
6963 });
6964 let _ub = b.subscribe({
6965 let observed = observed.clone();
6966 let observed_pins = observed_pins.clone();
6967 let b_refs = b_refs.clone();
6968 let graph = graph.clone();
6969 move |msg| {
6970 if matches!(msg, Message::Invalidate) {
6971 observed.borrow_mut().push(b_refs.get());
6972 observed_pins
6973 .borrow_mut()
6974 .push(graph.borrow().pin_count(b_key));
6975 }
6976 }
6977 });
6978
6979 let parent = Node::<i32>::derived_opts_in_arena(
6980 &arena,
6981 vec![a.erased(), b.erased()],
6982 NodeOpts::default(),
6983 |_| {},
6984 );
6985 let baseline = (a_refs.get(), b_refs.get());
6986 parent.core.up(vec![Message::Invalidate], None);
6987
6988 let observed = observed.borrow();
6989 assert_eq!(
6990 observed.len(),
6991 2,
6992 "both deps should receive one out-of-wave INVALIDATE control wave"
6993 );
6994 assert_eq!(
6995 observed[0], baseline.0,
6996 "dep a must not pick up a temporary counted up-route ref bump"
6997 );
6998 assert_eq!(
6999 observed[1], baseline.1,
7000 "dep b must not pick up a temporary counted up-route ref bump"
7001 );
7002 assert_eq!(
7003 *observed_pins.borrow(),
7004 vec![1, 1],
7005 "out-of-wave borrowed up-route should hold an arena pin around each dep callback"
7006 );
7007 }
7008
7009 #[test]
7010 fn public_wave_owner_entry_does_not_preclone_core() {
7011 let source = Node::<i32>::state_empty();
7017 let refs = source.core.refs.clone();
7018 let seen_refs = Rc::new(Cell::new(0usize));
7019 let seen = seen_refs.clone();
7020 let _u = source.subscribe(move |m| {
7021 if matches!(m, Message::Data(_)) {
7022 seen.set(refs.get());
7023 }
7024 });
7025
7026 source.set(1);
7027
7028 assert_eq!(
7029 seen_refs.get(),
7030 2,
7031 "expected source handle + unsubscribe handle; wave-owner/touched arena pins must not increment Core refs"
7032 );
7033 }
7034
7035 #[test]
7036 fn touched_arena_pin_defers_slot_free_without_core_refcount_pin() {
7037 let arena = GraphArena::new();
7041 let graph = arena.0.clone();
7042 let owner = Node::<i32>::state_in_arena(&arena, 0);
7043 let victim = Node::<i32>::state_in_arena(&arena, 1);
7044 let victim_key = victim.core.key();
7045 let victim_refs = victim.core.refs.clone();
7046 let graph_in_wave = graph.clone();
7047
7048 with_wave_owner(
7049 &owner.core,
7050 move || {
7051 assert_eq!(victim_refs.get(), 1);
7052 wave_register(&victim.core);
7053 assert_eq!(
7054 victim_refs.get(),
7055 1,
7056 "arena touched pin must not increment counted Core refs"
7057 );
7058 drop(victim);
7059 assert_eq!(
7060 victim_refs.get(),
7061 0,
7062 "dropping the final public handle marks the slot externally dead"
7063 );
7064 assert!(
7065 graph_in_wave.borrow().is_live_key(victim_key),
7066 "arena pin keeps the touched slot live until wave exit"
7067 );
7068 },
7069 || {},
7070 );
7071
7072 assert!(
7073 !graph.borrow().is_live_key(victim_key),
7074 "wave exit releases the arena pin and frees the externally-dead slot"
7075 );
7076 let reused = Node::<i32>::state_in_arena(&arena, 2);
7077 assert_eq!(victim_key.id, reused.core.key().id);
7078 assert_ne!(victim_key.generation, reused.core.key().generation);
7079 }
7080
7081 #[test]
7082 fn touched_arena_pin_dedupes_same_node_within_wave() {
7083 let arena = GraphArena::new();
7087 let graph = arena.0.clone();
7088 let owner = Node::<i32>::state_in_arena(&arena, 0);
7089 let victim = Node::<i32>::state_in_arena(&arena, 1);
7090 let victim_key = victim.core.key();
7091 let victim_refs = victim.core.refs.clone();
7092 let graph_in_wave = graph.clone();
7093
7094 with_wave_owner(
7095 &owner.core,
7096 move || {
7097 wave_register(&victim.core);
7098 wave_register(&victim.core);
7099 wave_register(&victim.core);
7100 WAVE.with(|w| {
7101 let wave = w.borrow();
7102 let scope = wave.as_ref().expect("wave installed");
7103 assert_eq!(scope.touched.len(), 1);
7104 });
7105 assert_eq!(
7106 graph_in_wave.borrow().pin_count(victim_key),
7107 1,
7108 "duplicate touches should share one arena pin"
7109 );
7110 drop(victim);
7111 assert_eq!(
7112 victim_refs.get(),
7113 0,
7114 "the deduped arena pin still allows external refs to reach zero"
7115 );
7116 assert!(
7117 graph_in_wave.borrow().is_live_key(victim_key),
7118 "one deduped pin must still keep the slot live through wave exit"
7119 );
7120 },
7121 || {},
7122 );
7123
7124 assert!(
7125 !graph.borrow().is_live_key(victim_key),
7126 "deduped pin releases at wave exit and frees externally-dead slot"
7127 );
7128 }
7129
7130 #[test]
7131 fn touched_arena_pins_release_before_committed_boundary_drain() {
7132 let arena = GraphArena::new();
7136 let graph = arena.0.clone();
7137 let owner = Node::<i32>::state_in_arena(&arena, 1);
7138 let victim = Node::<i32>::state_in_arena(&arena, 2);
7139 let victim_key = victim.core.key();
7140 let saw_boundary = Rc::new(Cell::new(false));
7141 let saw = saw_boundary.clone();
7142 let graph_for_sink = graph.clone();
7143 let _u = owner.subscribe(move |m| {
7144 if matches!(m, Message::Invalidate) {
7145 saw.set(true);
7146 assert!(
7147 !graph_for_sink.borrow().is_live_key(victim_key),
7148 "touched pins must release before committed-boundary tasks run"
7149 );
7150 }
7151 });
7152 let owner_core = owner.core.clone();
7153
7154 with_wave_owner(
7155 &owner.core,
7156 move || {
7157 wave_register(&victim.core);
7158 drop(victim);
7159 owner_core.request_up_next(vec![Message::Invalidate], None);
7160 },
7161 || {},
7162 );
7163
7164 assert!(saw_boundary.get(), "queued boundary INVALIDATE should run");
7165 let reused = Node::<i32>::state_in_arena(&arena, 3);
7166 assert_eq!(victim_key.id, reused.core.key().id);
7167 assert_ne!(victim_key.generation, reused.core.key().generation);
7168 }
7169
7170 #[test]
7171 fn run_wave_ctx_borrows_core_until_defer_promotes() {
7172 let refs_slot: Rc<RefCell<Option<Rc<Cell<usize>>>>> = Rc::new(RefCell::new(None));
7177 let sync_refs = Rc::new(Cell::new(0usize));
7178 let deferred_refs = Rc::new(Cell::new(0usize));
7179 let deferred_ctx: Rc<RefCell<Option<crate::ctx::DeferredCtx>>> =
7180 Rc::new(RefCell::new(None));
7181
7182 let p = Node::<i32>::producer({
7183 let refs_slot = refs_slot.clone();
7184 let sync_refs = sync_refs.clone();
7185 let deferred_refs = deferred_refs.clone();
7186 let deferred_ctx = deferred_ctx.clone();
7187 move |ctx| {
7188 let refs = refs_slot
7189 .borrow()
7190 .as_ref()
7191 .expect("test installed ref counter before activation")
7192 .clone();
7193 sync_refs.set(refs.get());
7194 *deferred_ctx.borrow_mut() = Some(ctx.defer());
7195 deferred_refs.set(refs.get());
7196 ctx.emit(1i32);
7197 }
7198 });
7199 *refs_slot.borrow_mut() = Some(p.core.refs.clone());
7200
7201 let _u = p.subscribe(|_| {});
7202
7203 assert_eq!(
7204 sync_refs.get(),
7205 1,
7206 "expected only the public handle; subscribe owner execution, touched arena pin, and Ctx itself must not count"
7207 );
7208 assert_eq!(
7209 deferred_refs.get(),
7210 2,
7211 "ctx.defer() promotes the borrowed fn-body Ctx to a counted async handle"
7212 );
7213 assert_eq!(
7214 p.core.refs.get(),
7215 3,
7216 "after activation, public handle + unsubscribe handle + stashed DeferredCtx remain"
7217 );
7218 drop(deferred_ctx.borrow_mut().take());
7219 assert_eq!(
7220 p.core.refs.get(),
7221 2,
7222 "dropping the deferred async handle releases its counted Core pin"
7223 );
7224 }
7225
7226 #[test]
7227 fn run_state_slot_resets_on_deactivation_while_call_slot_persists() {
7228 let runs = Rc::new(Cell::new(0usize));
7232 let p = Node::<i32>::producer({
7233 let runs = runs.clone();
7234 move |ctx| {
7235 runs.set(runs.get() + 1);
7236 ctx.emit(runs.get() as i32);
7237 }
7238 });
7239 let handle_before = p.core.handle().expect("producer registered a call slot");
7240
7241 let u = {
7242 let u = p.subscribe(|_| {});
7243 assert_eq!(runs.get(), 1);
7244 assert!(p
7245 .core
7246 .with_node_state(|_n, _c, _cfg, r, _e| r.has_called_fn_once));
7247 u
7248 };
7249 u();
7250
7251 assert!(
7252 !p.core
7253 .with_node_state(|_n, _c, _cfg, r, _e| r.has_called_fn_once),
7254 "deactivation resets run_slots.has_called_fn_once"
7255 );
7256 assert_eq!(
7257 p.core.handle(),
7258 Some(handle_before),
7259 "deactivation keeps the call slot registered for the next lifecycle"
7260 );
7261
7262 let _u2 = p.subscribe(|_| {});
7263 assert_eq!(
7264 runs.get(),
7265 2,
7266 "run state reset re-arms the depless producer on the next activation"
7267 );
7268 assert_eq!(p.cache(), Some(2));
7269 }
7270
7271 #[test]
7272 fn config_slot_partial_gate_runs_with_sentinel_dep() {
7273 let source = Node::<i32>::state_empty();
7277 let d: Node<i32> = Node::derived_opts(
7278 vec![source.erased()],
7279 NodeOpts {
7280 partial: true,
7281 ..NodeOpts::default()
7282 },
7283 |ctx| {
7284 assert!(ctx.data::<i32>(0).is_none());
7285 ctx.emit(7i32);
7286 },
7287 );
7288 assert!(d.core.with_config(|cfg| cfg.partial));
7289
7290 let _u = d.subscribe(|_| {});
7291 d.core.try_run();
7292
7293 assert_eq!(d.cache(), Some(7));
7294 assert!(d
7295 .core
7296 .with_node_state(|_n, _c, _cfg, r, _e| r.has_called_fn_once));
7297 }
7298
7299 #[test]
7300 fn reachable_upstream_uses_arena_keys_without_refcount_churn() {
7301 let a = Node::<i32>::state(1);
7305 let b: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7306 ctx.emit(*ctx.data::<i32>(0).unwrap())
7307 });
7308 let c: Node<i32> = Node::derived(vec![b.erased()], |ctx| {
7309 ctx.emit(*ctx.data::<i32>(0).unwrap())
7310 });
7311 let before = (a.core.refs.get(), b.core.refs.get(), c.core.refs.get());
7312
7313 assert!(reachable_upstream(&c.core, &a.core));
7314 assert!(!reachable_upstream(&a.core, &c.core));
7315
7316 assert_eq!(
7317 (a.core.refs.get(), b.core.refs.get(), c.core.refs.get()),
7318 before,
7319 "reachable_upstream must not allocate extra counted Core clones for traversal"
7320 );
7321 }
7322
7323 #[test]
7324 fn reachable_upstream_stale_key_fails_closed() {
7325 let arena = GraphArena::new();
7328 let graph = arena.0.clone();
7329
7330 let victim = Node::<i32>::state_in_arena(&arena, 1);
7331 let victim_key = victim.core.key();
7332 drop(victim);
7333
7334 let reused = Node::<i32>::state_in_arena(&arena, 2);
7335 assert_eq!(
7336 victim_key.id,
7337 reused.core.key().id,
7338 "slot should be reused so stale generation is visible"
7339 );
7340 assert_ne!(
7341 victim_key.generation,
7342 reused.core.key().generation,
7343 "reused slot must advance generation"
7344 );
7345 assert!(
7346 !graph.borrow().is_live_key(victim_key),
7347 "old generation key is dead after slot reuse"
7348 );
7349
7350 let stale_source = Core::from_borrowed_parts(
7351 graph.clone(),
7352 victim_key.id,
7353 victim_key.generation,
7354 Rc::new(Cell::new(0)),
7355 );
7356 assert!(
7357 !reachable_upstream(&stale_source, &reused.core),
7358 "stale source key should never reach a live target"
7359 );
7360 assert!(
7361 !reachable_upstream(&reused.core, &stale_source),
7362 "stale target key should never be considered reachable"
7363 );
7364 assert!(
7365 !reachable_upstream(&stale_source, &stale_source),
7366 "stale source and target keys should still fail closed"
7367 );
7368 }
7369
7370 #[test]
7371 fn topology_slot_retains_inactive_declared_deps() {
7372 let arena = GraphArena::new();
7376 let source = Node::<i32>::state_in_arena(&arena, 1);
7377 let source_key = source.core.key();
7378 let refs = source.core.refs.clone();
7379 let derived: Node<i32> = Node::derived_opts_in_arena(
7380 &arena,
7381 vec![source.erased()],
7382 NodeOpts::default(),
7383 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
7384 );
7385
7386 assert_eq!(
7387 refs.get(),
7388 2,
7389 "inactive topology retains the declared dep in addition to the caller handle"
7390 );
7391 assert_eq!(derived.core.borrow().deps.len(), 1);
7392 drop(source);
7393
7394 assert!(
7395 arena.0.borrow().is_live_key(source_key),
7396 "declared topology keeps the dep live after the original handle is dropped"
7397 );
7398 assert_eq!(refs.get(), 1);
7399 assert_eq!(derived.core.deps().len(), 1);
7400
7401 let u = derived.subscribe(|_| {});
7402 assert_eq!(
7403 derived.cache(),
7404 Some(1),
7405 "activation must still subscribe the retained declared dep"
7406 );
7407 u();
7408 drop(derived);
7409 assert!(
7410 !arena.0.borrow().is_live_key(source_key),
7411 "dropping the owner topology releases the last retained dep handle"
7412 );
7413 assert_eq!(refs.get(), 0);
7414 }
7415
7416 #[test]
7417 fn activation_subscribes_retained_topology_deps() {
7418 let arena = GraphArena::new();
7421 let source = Node::<i32>::state_in_arena(&arena, 1);
7422 let refs = source.core.refs.clone();
7423 let derived: Node<i32> = Node::derived_opts_in_arena(
7424 &arena,
7425 vec![source.erased()],
7426 NodeOpts::default(),
7427 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap() + 1),
7428 );
7429 assert_eq!(refs.get(), 2);
7430
7431 let u = derived.subscribe(|_| {});
7432 assert!(
7433 refs.get() > 2,
7434 "activation should add a subscription-owned counted dep handle"
7435 );
7436 assert_eq!(derived.cache(), Some(2));
7437 u();
7438 assert_eq!(
7439 refs.get(),
7440 2,
7441 "deactivation releases only the subscription-owned dep handle"
7442 );
7443 }
7444
7445 #[test]
7446 fn borrowed_core_cannot_promote_after_external_death() {
7447 let arena = GraphArena::new();
7452 let graph = arena.0.clone();
7453 let victim = Node::<i32>::state_in_arena(&arena, 1);
7454 let victim_key = victim.core.key();
7455 let victim_refs = victim.core.refs.clone();
7456 let pin = ArenaNodePin::from_core(&victim.core).expect("live slot pins");
7457 let borrowed = pin.borrowed_core().expect("pin can create borrowed view");
7458 drop(victim);
7459
7460 assert_eq!(victim_refs.get(), 0);
7461 assert!(
7462 graph.borrow().is_live_key(victim_key),
7463 "arena pin keeps the externally-dead slot live only for the current wave"
7464 );
7465 let result = catch_unwind(AssertUnwindSafe(|| {
7466 let _revived = borrowed.clone();
7467 }));
7468 assert!(
7469 result.is_err(),
7470 "borrowed Core clone must fail closed instead of reviving refs from zero"
7471 );
7472 drop(borrowed);
7473 drop(pin);
7474 assert!(!graph.borrow().is_live_key(victim_key));
7475 let reused = Node::<i32>::state_in_arena(&arena, 2);
7476 assert_eq!(victim_key.id, reused.core.key().id);
7477 assert_ne!(victim_key.generation, reused.core.key().generation);
7478 }
7479
7480 #[test]
7481 fn boundary_owner_actions_do_not_pin_core_while_queued() {
7482 let source = Node::<i32>::state(1);
7487 let refs = source.core.refs.clone();
7488
7489 with_wave_owner(
7490 &source.core,
7491 || {
7492 let owner_refs = refs.get();
7493 source.core.request_up_next(vec![Message::Dirty], None);
7494 assert_eq!(
7495 refs.get(),
7496 owner_refs,
7497 "queued up_next stores an owner token, not a counted Core"
7498 );
7499
7500 source
7501 .core
7502 .request_rewire_next(RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(2i32))));
7503 assert_eq!(
7504 refs.get(),
7505 owner_refs,
7506 "queued rewire_next stores an owner token, not a counted Core"
7507 );
7508 },
7509 || {},
7510 );
7511
7512 assert_eq!(
7513 refs.get(),
7514 1,
7515 "boundary execution releases its temporary counted handle"
7516 );
7517 }
7518
7519 #[test]
7520 fn boundary_up_task_executes_with_borrowed_pinned_owner() {
7521 let source = Node::<i32>::state(1);
7525 let refs = source.core.refs.clone();
7526 let seen_refs = Rc::new(Cell::new(usize::MAX));
7527 let seen = seen_refs.clone();
7528 let refs_for_sink = refs.clone();
7529 let _u = source.subscribe(move |m| {
7530 if matches!(m, Message::Invalidate) {
7531 seen.set(refs_for_sink.get());
7532 }
7533 });
7534 let refs_before = refs.get();
7535
7536 with_wave_owner(
7537 &source.core,
7538 || source.core.request_up_next(vec![Message::Invalidate], None),
7539 || {},
7540 );
7541
7542 assert_eq!(
7543 seen_refs.get(),
7544 refs_before,
7545 "boundary up task should not add a counted owner Core while executing"
7546 );
7547 assert_eq!(refs.get(), refs_before);
7548 }
7549
7550 #[test]
7551 fn boundary_rewire_task_executes_with_borrowed_pinned_owner() {
7552 let a = Node::<i32>::state(1);
7556 let b = Node::<i32>::state(10);
7557 let observed_refs = Rc::new(Cell::new(usize::MAX));
7558 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7559 ctx.emit(*ctx.data::<i32>(0).unwrap())
7560 });
7561 let _u = d.subscribe(|_| {});
7562 let refs = d.core.refs.clone();
7563 let refs_before = refs.get();
7564
7565 with_wave_owner(
7566 &d.core,
7567 || {
7568 let observed_refs = observed_refs.clone();
7569 let refs = refs.clone();
7570 d.core.request_rewire_next(RewireRequest::Set(
7571 vec![b.erased()],
7572 Rc::new(move |ctx| {
7573 observed_refs.set(refs.get());
7574 ctx.emit(*ctx.data::<i32>(0).unwrap());
7575 }),
7576 ));
7577 },
7578 || {},
7579 );
7580
7581 assert_eq!(
7582 observed_refs.get(),
7583 refs_before,
7584 "boundary rewire task should run without counted owner promotions"
7585 );
7586 assert_eq!(refs.get(), refs_before);
7587 assert_eq!(d.cache(), Some(10));
7588 }
7589
7590 #[test]
7591 fn deferred_rewire_remove_does_not_pin_removed_dep_while_queued() {
7592 let dep = Node::<i32>::state(1);
7596 let derived: Node<i32> = Node::derived(vec![dep.erased()], |ctx| {
7597 ctx.emit(*ctx.data::<i32>(0).unwrap())
7598 });
7599 let _u = derived.subscribe(|_| {});
7600 let refs = dep.core.refs.clone();
7601
7602 with_wave_owner(
7603 &derived.core,
7604 || {
7605 let before = refs.get();
7606 derived
7607 .core
7608 .request_rewire_next(RewireRequest::remove(&dep.core, Rc::new(|_| {})));
7609 assert_eq!(
7610 refs.get(),
7611 before,
7612 "queued remove stores dep identity, not a counted dep Core"
7613 );
7614 },
7615 || {},
7616 );
7617
7618 assert!(
7619 derived.core.deps().is_empty(),
7620 "queued remove still applies at the committed boundary"
7621 );
7622 }
7623
7624 #[test]
7625 fn core_identity_does_not_match_reused_slot_generation() {
7626 let arena = GraphArena::new();
7629 let old = Node::<i32>::state_in_arena(&arena, 1);
7630 let old_identity = CoreIdentity::from_core(&old.core);
7631 let old_key = old.core.key();
7632 drop(old);
7633
7634 let reused = Node::<i32>::state_in_arena(&arena, 2);
7635 assert_eq!(old_key.id, reused.core.key().id);
7636 assert_ne!(old_key.generation, reused.core.key().generation);
7637 assert!(
7638 !old_identity.matches(&reused.core),
7639 "stale identity must not match a reused arena slot"
7640 );
7641 }
7642
7643 #[test]
7644 fn queued_remove_identity_noops_after_prior_remove_and_preserves_later_add() {
7645 let old = Node::<i32>::state(1);
7649 let replacement = Node::<i32>::state(2);
7650 let derived: Node<i32> = Node::derived(vec![old.erased()], |ctx| {
7651 ctx.emit(*ctx.data::<i32>(0).unwrap())
7652 });
7653 let _u = derived.subscribe(|_| {});
7654
7655 with_wave_owner(
7656 &derived.core,
7657 || {
7658 derived
7659 .core
7660 .request_rewire_next(RewireRequest::remove(&old.core, Rc::new(|_| {})));
7661 derived.core.request_rewire_next(RewireRequest::Add(
7662 replacement.erased(),
7663 Rc::new(|ctx| {
7664 if let Some(v) = ctx.data::<i32>(0) {
7665 ctx.emit(*v);
7666 }
7667 }),
7668 ));
7669 derived
7670 .core
7671 .request_rewire_next(RewireRequest::remove(&old.core, Rc::new(|_| {})));
7672 },
7673 || {},
7674 );
7675
7676 let deps = derived.core.deps();
7677 assert_eq!(deps.len(), 1);
7678 assert!(
7679 deps[0].ptr_eq(&replacement.core),
7680 "second stale remove must not disturb the replacement dep"
7681 );
7682 assert_eq!(
7683 derived.cache(),
7684 Some(2),
7685 "replacement add should install the replacement fn"
7686 );
7687
7688 replacement.set(3);
7689 assert_eq!(
7690 derived.cache(),
7691 Some(3),
7692 "stale remove must not swap in its stale fn after the replacement add"
7693 );
7694 }
7695
7696 #[test]
7697 fn stale_boundary_owner_action_does_not_target_reused_slot() {
7698 let arena = GraphArena::new();
7702 let old = Node::<i32>::state_in_arena(&arena, 1);
7703 let old_key = old.core.key();
7704 old.core.request_up_next(vec![Message::Invalidate], None);
7705 drop(old);
7706
7707 let reused = Node::<i32>::state_in_arena(&arena, 2);
7708 assert_eq!(old_key.id, reused.core.key().id);
7709 assert_ne!(old_key.generation, reused.core.key().generation);
7710
7711 drain_committed_boundary(&reused.core);
7712
7713 assert_eq!(reused.status(), Status::Settled);
7714 assert_eq!(
7715 reused.cache(),
7716 Some(2),
7717 "stale queued owner action must not invalidate the reused slot occupant"
7718 );
7719 }
7720
7721 #[test]
7722 fn nested_cross_arena_boundary_task_drains_at_outer_committed_boundary() {
7723 let arena_a = GraphArena::new();
7727 let arena_b = GraphArena::new();
7728 let outer = Node::<i32>::state_empty_in_arena(&arena_a);
7729 let inner_src = Node::<i32>::state_empty_in_arena(&arena_b);
7730 let inner_dep = inner_src.erased();
7731 let replacement_dep = inner_dep.clone();
7732 let inner: Node<i32> =
7733 Node::derived_opts_in_arena(&arena_b, vec![inner_dep.clone()], NodeOpts::default(), {
7734 let replacement_dep = replacement_dep.clone();
7735 move |ctx| {
7736 ctx.rewire_next_replace_deps(vec![replacement_dep.clone()], |next| {
7737 next.emit(*next.data::<i32>(0).unwrap() * 10)
7738 });
7739 ctx.emit(*ctx.data::<i32>(0).unwrap());
7740 }
7741 });
7742 let _inner_sub = inner.subscribe(|_| {});
7743 let inner_src_for_outer = inner_src.clone();
7744 let _outer_sub = outer.subscribe(move |m| {
7745 if matches!(m, Message::Data(_)) {
7746 inner_src_for_outer.set(1);
7747 }
7748 });
7749
7750 outer.set(1);
7751 assert_eq!(inner.cache(), Some(1));
7752
7753 inner_src.set(2);
7754 assert_eq!(
7755 inner.cache(),
7756 Some(20),
7757 "inner arena rewire_next must have drained before the next inner wave"
7758 );
7759 }
7760
7761 #[test]
7762 fn batch_boundary_root_tracking_does_not_pin_queued_owner() {
7763 let source = Node::<i32>::state(1);
7767 let refs = source.core.refs.clone();
7768
7769 crate::batch::batch(|_| {
7770 let before = refs.get();
7771 source.core.request_up_next(vec![Message::Dirty], None);
7772 assert_eq!(
7773 refs.get(),
7774 before,
7775 "batch boundary root is a weak graph root, not a counted Core"
7776 );
7777 });
7778
7779 assert_eq!(refs.get(), 1);
7780 }
7781
7782 #[test]
7783 fn batch_deferred_external_rewire_does_not_pin_owner_while_queued() {
7784 let a = Node::<i32>::state(1);
7788 let b = Node::<i32>::state(2);
7789 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7790 ctx.emit(*ctx.data::<i32>(0).unwrap())
7791 });
7792 let _u = d.subscribe(|_| {});
7793
7794 crate::batch::batch(|_| {
7795 a.set(3);
7796 let refs_after_batched_settle = d.core.refs.get();
7797 d.replace_deps(vec![b.erased()], |ctx| {
7798 ctx.emit(*ctx.data::<i32>(0).unwrap() * 10)
7799 });
7800 assert_eq!(
7801 d.core.refs.get(),
7802 refs_after_batched_settle,
7803 "queued batch-deferred external rewire must not retain a counted owner Core"
7804 );
7805 });
7806
7807 assert_eq!(
7808 d.cache(),
7809 Some(20),
7810 "batch-deferred external rewire still drains at the committed boundary"
7811 );
7812 }
7813
7814 #[test]
7815 fn batch_deferred_subscribe_dep_returns_intended_index_while_queued() {
7816 let a = Node::<i32>::state(1);
7817 let b = Node::<i32>::state(2);
7818 let c = Node::<i32>::state(3);
7819 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7820 ctx.emit(*ctx.data::<i32>(0).unwrap())
7821 });
7822 let _u = d.subscribe(|_| {});
7823
7824 crate::batch::batch(|_| {
7825 d.down(vec![Message::Data(Rc::new(5i32))]);
7826 let index = d.subscribe_dep(b.erased(), |ctx| {
7827 ctx.emit(*ctx.data::<i32>(1).unwrap() * 10)
7828 });
7829 assert_eq!(
7830 index, 1,
7831 "queued subscribe_dep returns the requested next-shape index"
7832 );
7833 let c_index = d.subscribe_dep(c.erased(), |ctx| {
7834 ctx.emit(*ctx.data::<i32>(2).unwrap() * 100)
7835 });
7836 assert_eq!(
7837 c_index, 2,
7838 "queued subscribe_dep composes with earlier queued external rewire intents"
7839 );
7840 assert_eq!(
7841 d.core.deps().len(),
7842 1,
7843 "D67 keeps the live dep shape old until batch commit"
7844 );
7845 });
7846
7847 assert_eq!(
7848 d.cache(),
7849 Some(300),
7850 "old-shape batch commit precedes fresh subscribeDep boundary waves in FIFO order"
7851 );
7852 assert_eq!(
7853 d.core.deps().len(),
7854 3,
7855 "queued subscribeDep requests compose instead of replacing one another"
7856 );
7857 }
7858
7859 #[test]
7860 fn batch_deferred_subscribe_then_unsubscribe_composes_while_queued() {
7861 let a = Node::<i32>::state(1);
7862 let b = Node::<i32>::state(2);
7863 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
7864 ctx.emit(*ctx.data::<i32>(0).unwrap())
7865 });
7866 let _u = d.subscribe(|_| {});
7867
7868 crate::batch::batch(|_| {
7869 d.down(vec![Message::Data(Rc::new(5i32))]);
7870 d.subscribe_dep(b.erased(), |ctx| {
7871 ctx.emit(*ctx.data::<i32>(1).unwrap() * 10)
7872 });
7873 d.unsubscribe_dep(b.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
7874 assert_eq!(
7875 d.core.deps().len(),
7876 1,
7877 "D67 keeps the live dep shape old until batch commit"
7878 );
7879 });
7880
7881 assert_eq!(
7882 d.core.deps().len(),
7883 1,
7884 "queued unsubscribeDep composes with an earlier queued subscribeDep"
7885 );
7886 let before_removed_dep_set = d.cache();
7887 b.set(9);
7888 assert_eq!(
7889 d.cache(),
7890 before_removed_dep_set,
7891 "removed queued dep no longer drives"
7892 );
7893 a.set(6);
7894 assert_eq!(
7895 d.cache(),
7896 Some(6),
7897 "original dep remains live after composed drain"
7898 );
7899 }
7900
7901 #[test]
7902 fn batch_boundary_task_executes_after_last_public_owner_drops() {
7903 let arena = GraphArena::new();
7906 let graph = arena.0.clone();
7907 let dep = Node::<i32>::state_in_arena(&arena, 10);
7908 let source = Node::<i32>::state_in_arena(&arena, 1);
7909 source.core.activate();
7910 let key = source.core.key();
7911 let refs = source.core.refs.clone();
7912 let seen_refs = Rc::new(Cell::new(usize::MAX));
7913
7914 crate::batch::batch({
7915 let dep = dep.erased();
7916 let graph = graph.clone();
7917 let refs = refs.clone();
7918 let seen_refs = seen_refs.clone();
7919 move |_| {
7920 let owned = source;
7921 owned.set(2);
7922 owned.replace_deps(vec![dep], {
7923 let refs = refs.clone();
7924 let seen_refs = seen_refs.clone();
7925 move |ctx| {
7926 seen_refs.set(refs.get());
7927 ctx.emit(*ctx.data::<i32>(0).expect("cached added dep data"));
7928 }
7929 });
7930 drop(owned);
7931 assert_eq!(
7932 refs.get(),
7933 0,
7934 "batch-deferred external rewire owner can drop to zero while queued"
7935 );
7936 assert!(
7937 graph.borrow().pin_count(key) >= 1,
7938 "batch target pin keeps the arena slot live during commit"
7939 );
7940 assert!(
7941 graph.borrow().is_live_key(key),
7942 "batch target pin keeps slot live during committed-boundary drain"
7943 );
7944 }
7945 });
7946
7947 assert_eq!(
7948 seen_refs.get(),
7949 0,
7950 "batch-deferred external rewire executes from borrowed pin when refs reach zero"
7951 );
7952 assert!(
7953 !graph.borrow().is_live_key(key),
7954 "batch pin should release when commit finishes"
7955 );
7956 }
7957
7958 #[test]
7959 fn defer_to_batch_does_not_increment_refs() {
7960 let source = Node::<i32>::state(1);
7961 let refs = source.core.refs.clone();
7962 let before = refs.get();
7963
7964 crate::batch::batch(|_| {
7965 source.set(2);
7966 source.set(3);
7967 source.set(4);
7968 assert_eq!(
7969 refs.get(),
7970 before,
7971 "defer_to_batch should use generation-checked batch pins, not counted Core handles"
7972 );
7973 });
7974
7975 assert_eq!(
7976 refs.get(),
7977 before,
7978 "collecting batch should not permanently retain extra refs"
7979 );
7980 assert_eq!(source.cache(), Some(4));
7981 }
7982
7983 #[test]
7984 fn dropping_last_public_handle_inside_batch_keeps_slot_live_for_commit_and_rollback() {
7985 let arena = GraphArena::new();
7986 let graph = arena.0.clone();
7987
7988 {
7989 let source = Node::<i32>::state_in_arena(&arena, 0);
7990 let refs = source.core.refs.clone();
7991 let key = source.core.key();
7992 let initial_refs = refs.get();
7993 assert_eq!(initial_refs, 1);
7994
7995 crate::batch::batch({
7996 let graph = graph.clone();
7997 let refs = refs.clone();
7998 move |_| {
7999 let owned = source;
8000 owned.set(1);
8001 drop(owned);
8002
8003 assert_eq!(
8004 refs.get(),
8005 0,
8006 "explicitly dropping the public handle should leave no counted owners"
8007 );
8008 assert!(
8009 graph.borrow().is_live_key(key),
8010 "arena pin keeps the node slot live for open batch finish"
8011 );
8012 }
8013 });
8014
8015 assert_eq!(refs.get(), 0);
8016 assert!(
8017 !graph.borrow().is_live_key(key),
8018 "batch pin should release when commit finishes"
8019 );
8020 };
8021
8022 let source = Node::<i32>::state_in_arena(&arena, 0);
8023 let refs = source.core.refs.clone();
8024 let key = source.core.key();
8025 crate::batch::batch({
8026 let graph = graph.clone();
8027 let refs = refs.clone();
8028 move |bctx| {
8029 let owned = source;
8030 owned.set(2);
8031 drop(owned);
8032
8033 assert_eq!(
8034 refs.get(),
8035 0,
8036 "explicit rollback should not restore a counted batch owner reference"
8037 );
8038 assert!(
8039 graph.borrow().is_live_key(key),
8040 "rollback also runs with a pinned slot while the batch remains open"
8041 );
8042 bctx.rollback();
8043 }
8044 });
8045
8046 assert_eq!(refs.get(), 0);
8047 assert!(
8048 !graph.borrow().is_live_key(key),
8049 "batch pin should release after rollback too"
8050 );
8051 }
8052
8053 #[test]
8054 fn batch_target_stale_generation_does_not_borrow_reused_slot() {
8055 let arena = GraphArena::new();
8056 let graph = arena.0.clone();
8057 let stale_target: BatchTarget;
8058 let old_key;
8059 {
8060 let node = Node::<i32>::state_in_arena(&arena, 1);
8061 old_key = node.core.key();
8062 stale_target =
8063 BatchTarget::from_core(&node.core).expect("pin tracks live batch target");
8064 let borrowed = stale_target
8065 .borrowed_core()
8066 .expect("pin may borrow while owner is alive");
8067 assert_eq!(
8068 borrowed
8069 .cache_any()
8070 .and_then(|value| value.downcast_ref::<i32>().copied()),
8071 Some(1)
8072 );
8073 drop(node);
8074 assert!(
8075 graph.borrow().is_live_key(old_key),
8076 "pin keeps dead owner slot alive while batch-target lives"
8077 );
8078 }
8079
8080 drop(stale_target);
8081 assert!(!graph.borrow().is_live_key(old_key));
8082
8083 let reused = Node::<i32>::state_in_arena(&arena, 2);
8084 assert_eq!(old_key.id, reused.core.key().id);
8085 assert_ne!(old_key.generation, reused.core.key().generation);
8086 assert!(
8087 graph.borrow().is_live_key(reused.core.key()),
8088 "reused slot is live with the new generation"
8089 );
8090 }
8091
8092 #[test]
8093 fn stale_external_rewire_token_does_not_target_reused_slot() {
8094 let arena = GraphArena::new();
8098 let old = Node::<i32>::state_in_arena(&arena, 1);
8099 let old_key = old.core.key();
8100 let committed = Rc::new(Cell::new(true));
8101 defer_boundary(
8102 &old.core,
8103 BoundaryTask::ExternalRewire {
8104 target: CoreToken::from_core(&old.core),
8105 req: RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(999i32))),
8106 committed,
8107 },
8108 );
8109 drop(old);
8110
8111 let reused = Node::<i32>::state_in_arena(&arena, 2);
8112 assert_eq!(old_key.id, reused.core.key().id);
8113 assert_ne!(old_key.generation, reused.core.key().generation);
8114
8115 drain_committed_boundary(&reused.core);
8116
8117 assert_eq!(reused.cache(), Some(2));
8118 assert_eq!(
8119 reused.status(),
8120 Status::Settled,
8121 "stale ExternalRewire token must not disturb the reused slot occupant"
8122 );
8123 assert!(
8124 reused.core.deps().is_empty(),
8125 "state node topology remains unchanged after stale external rewire drains"
8126 );
8127 }
8128
8129 #[test]
8130 fn boundary_task_queued_during_batch_commit_drains_before_next_wave() {
8131 let arena_a = GraphArena::new();
8135 let arena_b = GraphArena::new();
8136 let outer = Node::<i32>::state_empty_in_arena(&arena_a);
8137 let inner_src = Node::<i32>::state_empty_in_arena(&arena_b);
8138 let inner_dep = inner_src.erased();
8139 let replacement_dep = inner_dep.clone();
8140 let inner: Node<i32> =
8141 Node::derived_opts_in_arena(&arena_b, vec![inner_dep.clone()], NodeOpts::default(), {
8142 let replacement_dep = replacement_dep.clone();
8143 move |ctx| {
8144 ctx.rewire_next_replace_deps(vec![replacement_dep.clone()], |next| {
8145 next.emit(*next.data::<i32>(0).unwrap() * 10)
8146 });
8147 ctx.emit(*ctx.data::<i32>(0).unwrap());
8148 }
8149 });
8150 let _inner_sub = inner.subscribe(|_| {});
8151 let inner_src_for_commit = inner_src.clone();
8152 let _outer_sub = outer.subscribe(move |m| {
8153 if matches!(m, Message::Data(_)) {
8154 inner_src_for_commit.set(1);
8155 }
8156 });
8157
8158 crate::batch::batch(|_| {
8159 outer.set(1);
8160 });
8161 assert_eq!(inner.cache(), Some(1));
8162
8163 inner_src.set(2);
8164 assert_eq!(
8165 inner.cache(),
8166 Some(20),
8167 "inner rewire queued during batch commit must drain before the next inner wave"
8168 );
8169 }
8170
8171 #[test]
8172 fn wave_boundary_drains_later_roots_after_earlier_root_panics() {
8173 let arena_a = GraphArena::new();
8176 let arena_b = GraphArena::new();
8177 let a = Node::<i32>::state_in_arena(&arena_a, 1);
8178 let b_src = Node::<i32>::state_in_arena(&arena_b, 2);
8179 let b: Node<i32> = Node::derived_opts_in_arena(
8180 &arena_b,
8181 vec![b_src.erased()],
8182 NodeOpts::default(),
8183 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
8184 );
8185 let _u = b.subscribe(|_| {});
8186 let committed = Rc::new(Cell::new(true));
8187
8188 let result = catch_unwind(AssertUnwindSafe(|| {
8189 with_wave_owner(
8190 &a.core,
8191 || {
8192 defer_boundary(
8193 &a.core,
8194 BoundaryTask::ExternalRewire {
8195 target: CoreToken::from_core(&a.core),
8196 req: RewireRequest::Set(vec![a.erased()], Rc::new(|_| {})),
8197 committed: committed.clone(),
8198 },
8199 );
8200 defer_boundary(
8201 &b.core,
8202 BoundaryTask::ExternalRewire {
8203 target: CoreToken::from_core(&b.core),
8204 req: RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(99i32))),
8205 committed: committed.clone(),
8206 },
8207 );
8208 },
8209 || {},
8210 );
8211 }));
8212
8213 assert!(result.is_err());
8214 assert!(
8215 b.core.borrow().deps.is_empty(),
8216 "a panic in the owner graph's boundary queue must not strand later graph roots"
8217 );
8218 }
8219
8220 #[test]
8221 fn batch_boundary_drains_later_roots_after_earlier_root_panics() {
8222 let arena_a = GraphArena::new();
8225 let arena_b = GraphArena::new();
8226 let a = Node::<i32>::state_in_arena(&arena_a, 1);
8227 let b_src = Node::<i32>::state_in_arena(&arena_b, 2);
8228 let b: Node<i32> = Node::derived_opts_in_arena(
8229 &arena_b,
8230 vec![b_src.erased()],
8231 NodeOpts::default(),
8232 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
8233 );
8234 let _u = b.subscribe(|_| {});
8235 let committed = Rc::new(Cell::new(true));
8236
8237 let result = catch_unwind(AssertUnwindSafe(|| {
8238 crate::batch::batch(|_| {
8239 defer_boundary(
8240 &a.core,
8241 BoundaryTask::ExternalRewire {
8242 target: CoreToken::from_core(&a.core),
8243 req: RewireRequest::Set(vec![a.erased()], Rc::new(|_| {})),
8244 committed: committed.clone(),
8245 },
8246 );
8247 defer_boundary(
8248 &b.core,
8249 BoundaryTask::ExternalRewire {
8250 target: CoreToken::from_core(&b.core),
8251 req: RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(99i32))),
8252 committed: committed.clone(),
8253 },
8254 );
8255 });
8256 }));
8257
8258 assert!(result.is_err());
8259 assert!(
8260 b.core.borrow().deps.is_empty(),
8261 "a panic in one batch boundary root must not strand later graph roots"
8262 );
8263 }
8264
8265 #[test]
8266 fn terminal_owner_rewire_drains_topology_without_post_terminal_settle() {
8267 let a = Node::<i32>::state(1);
8272 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
8273 ctx.emit(*ctx.data::<i32>(0).unwrap())
8274 });
8275 let (log, sink) = recorder();
8276 let _u = d.subscribe(sink);
8277 log.borrow_mut().clear();
8278
8279 d.core.with_inner_edges_mut(|_n, e| {
8280 e.value.terminal = true;
8281 e.value.status = Status::Completed;
8282 e.wave.emitted_dirty_this_wave = true;
8283 e.state.dirty[0] = true;
8284 e.state.pending = 1;
8285 });
8286
8287 d.core
8288 .apply_rewire_next(RewireRequest::Set(vec![], Rc::new(|ctx| ctx.emit(999i32))));
8289
8290 assert_eq!(d.status(), Status::Completed);
8291 assert!(
8292 log.borrow().is_empty(),
8293 "terminal-owner topology drain must not emit post-terminal messages: {:?}",
8294 *log.borrow()
8295 );
8296 assert!(
8297 d.core.borrow().deps.is_empty(),
8298 "queued topology still drains on the terminal owner"
8299 );
8300 assert!(
8301 !d.core.with_inner_edges(|_, e| e.wave.in_dep_mutation),
8302 "terminal-owner early return must still clear the dep-mutation guard"
8303 );
8304 }
8305
8306 #[test]
8307 fn wave_scope_blamed_does_not_add_ref_pin() {
8308 let source = Node::<i32>::state(1);
8312 let refs = source.core.refs.clone();
8313
8314 with_wave_owner(
8315 &source.core,
8316 || {
8317 let owner_pinned = refs.get();
8318 wave_register(&source.core);
8319 let touched_pinned = refs.get();
8320 assert_eq!(touched_pinned, owner_pinned);
8321
8322 wave_set_blamed(&source.core);
8323 assert_eq!(
8324 refs.get(),
8325 touched_pinned,
8326 "blame records a generation key, not a counted Core clone"
8327 );
8328 },
8329 || {},
8330 );
8331
8332 assert_eq!(
8333 refs.get(),
8334 1,
8335 "all wave-owner/touched arena pins are released after the wave"
8336 );
8337 }
8338
8339 #[test]
8340 fn generation_keyed_blame_recovers_after_callback_drop_and_fn_panic() {
8341 let held: Rc<RefCell<Option<Node<i32>>>> =
8346 Rc::new(RefCell::new(Some(Node::<i32>::producer(|_| {}))));
8347 let source = Node::<i32>::state_empty();
8348 let derived: Node<i32> = Node::derived(vec![source.erased()], |ctx| {
8349 ctx.emit(*ctx.data::<i32>(0).unwrap());
8350 panic!("panic after subscriber callback");
8351 });
8352 let log = Rc::new(RefCell::new(Vec::<String>::new()));
8353 let h = held.clone();
8354 let l = log.clone();
8355 let _u = derived.subscribe(move |m| {
8356 l.borrow_mut().push(format!("{m:?}"));
8357 if matches!(m, Message::Data(_)) {
8358 let _ = h.borrow_mut().take();
8359 }
8360 });
8361
8362 source.set(1);
8363
8364 assert!(held.borrow().is_none());
8365 assert_eq!(derived.status(), Status::Errored);
8366 assert!(
8367 log.borrow().iter().any(|k| k == "ERROR"),
8368 "panic recovery should emit ERROR on the generation-keyed blamed node"
8369 );
8370 }
8371
8372 #[test]
8373 fn stale_blamed_generation_does_not_target_reused_slot() {
8374 let arena = GraphArena::new();
8378 let old = Node::<i32>::state_in_arena(&arena, 1);
8379 let old_key = old.core.key();
8380 let old_arena_key = old.core.arena_node_key();
8381 drop(old);
8382
8383 let reused = Node::<i32>::state_in_arena(&arena, 2);
8384 assert_eq!(old_key.id, reused.core.key().id);
8385 assert_ne!(old_key.generation, reused.core.key().generation);
8386
8387 let owner = Node::<i32>::state_in_arena(&arena, 3);
8388 with_wave_owner(
8389 &owner.core,
8390 || {
8391 wave_register(&reused.core);
8392 WAVE.with(|w| {
8393 w.borrow_mut().as_mut().expect("wave installed").blamed = Some(old_arena_key);
8394 });
8395 panic!("stale blame");
8396 },
8397 || {},
8398 );
8399
8400 assert_eq!(
8401 reused.status(),
8402 Status::Settled,
8403 "stale generation key must not ERROR/reset the reused slot occupant"
8404 );
8405 assert_eq!(
8406 owner.status(),
8407 Status::Errored,
8408 "stale blame falls back to the live wave owner"
8409 );
8410 }
8411
8412 #[test]
8413 fn blame_key_is_arena_qualified_for_nested_cross_arena_panic() {
8414 let arena_a = GraphArena::new();
8419 let arena_b = GraphArena::new();
8420 let outer = Node::<i32>::state_empty_in_arena(&arena_a);
8421 let panicker = Node::<i32>::producer_opts_in_arena(&arena_b, NodeOpts::default(), |_| {
8422 panic!("nested cross-arena panic");
8423 });
8424
8425 assert_eq!(outer.core.key(), panicker.core.key());
8426 assert_ne!(outer.core.arena_node_key(), panicker.core.arena_node_key());
8427
8428 let p = panicker.clone();
8429 let _u = outer.subscribe(move |m| {
8430 if matches!(m, Message::Data(_)) {
8431 let _ = p.subscribe(|_| {});
8432 }
8433 });
8434
8435 outer.set(1);
8436
8437 assert_eq!(
8438 outer.status(),
8439 Status::Settled,
8440 "outer same-slot node must not receive the nested arena's recovery ERROR"
8441 );
8442 assert_eq!(
8443 panicker.status(),
8444 Status::Errored,
8445 "panic recovery should blame the nested arena node that ran the fn"
8446 );
8447 }
8448
8449 #[test]
8450 #[should_panic(expected = "GraphCore generation overflow")]
8451 fn arena_generation_overflow_fails_closed() {
8452 let arena = GraphArena::new();
8454 let first = Node::<i32>::state_in_arena(&arena, 1);
8455 let slot = first.core.id.0;
8456 drop(first);
8457 arena.0.borrow_mut().generations[slot] = u64::MAX;
8458
8459 let _second = Node::<i32>::state_in_arena(&arena, 2);
8460 }
8461
8462 #[test]
8463 fn resumeall_buffers_each_recompute_and_replays_all_on_resume() {
8464 let runs = Rc::new(Cell::new(0usize));
8470 let src = Node::<i32>::state(1);
8471 let doubled = {
8472 let r = runs.clone();
8473 Node::<i32>::derived_opts(
8474 vec![src.erased()],
8475 NodeOpts {
8476 pool: PoolKind::Sync,
8477 pausable: Pausable::ResumeAll,
8478 ..NodeOpts::default()
8479 },
8480 move |ctx| {
8481 r.set(r.get() + 1);
8482 ctx.emit(*ctx.data::<i32>(0).unwrap() * 2);
8483 },
8484 )
8485 };
8486 let (log, sink) = recorder();
8487 let _u = doubled.subscribe(sink); assert_eq!(doubled.cache(), Some(2));
8489 runs.set(0);
8490
8491 let l = LockId::new("p");
8492 doubled.up(vec![Message::Pause(l.clone())]);
8493 log.borrow_mut().clear();
8494
8495 src.set(5); src.set(6); src.set(7); let data_while_paused = log.borrow().iter().filter(|k| *k == "DATA").count();
8499 assert_eq!(
8500 runs.get(),
8501 3,
8502 "resumeAll runs the fn on each dep wave (NOT coalesced to one like `true`)"
8503 );
8504 assert_eq!(
8505 data_while_paused, 0,
8506 "no DATA delivered while paused (output buffered)"
8507 );
8508 assert_eq!(doubled.cache(), Some(2), "cache not advanced while paused");
8509
8510 doubled.up(vec![Message::Resume(l)]); let data_after_resume = log.borrow().iter().filter(|k| *k == "DATA").count();
8512 assert_eq!(
8513 data_after_resume, 3,
8514 "all three buffered settles replay on final-lock RESUME (arrival order)"
8515 );
8516 assert_eq!(doubled.cache(), Some(14)); }
8518
8519 #[test]
8520 fn resumeall_buffers_dep_invalidate_until_resume() {
8521 let src = Node::<i32>::state(1);
8525 let flushes = Rc::new(Cell::new(0usize));
8526 let d = {
8527 let flushes = flushes.clone();
8528 Node::<i32>::derived_opts(
8529 vec![src.erased()],
8530 NodeOpts {
8531 pausable: Pausable::ResumeAll,
8532 ..NodeOpts::default()
8533 },
8534 move |ctx| {
8535 let f = flushes.clone();
8536 ctx.on_invalidate(move || f.set(f.get() + 1));
8537 ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
8538 },
8539 )
8540 };
8541 let (log, sink) = recorder();
8542 let _u = d.subscribe(sink);
8543 assert_eq!(d.cache(), Some(2));
8544 log.borrow_mut().clear();
8545
8546 let lock = LockId::new("resumeall-invalidate");
8547 d.up(vec![Message::Pause(lock.clone())]);
8548 src.down(vec![Message::Invalidate]);
8549
8550 assert_eq!(flushes.get(), 0, "onInvalidate waits for buffered replay");
8551 assert_eq!(
8552 d.cache(),
8553 Some(2),
8554 "cache is not cleared until RESUME replay"
8555 );
8556 assert_eq!(
8557 log.borrow().iter().filter(|k| *k == "INVALIDATE").count(),
8558 0,
8559 "INVALIDATE is not visible while resumeAll-paused"
8560 );
8561
8562 d.up(vec![Message::Resume(lock)]);
8563 assert_eq!(flushes.get(), 1);
8564 assert_eq!(d.cache(), None);
8565 assert_eq!(
8566 log.borrow().iter().filter(|k| *k == "INVALIDATE").count(),
8567 1,
8568 "buffered INVALIDATE replays once on final RESUME"
8569 );
8570 }
8571
8572 #[test]
8575 fn rewire_subscribe_dep_sentinel_dep_does_not_rearm_gate() {
8576 let a = Node::<i32>::state(1);
8579 let b = Node::<i32>::state_empty(); let runs = Rc::new(Cell::new(0usize));
8581 let d = {
8582 let r = runs.clone();
8583 Node::<i32>::derived(vec![a.erased()], move |ctx| {
8584 r.set(r.get() + 1);
8585 ctx.emit(*ctx.data::<i32>(0).unwrap() * 10);
8586 })
8587 };
8588 let (_log, sink) = recorder();
8589 let _u = d.subscribe(sink);
8590 assert_eq!(d.cache(), Some(10));
8591 runs.set(0);
8592
8593 let r2 = runs.clone();
8594 d.subscribe_dep(b.erased(), move |ctx| {
8595 r2.set(r2.get() + 1);
8596 let bv = ctx.data::<i32>(1).map(|v| *v).unwrap_or(0); ctx.emit(*ctx.data::<i32>(0).unwrap() * 10 + bv);
8598 });
8599 assert_eq!(
8600 runs.get(),
8601 0,
8602 "a SENTINEL added dep delivers START only — no recompute"
8603 );
8604
8605 a.set(2); assert_eq!(runs.get(), 1);
8607 assert_eq!(d.cache(), Some(20)); }
8609
8610 #[test]
8611 fn rewire_unsubscribe_dep_drains_and_preserves_cache() {
8612 let a = Node::<i32>::state(1);
8615 let b = Node::<i32>::state(2);
8616 let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], |ctx| {
8617 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap())
8618 });
8619 let (_log, sink) = recorder();
8620 let _u = d.subscribe(sink);
8621 assert_eq!(d.cache(), Some(3));
8622
8623 d.unsubscribe_dep(b.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
8624 assert_eq!(d.cache(), Some(3)); b.set(99); assert_eq!(d.cache(), Some(3));
8628
8629 a.set(5); assert_eq!(d.cache(), Some(5));
8631 }
8632
8633 #[test]
8634 fn rewire_unsubscribe_dep_to_zero_deps_is_inert() {
8635 let a = Node::<i32>::state(7);
8637 let runs = Rc::new(Cell::new(0usize));
8638 let d = {
8639 let r = runs.clone();
8640 Node::<i32>::derived(vec![a.erased()], move |ctx| {
8641 r.set(r.get() + 1);
8642 ctx.emit(*ctx.data::<i32>(0).unwrap());
8643 })
8644 };
8645 let (_log, sink) = recorder();
8646 let _u = d.subscribe(sink);
8647 assert_eq!(d.cache(), Some(7));
8648 runs.set(0);
8649
8650 let r2 = runs.clone();
8651 d.unsubscribe_dep(a.erased(), move |ctx| {
8652 r2.set(r2.get() + 1);
8653 ctx.emit(-1i32);
8654 });
8655 assert_eq!(d.cache(), Some(7)); assert_eq!(runs.get(), 0); a.set(8); assert_eq!(d.cache(), Some(7));
8660 assert_eq!(runs.get(), 0);
8661 }
8662
8663 #[test]
8664 fn rewire_replace_deps_reorders_kept_deps() {
8665 fn combine(ctx: &Ctx) {
8668 ctx.emit(*ctx.data::<i32>(0).unwrap() * 100 + *ctx.data::<i32>(1).unwrap());
8669 }
8670 let a = Node::<i32>::state(10);
8671 let b = Node::<i32>::state(20);
8672 let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], combine);
8673 let (_log, sink) = recorder();
8674 let _u = d.subscribe(sink);
8675 assert_eq!(d.cache(), Some(1020)); d.replace_deps(vec![b.erased(), a.erased()], combine); a.set(11); assert_eq!(d.cache(), Some(2011)); }
8681
8682 #[test]
8683 fn rewire_same_order_deps_swaps_fn_without_rebuilding_dep_state() {
8684 let a = Node::<i32>::state(1);
8687 let b = Node::<i32>::state(2);
8688 let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], |ctx| {
8689 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap())
8690 });
8691 let _u = d.subscribe(|_| {});
8692 assert_eq!(d.cache(), Some(3));
8693 assert_eq!(a.core.subscriber_count(), 1);
8694 assert_eq!(b.core.subscriber_count(), 1);
8695
8696 d.core.with_inner_edges_mut(|_, e| {
8697 e.state.prev[0] = Some(Rc::new(11i32));
8698 e.state.prev[1] = Some(Rc::new(22i32));
8699 });
8700
8701 d.replace_deps(vec![a.erased(), b.erased()], |ctx| {
8702 ctx.emit(*ctx.data::<i32>(0).unwrap() * *ctx.data::<i32>(1).unwrap())
8703 });
8704
8705 assert_eq!(a.core.subscriber_count(), 1);
8706 assert_eq!(b.core.subscriber_count(), 1);
8707 d.core.with_inner_edges(|_, e| {
8708 assert_eq!(
8709 e.state.prev[0]
8710 .as_ref()
8711 .and_then(|v| v.downcast_ref::<i32>())
8712 .copied(),
8713 Some(11),
8714 );
8715 assert_eq!(
8716 e.state.prev[1]
8717 .as_ref()
8718 .and_then(|v| v.downcast_ref::<i32>())
8719 .copied(),
8720 Some(22),
8721 );
8722 });
8723
8724 a.set(3);
8725 assert_eq!(d.cache(), Some(66)); }
8727
8728 #[test]
8729 fn rewire_same_order_fn_swap_rejects_reentrant_rewire_from_old_fn_drop() {
8730 struct ReenterOnDrop {
8731 target: Rc<RefCell<Option<Node<i32>>>>,
8732 dep: Core,
8733 saw_guard: Rc<Cell<bool>>,
8734 }
8735
8736 impl Drop for ReenterOnDrop {
8737 fn drop(&mut self) {
8738 let Some(target) = self.target.borrow().as_ref().cloned() else {
8739 return;
8740 };
8741 self.saw_guard
8742 .set(target.core.with_inner_edges(|_, e| e.wave.in_dep_mutation));
8743 target.replace_deps(vec![self.dep.clone()], |_| {});
8744 }
8745 }
8746
8747 let a = Node::<i32>::state(1);
8748 let holder: Rc<RefCell<Option<Node<i32>>>> = Rc::new(RefCell::new(None));
8749 let saw_guard = Rc::new(Cell::new(false));
8750 let drop_probe = ReenterOnDrop {
8751 target: holder.clone(),
8752 dep: a.erased(),
8753 saw_guard: saw_guard.clone(),
8754 };
8755 let d: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
8756 let _keep = &drop_probe;
8757 ctx.emit(*ctx.data::<i32>(0).unwrap());
8758 });
8759 *holder.borrow_mut() = Some(d.clone());
8760 let _u = d.subscribe(|_| {});
8761
8762 let _ = catch_unwind(AssertUnwindSafe(|| {
8763 d.replace_deps(vec![a.erased()], |ctx| {
8764 ctx.emit(*ctx.data::<i32>(0).unwrap() + 1);
8765 });
8766 }));
8767
8768 assert!(saw_guard.get(), "old fn Drop should see the rewire guard");
8769 assert!(
8770 !d.core.with_inner_edges(|_, e| e.wave.in_dep_mutation),
8771 "DepMutationGuard must clear the fast-path mutation guard after panic"
8772 );
8773 *holder.borrow_mut() = None;
8774 }
8775
8776 #[test]
8777 fn rewire_precompute_keeps_first_duplicate_old_dep_slot() {
8778 let a = Node::<i32>::state(1);
8783 let d: Node<i32> = Node::derived(vec![a.erased(), a.erased()], |ctx| {
8784 ctx.emit(*ctx.data::<i32>(0).unwrap())
8785 });
8786 let _u = d.subscribe(|_| {});
8787 assert_eq!(
8788 a.core.subscriber_count(),
8789 2,
8790 "duplicate old deps install two internal edges before rewire"
8791 );
8792
8793 d.core.with_inner_edges_mut(|_, e| {
8794 e.state.prev[0] = Some(Rc::new(11i32));
8795 e.state.prev[1] = Some(Rc::new(22i32));
8796 e.state.has_data[0] = true;
8797 e.state.has_data[1] = true;
8798 });
8799
8800 d.replace_deps(vec![a.erased()], |ctx| {
8801 ctx.emit(*ctx.data::<i32>(0).unwrap())
8802 });
8803
8804 d.core.with_inner_edges(|n, e| {
8805 assert_eq!(n.deps.len(), 1);
8806 assert_eq!(e.state.prev.len(), 1);
8807 assert_eq!(
8808 e.state.prev[0]
8809 .as_ref()
8810 .and_then(|v| v.downcast_ref::<i32>())
8811 .copied(),
8812 Some(11),
8813 "rewire should keep the first old duplicate dep slot"
8814 );
8815 assert_eq!(
8816 e.idx_boxes[0].get(),
8817 0,
8818 "kept first duplicate edge should reroute to new index 0"
8819 );
8820 });
8821 assert_eq!(
8822 a.core.subscriber_count(),
8823 1,
8824 "the duplicate old edge is drained during rewire"
8825 );
8826 }
8827
8828 #[test]
8829 fn rewire_replace_deps_all_replaced_fast_path() {
8830 let a = Node::<i32>::state(1);
8834 let b = Node::<i32>::state(2);
8835 let c = Node::<i32>::state(10);
8836 let e = Node::<i32>::state(20);
8837 let runs = Rc::new(Cell::new(0usize));
8838 let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], {
8839 let runs = runs.clone();
8840 move |ctx| {
8841 runs.set(runs.get() + 1);
8842 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
8843 }
8844 });
8845 let _u = d.subscribe(|_| {});
8846 assert_eq!(d.cache(), Some(3));
8847 runs.set(0);
8848
8849 let runs2 = runs.clone();
8850 d.replace_deps(vec![c.erased(), e.erased()], move |ctx| {
8851 runs2.set(runs2.get() + 1);
8852 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap() + 100);
8853 });
8854 assert_eq!(
8855 runs.get(),
8856 1,
8857 "one recompute after full no-kept replacement"
8858 );
8859 assert_eq!(d.cache(), Some(130));
8860
8861 assert_eq!(
8862 a.core.subscriber_count(),
8863 0,
8864 "old dep A edge is fully drained"
8865 );
8866 assert_eq!(
8867 b.core.subscriber_count(),
8868 0,
8869 "old dep B edge is fully drained"
8870 );
8871 assert_eq!(c.core.subscriber_count(), 1, "new dep C edge is active");
8872 assert_eq!(e.core.subscriber_count(), 1, "new dep E edge is active");
8873
8874 a.set(99);
8876 b.set(99);
8877 assert_eq!(runs.get(), 1);
8878 assert_eq!(d.cache(), Some(130));
8879
8880 c.set(11);
8882 assert_eq!(runs.get(), 2);
8883 assert_eq!(d.cache(), Some(131));
8884 }
8885
8886 #[test]
8887 fn rewire_no_kept_fast_path_preserves_remaining_unsubs_on_removed_cleanup_panic() {
8888 let b_deactivated = Rc::new(Cell::new(false));
8893 let a = Node::<i32>::producer(|ctx| {
8894 ctx.on_deactivation(|| panic!("removed cleanup boom"));
8895 ctx.emit(1i32);
8896 });
8897 let b = {
8898 let b_deactivated = b_deactivated.clone();
8899 Node::<i32>::producer(move |ctx| {
8900 let flag = b_deactivated.clone();
8901 ctx.on_deactivation(move || flag.set(true));
8902 ctx.emit(2i32);
8903 })
8904 };
8905 let c = Node::<i32>::state(10);
8906 let e = Node::<i32>::state(20);
8907
8908 {
8909 let d: Node<i32> = Node::derived(vec![a.erased(), b.erased()], |ctx| {
8910 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
8911 });
8912 let _u = d.subscribe(|_| {});
8913 assert_eq!(d.cache(), Some(3));
8914 assert_eq!(b.core.subscriber_count(), 1);
8915
8916 d.replace_deps(vec![c.erased(), e.erased()], |ctx| {
8917 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap());
8918 });
8919 assert_eq!(
8920 b.core.subscriber_count(),
8921 1,
8922 "later old edge remains owned by the rewiring node after the first cleanup panic"
8923 );
8924 }
8925
8926 assert!(
8927 b_deactivated.get(),
8928 "dropping the rewiring node must still run the remaining old-edge unsubscribe"
8929 );
8930 assert_eq!(
8931 b.core.subscriber_count(),
8932 0,
8933 "remaining old edge is not leaked after owner drop"
8934 );
8935 }
8936
8937 #[test]
8938 fn rewire_atomic_multi_add_settles_once() {
8939 let a = Node::<i32>::state(1);
8942 let b = Node::<i32>::state(10);
8943 let c = Node::<i32>::state(100);
8944 let runs = Rc::new(Cell::new(0usize));
8945 let saw_partial = Rc::new(Cell::new(false));
8946 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
8947 ctx.emit(*ctx.data::<i32>(0).unwrap())
8948 });
8949 let (_log, sink) = recorder();
8950 let _u = d.subscribe(sink);
8951 runs.set(0);
8952
8953 let r = runs.clone();
8954 let sp = saw_partial.clone();
8955 d.replace_deps(vec![a.erased(), b.erased(), c.erased()], move |ctx| {
8956 r.set(r.get() + 1);
8957 if ctx.data::<i32>(1).is_none() || ctx.data::<i32>(2).is_none() {
8958 sp.set(true);
8959 }
8960 ctx.emit(
8961 *ctx.data::<i32>(0).unwrap()
8962 + *ctx.data::<i32>(1).unwrap()
8963 + *ctx.data::<i32>(2).unwrap(),
8964 );
8965 });
8966 assert_eq!(
8967 runs.get(),
8968 1,
8969 "ONE atomic settle, not one fire per added dep"
8970 );
8971 assert!(
8972 !saw_partial.get(),
8973 "never fired with an added dep still SENTINEL"
8974 );
8975 assert_eq!(d.cache(), Some(111)); }
8977
8978 #[test]
8979 fn rewire_settle_emits_dirty_before_data() {
8980 let a = Node::<i32>::state(1);
8982 let b = Node::<i32>::state(100);
8983 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
8984 ctx.emit(*ctx.data::<i32>(0).unwrap())
8985 });
8986 let (log, sink) = recorder();
8987 let _u = d.subscribe(sink);
8988 log.borrow_mut().clear(); d.subscribe_dep(b.erased(), |ctx| {
8991 ctx.emit(*ctx.data::<i32>(0).unwrap() + *ctx.data::<i32>(1).unwrap())
8992 });
8993 assert_eq!(*log.borrow(), vec!["DIRTY", "DATA"]); assert_eq!(d.cache(), Some(101));
8995 }
8996
8997 #[test]
8998 #[should_panic(expected = "self-dependency")]
8999 fn rewire_rejects_self_dep() {
9000 let a = Node::<i32>::state(1);
9001 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
9002 ctx.emit(*ctx.data::<i32>(0).unwrap())
9003 });
9004 let _u = d.subscribe(|_| {});
9005 d.replace_deps(vec![d.erased()], |_| {}); }
9007
9008 #[test]
9009 #[should_panic(expected = "cycle")]
9010 fn rewire_rejects_cycle() {
9011 let a = Node::<i32>::state(1);
9012 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
9013 ctx.emit(*ctx.data::<i32>(0).unwrap())
9014 });
9015 let e: Node<i32> = Node::derived(vec![d.erased()], |ctx| {
9016 ctx.emit(*ctx.data::<i32>(0).unwrap())
9017 });
9018 let _u = e.subscribe(|_| {}); d.subscribe_dep(e.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
9020 }
9022
9023 #[test]
9024 #[should_panic(expected = "terminal dep")]
9025 fn rewire_rejects_adding_terminal_dep() {
9026 let term: Node<i32> = Node::producer(|ctx| ctx.down(vec![Message::Complete]));
9027 let _ut = term.subscribe(|_| {}); assert_eq!(term.status(), Status::Completed);
9029 let a = Node::<i32>::state(1);
9030 let d: Node<i32> = Node::derived(vec![a.erased()], |ctx| {
9031 ctx.emit(*ctx.data::<i32>(0).unwrap())
9032 });
9033 let _u = d.subscribe(|_| {});
9034 d.subscribe_dep(term.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
9035 }
9037
9038 #[test]
9039 fn rewire_midfn_becomes_error_not_panic() {
9040 let a = Node::<i32>::state(1);
9044 let x = Node::<i32>::state(9);
9045 let slot: Rc<RefCell<Option<Node<i32>>>> = Rc::new(RefCell::new(None));
9046 let slot2 = slot.clone();
9047 let d: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
9048 if let Some(dh) = slot2.borrow().as_ref() {
9049 dh.subscribe_dep(x.erased(), |c| c.emit(*c.data::<i32>(0).unwrap()));
9051 }
9052 ctx.emit(*ctx.data::<i32>(0).unwrap());
9053 });
9054 *slot.borrow_mut() = Some(d.clone());
9055 let (log, sink) = recorder();
9056 let _u = d.subscribe(sink); assert_eq!(d.status(), Status::Errored);
9058 assert!(
9059 log.borrow().iter().any(|k| k == "ERROR"),
9060 "mid-fn rewire surfaces as ERROR (got {:?})",
9061 *log.borrow()
9062 );
9063 }
9064
9065 #[test]
9066 fn rewire_fnswap_frees_old_fn_handle() {
9067 struct DropFlag(Rc<Cell<bool>>);
9070 impl Drop for DropFlag {
9071 fn drop(&mut self) {
9072 self.0.set(true);
9073 }
9074 }
9075 let old_fn_dropped = Rc::new(Cell::new(false));
9076 let a = Node::<i32>::state(1);
9077 let guard = DropFlag(old_fn_dropped.clone());
9078 let d: Node<i32> = Node::derived(vec![a.erased()], move |ctx| {
9079 let _hold = &guard; ctx.emit(*ctx.data::<i32>(0).unwrap());
9081 });
9082 let _u = d.subscribe(|_| {});
9083 assert!(!old_fn_dropped.get(), "old fn is live before the swap");
9084
9085 d.replace_deps(vec![a.erased()], |ctx| {
9086 ctx.emit(*ctx.data::<i32>(0).unwrap())
9087 }); assert!(
9089 old_fn_dropped.get(),
9090 "the rewired-away fn (and its capture) is freed on swap (B32)"
9091 );
9092 }
9093
9094 #[test]
9095 fn rewire_panic_in_added_dep_activation_does_not_wedge_node() {
9096 let a = Node::<i32>::state(1);
9110 let d: Node<i32> = Node::derived_opts(
9111 vec![a.erased()],
9112 NodeOpts {
9113 error_when_deps_error: false,
9114 ..NodeOpts::default()
9115 },
9116 |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()),
9117 );
9118 let (_log, sink) = recorder();
9119 let _u = d.subscribe(sink);
9120 assert_eq!(d.cache(), Some(1));
9121
9122 let boom = Node::<i32>::producer(|_ctx| panic!("boom on activation"));
9124 d.subscribe_dep(boom.erased(), |ctx| ctx.emit(*ctx.data::<i32>(0).unwrap()));
9125
9126 assert_eq!(
9127 boom.core.subscriber_count(),
9128 0,
9129 "failed added-dep activation rolls back the partially registered subscriber"
9130 );
9131
9132 assert_ne!(
9135 d.status(),
9136 Status::Errored,
9137 "d absorbs the dep error (errorWhenDepsError:false) and survives the rewire panic"
9138 );
9139
9140 a.set(5);
9143 assert_eq!(
9144 d.cache(),
9145 Some(5),
9146 "d recovers and recomputes — a stuck in_dep_mutation would have deferred this"
9147 );
9148
9149 d.replace_deps(vec![a.erased()], |ctx| {
9151 ctx.emit(*ctx.data::<i32>(0).unwrap() * 2)
9152 });
9153 a.set(3);
9154 assert_eq!(
9155 d.cache(),
9156 Some(6),
9157 "a later rewire works (not 'reentrant'-rejected)"
9158 );
9159 }
9160
9161 #[test]
9162 fn d109_default_v0_advances_only_on_data() {
9163 let s = Node::<i32>::state(1);
9164 assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 0 }));
9165
9166 s.down(vec![Message::Resolved]);
9167 assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 0 }));
9168
9169 s.set(2);
9170 assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 1 }));
9171
9172 s.down(vec![Message::Data(Rc::new(3)), Message::Data(Rc::new(4))]);
9173 assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 3 }));
9174
9175 s.down(vec![Message::Invalidate]);
9176 s.down(vec![Message::Complete]);
9177 assert_eq!(s.version(), Some(NodeVersion::V0 { counter: 3 }));
9178 }
9179
9180 #[test]
9181 fn d112_v1_hash_receives_strict_canonical_json_bytes() {
9182 let calls = Rc::new(RefCell::new(Vec::<String>::new()));
9183 let calls_for_hash = calls.clone();
9184 let hash = Rc::new(move |bytes: &[u8]| {
9185 let text = std::str::from_utf8(bytes)
9186 .expect("strict canonical JSON bytes are UTF-8")
9187 .to_owned();
9188 calls_for_hash.borrow_mut().push(text.clone());
9189 format!("h:{text}")
9190 });
9191 let node = Node::<serde_json::Value>::derived_opts(
9192 vec![],
9193 NodeOpts {
9194 versioning: Some(NodeVersioningPolicy::Level1 { hash: Some(hash) }),
9195 ..NodeOpts::default()
9196 },
9197 |ctx| ctx.emit(json!({ "b": 2, "a": 1 })),
9198 );
9199 let _u = node.subscribe(|_| {});
9200
9201 assert_eq!(
9202 calls.borrow().as_slice(),
9203 &[
9204 "{\"@graphrefly/node-version\":\"v1-absent\"}".to_owned(),
9205 "{\"a\":1,\"b\":2}".to_owned(),
9206 ]
9207 );
9208 assert_eq!(
9209 node.version(),
9210 Some(NodeVersion::V1 {
9211 counter: 1,
9212 cid: "h:{\"a\":1,\"b\":2}".to_owned(),
9213 prev: Some("h:{\"@graphrefly/node-version\":\"v1-absent\"}".to_owned()),
9214 })
9215 );
9216 }
9217
9218 #[test]
9219 fn d112_v1_rejects_invalid_data_before_cache_or_version_mutation() {
9220 let node = Node::<serde_json::Value>::derived_opts(
9221 vec![],
9222 NodeOpts {
9223 versioning: Some(NodeVersioningPolicy::Level1 { hash: None }),
9224 ..NodeOpts::default()
9225 },
9226 |_ctx| {},
9227 );
9228 let before = node.version();
9229 node.down(vec![Message::Data(Rc::new(f64::NAN))]);
9230
9231 assert_eq!(node.version(), before);
9232 assert_eq!(node.cache(), None);
9233 assert_eq!(node.status(), Status::Errored);
9234 }
9235
9236 #[test]
9237 fn d112_v1_hash_runs_outside_graphcore_borrow() {
9238 let graph = crate::graph::graph();
9239 let probe = graph.state_opts(1i32, crate::graph::GraphNodeOpts::named("probe"));
9240 let probe_core = probe.erased();
9241 let calls = Rc::new(Cell::new(0usize));
9242 let calls_for_hash = calls.clone();
9243 let hash = Rc::new(move |bytes: &[u8]| {
9244 let _ = probe_core.version();
9245 calls_for_hash.set(calls_for_hash.get() + 1);
9246 format!(
9247 "h:{}",
9248 std::str::from_utf8(bytes).expect("strict canonical JSON bytes are UTF-8")
9249 )
9250 });
9251 let node = graph.state_opts(
9252 json!(1),
9253 crate::graph::GraphNodeOpts {
9254 name: Some("versioned".to_owned()),
9255 node: NodeOpts {
9256 versioning: Some(NodeVersioningPolicy::Level1 { hash: Some(hash) }),
9257 ..NodeOpts::default()
9258 },
9259 ..crate::graph::GraphNodeOpts::default()
9260 },
9261 );
9262
9263 node.set(json!(2));
9264
9265 assert_eq!(calls.get(), 2);
9266 assert_eq!(
9267 node.version(),
9268 Some(NodeVersion::V1 {
9269 counter: 1,
9270 cid: "h:2".to_owned(),
9271 prev: Some("h:1".to_owned()),
9272 })
9273 );
9274 }
9275}