1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
//! Opt-in propagation scheduler — deferred, ordered, glitch-free flushes under
//! an explicit [`batch`] boundary.
//!
//! Gated behind the `scheduler` feature. When the feature is off, or when code
//! runs outside a [`batch`], propagation takes hyphae's exact synchronous
//! eager-push path — this module changes nothing about the default.
//!
//! # Model
//!
//! Outside a batch the tick context is inactive and [`Cell::notify`] runs the
//! synchronous cascade. Inside [`batch`], each `notify` instead **enqueues** a
//! deferred `(write_value, fanout)` op keyed by cell id, at the cell's
//! **height** (`1 + max(dep.height)`; sources are 0). Enqueues **coalesce**
//! last-write-wins per cell within the tick. At the closing brace the queue
//! **drains in non-decreasing height order**, settling each cell's value then
//! running its fanout (which enqueues its subscribers at strictly greater
//! heights).
//!
//! So every height-`k` cell settles before any height-`k+1` subscriber runs,
//! and a multi-input node (a diamond's join) is popped **once**, after every
//! lower input has coalesced into it — it emits once per tick with the settled
//! value instead of once per input arrival. That is the glitch-freedom: the
//! redundant re-fires a synchronous diamond makes are collapsed.
//!
//! # Scope (Phase 0)
//!
//! Coalescing is last-write-wins, which is correct for **behavior** cells
//! (`map`/`filter`/`join`/`switch_map` — latest-value semantics, exactly where
//! the glitch lives). It is **not** correct for **event** operators
//! (`scan`/`pairwise`/`buffer`/`zip`/`merge`), where dropping an intermediate
//! value changes the result; scoping the opt-in away from those is the next
//! phase. Height is memoized per tick (topology is assumed stable within a
//! tick); a persistent height cache with topology-epoch invalidation, and
//! recompute-at-pop for switch_map rewiring mid-drain, are later phases too.
//!
//! Because a batch defers value settlement to the drain, a cell read *inside*
//! the batch (before the closing brace) still sees its pre-batch value — the
//! glitch-free trade-off, and the reason this is opt-in.
use ;
use ;
use Uuid;
use crateDepNode;
thread_local!
/// Global topology epoch. Bumped on every edge change (`Cell::own`,
/// `Cell::own_keyed`, `SubscriptionGuard::drop`) so cached cell heights, tagged
/// with the epoch they were computed under, are invalidated lazily on the next
/// read. Starts at 1 so a zero-initialized `height_cache` reads as stale.
static TOPOLOGY_EPOCH: AtomicU64 = new;
/// Invalidate all cached heights by advancing the topology epoch. Cheap enough
/// to call unconditionally on any edge change.
pub
/// The current epoch, truncated to the 32 bits packed into a height cache. Wraps
/// only after ~4 billion topology changes, which cannot alias a live cache entry
/// in practice.
/// Per-thread propagation tick: the deferred frontier plus the reentrancy depth
/// and a per-tick height memo.
/// The cell's propagation height, `1 + max(dep.height)` (sources are 0), read
/// from its persistent per-node cache when the cache is current for `epoch` and
/// recomputed (and re-cached) otherwise. In a stable topology this is one atomic
/// load; `deps()` is walked only on the first read after an edge change.
/// Height DFS backing [`compute_height`]. Uses each node's [`DepNode::height_cache`]
/// as an epoch-tagged memo (so results persist across ticks and across the
/// recursion). `stack` breaks dependency cycles — a back-edge to a node already
/// on the current path contributes height 0 rather than recursing forever.
/// Whether a batch is active on the current thread. Cheap — one thread-local
/// borrow and an integer compare. This is the single check `notify` pays on the
/// synchronous hot path when the `scheduler` feature is on but no batch is open.
pub
/// Enqueue a deferred propagation op for cell `id`, computing its height from
/// `node`. Called by `notify` only when [`tick_active`] is already true.
pub
/// Drain the frontier to fixpoint in height order. Runs each op *outside* the
/// thread-local borrow, because the op's fanout re-enters [`enqueue`] for its
/// subscribers. The depth stays raised across the drain, so those re-entrant
/// notifies defer too (rather than cascading synchronously).
/// Decrements the tick depth on scope exit (including unwind), and clears the
/// tick when the outermost batch closes.
;
/// Construct cells that opt out of the scheduler's last-write-wins coalescing.
///
/// Every cell created while this scope is on the stack — via [`Cell::new`], and
/// so via operator `materialize` too — is stamped
/// [`Cell::no_coalesce`](crate::Cell::no_coalesce) at birth. Use it to exempt an
/// *event-semantic subgraph* as a unit: the stateful operator (a `scan`,
/// `pairwise`, or a hand-rolled edge-detector `map`) **and the sources feeding
/// it**, because coalescing a source upstream of the operator would drop the
/// intermediates before they ever reach it — tagging only the operator's own
/// cell would still starve it.
///
/// The stamp rides each cell for its lifetime, so it holds when the cell later
/// fires under [`batch`], regardless of where the batch is opened. Nestable;
/// composes with `batch` (you may open a batch inside or outside this scope).
///
/// Only cells *born inside* the closure are affected — a factory registered
/// here but invoked later builds its cells outside the scope and is **not**
/// stamped. Wrap the actual construction, not a deferred builder.
/// Whether a [`no_coalesce`] construction scope is active on this thread. Read
/// by `Cell::new`/`with_metrics` to stamp a cell's coalescing policy at birth.
pub
/// Run `f` as a single propagation batch.
///
/// Every `set`/`notify` inside `f` enqueues instead of cascading; the whole
/// downstream DAG then flushes once, in height order, glitch-free, at the
/// closing brace. Nested `batch` calls join the outermost tick — only the
/// outermost drains.
///
/// Outside the `scheduler` feature this is a behavior-preserving passthrough;
/// see the module docs for the deferred semantics when the feature is on.