polydat 0.2.0

Polydat — a variates construction engine
Documentation
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
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
# Cross-Fiber Cell Invalidation


The validity-tracking mechanism for `SharedCell`s
([composition_substrate.md] §S5). A producer's write
on any fiber is observable by every consumer on its
next read, with no host-side ceremony — satisfying
S5's reader contract by construction.

Implementation: `polydat/src/kernel/engines.rs`
(`SharedCellInner`, `EngineCore::check_cell_clean`,
`EngineCore::build_cell_cone`,
`EngineCore::allocate_cell_bit`).

[composition_substrate.md]: composition_substrate.md

---

## 1. Mechanism


Each cell carries a monotonic 64-bit revision counter.
Each scope holds a `Vec<Arc<AtomicU64>>` of intent-
dirty words — one word per 64 cells, appended on
demand by the allocator. Each cell holds a clone of
its scope's specific word `Arc` plus a bit position
within that word.

On write, the producer mutates the cell value under
the cell's `Mutex`, bumps the cell's revision (one
`fetch_add(Release)`), and sets the cell's bit on the
scope's word (one `fetch_or(Release)`). Three Release
stores, O(1) total, no fan-out.

On read, each consumer fiber's cone walker iterates
the precomputed cell-cone groups for the node it is
about to evaluate. Each group corresponds to one
scope-word the cone reads cells from. For each group:
load the word (one `Acquire` load), AND with the
group's interest mask; if zero, every cell in this
group is clean for this consumer (one bit per cell,
short-circuit). Otherwise, drill down by comparing
each cell's revision against the consumer fiber's
`last_seen[cell_handle]`; cells whose revision has
advanced mark the cone dirty and update `last_seen`
in preparation for re-evaluation.

Bulk-clean cost: one Acquire load + one AND per
group, typically ≤4 ops per node-eval regardless of
how many cells exist substrate-wide. Per-cell
compares are reached only when the bulk mask
indicates change.

---

## 2. Data shapes


The canonical Rust types in `polydat/src/kernel/engines.rs`:

```rust
pub struct SharedCellInner {
    pub value:               Mutex<Value>,
    pub revision:            AtomicU64,
    pub scope_intent_dirty:  Arc<AtomicU64>,  // shared with sibling cells
                                              // in the same scope-word
    pub bit:                 u8,              // 0..64 position in
                                              // scope_intent_dirty
}

pub type SharedCell = Arc<SharedCellInner>;

pub struct EngineCore {
    // ... unrelated fields ...

    pub(crate) shared_cells:      Vec<Option<SharedCell>>,
    pub(crate) output_cells:      Vec<Option<SharedCell>>,
    pub(crate) scope_intent_words: Vec<Arc<AtomicU64>>,
    pub(crate) next_cell_bit:     u32,
    pub(crate) last_seen:         HashMap<*const SharedCellInner, u64>,
    pub(crate) cell_cones:        Vec<Option<CellCone>>,
}

pub(crate) struct CellCone {
    pub(crate) groups: Vec<CellConeGroup>,
}

pub(crate) struct CellConeGroup {
    pub(crate) intent_dirty:   Arc<AtomicU64>,   // one scope-word
    pub(crate) interest_mask:  u64,              // OR of cells'
                                                  // 1<<bit in this group
    pub(crate) cells:          Vec<CellConeEntry>,
}

pub(crate) struct CellConeEntry {
    pub(crate) bit:         u8,
    pub(crate) input_slot:  usize,
}
```

`shared_cells[i]` is the cell attached to input slot
`i` on this fiber's kernel. `output_cells[j]` is the
broadcast cell for output `j` (SRD-13f Push B.2) —
descendants that bind to this output receive the same
`Arc<SharedCellInner>` via `materialize_wiring_from_outer`.

`scope_intent_words` and `next_cell_bit` together are
the scope's bit allocator. `last_seen` keys are raw
pointers (`Arc::as_ptr`) — they are compared by
identity only, never dereferenced, and stay valid for
the cell's `Arc` lifetime. `cell_cones` is the per-
node, per-fiber metadata cache; entries are built
lazily and invalidated on `attach_shared_cell`.

---

## 3. Lifecycle


### 3.1 Cell creation


`EngineCore::allocate_cell_bit` yields the next bit
position from `scope_intent_words`, growing the Vec
by one fresh `Arc<AtomicU64>` whenever the cursor
crosses a 64-bit boundary. `EngineCore::make_shared_cell`
wraps this allocator: it draws the next bit, builds
a `SharedCellInner` with the corresponding word Arc,
and returns the cell as `Arc<SharedCellInner>`.

Two call sites in `polydat/src/kernel/state.rs` /
`engines.rs` create cells:

- `seed_shared_cells` — creates one cell per `shared`
  output that has a backing input slot. Called at
  kernel construction from `PolydatKernel::from_program`.
- `seed_output_cells` — creates one cell per output
  for SRD-13f Push B.2 broadcast. Called from the
  same constructor.

Both paths route through `make_shared_cell`, so every
cell allocated by an `EngineCore` inherits that
core's `scope_intent_words`.

### 3.2 Cone metadata


Built lazily by `EngineCore::build_cell_cone` on the
first `check_cell_clean` invocation per node:

1. Read `program.input_provenance[node_idx]` — a u64
   bitmask of every input slot that flows into the
   node transitively.
2. Iterate set bits. For each input slot, look up
   `shared_cells[slot]`; if `Some(cell)`, the cell
   joins the cone.
3. Group cells by `Arc::ptr_eq` of their
   `scope_intent_dirty` word. Within each group, OR
   `1 << cell.bit` into `interest_mask` and append
   `CellConeEntry { bit, input_slot }` to `cells`.

Built cones are cached in `EngineCore::cell_cones[node_idx]`.
`EngineCore::invalidate_cell_cones` (called by
`GkState::attach_shared_cell`) clears every cached
entry to `None`, forcing rebuild on next access.

### 3.3 Per-fiber state


`EngineCore::last_seen` starts empty when the
`EngineCore` is constructed. Entries are inserted
lazily on first observation in §5's consumer
protocol. The map is per-`EngineCore`, hence
per-fiber-state — no contention.

### 3.4 Scope teardown


Dropping an `EngineCore` releases its
`scope_intent_words` Arcs and `cell_cones` cache.
Cells defined in this scope are dropped when their
`Arc` refcount hits zero (descendants that hold
clones keep them alive). Consumer `last_seen`
entries for dropped cells stay in the map but the
pointer is never observed again — harmless.

---

## 4. Producer protocol


```rust
impl SharedCellInner {
    pub fn publish(&self, value: Value) {
        {
            let mut guard = self.value.lock().unwrap();
            *guard = value;
        }
        self.revision.fetch_add(1, Ordering::Release);
        self.scope_intent_dirty
            .fetch_or(1u64 << self.bit, Ordering::Release);
    }
}
```

Every cell-write path calls `publish`:

- `GkState::set_input` on a cell-bound slot
  (`engines.rs`).
- `EngineCore::pull` on an output whose `output_cell`
  is attached (the SRD-13f Push B.2 broadcast).

Cost: one mutex acquire/release + one `fetch_add` +
one `fetch_or`. O(1). No upward propagation, no
fan-out, no consumer enumeration.

---

## 5. Consumer protocol


```rust
fn check_cell_clean(
    &mut self,
    program: &GkProgram,
    node_idx: usize,
) -> bool {
    // Lazy-build the cone metadata.
    if self.cell_cones.len() <= node_idx {
        self.cell_cones.resize_with(node_idx + 1, || None);
    }
    if self.cell_cones[node_idx].is_none() {
        let cone = self.build_cell_cone(program, node_idx);
        self.cell_cones[node_idx] = Some(cone);
    }

    // Pass 1: walk the cone, collect mismatches.
    let mut dirty: Vec<(*const SharedCellInner, u64)> = Vec::new();
    {
        let cone = self.cell_cones[node_idx].as_ref().unwrap();
        for group in &cone.groups {
            let intent = group.intent_dirty.load(Ordering::Acquire);
            let masked = intent & group.interest_mask;
            if masked == 0 { continue; }
            for entry in &group.cells {
                if masked & (1u64 << entry.bit) == 0 { continue; }
                let Some(Some(cell)) = self.shared_cells.get(entry.input_slot)
                    else { continue };
                let r = cell.revision.load(Ordering::Acquire);
                let ptr = Arc::as_ptr(cell);
                let prev = self.last_seen.get(&ptr).copied().unwrap_or(0);
                if r != prev {
                    dirty.push((ptr, r));
                }
            }
        }
    }
    // Pass 2: update last_seen for advanced cells.
    let clean = dirty.is_empty();
    for (ptr, r) in dirty {
        self.last_seen.insert(ptr, r);
    }
    clean
}
```

Invoked by `EngineCore::eval_node` ahead of the
memoization early-return: when `node_clean[node_idx]`
is `true`, the early-return fires only if
`check_cell_clean` also returns `true`. A `false`
return forces `node_clean[node_idx] := false` and
falls through to re-evaluation; the re-evaluation
re-reads the cells through `read_input → cell.value.lock()`.

Cost when clean: `|groups|` Acquire loads + ANDs
(typically ≤4). No allocation, no host-visible side
effect.

Cost when dirty: above + one Acquire load and one
HashMap lookup per cell whose intent bit is set,
plus one HashMap insert per cell whose revision
mismatched.

---

## 6. Memory ordering


| Operation | Ordering | Pairs with | Guarantee |
| --- | --- | --- | --- |
| Producer cell value write | mutex acquire/release | consumer mutex acquire | atomic value visibility |
| Producer `revision.fetch_add` | Release | consumer `revision.load(Acquire)` | revision visible |
| Producer `intent_dirty.fetch_or` | Release | consumer `intent_dirty.load(Acquire)` | dirty bit visible |
| Consumer `last_seen` access | non-atomic | n/a (per-fiber) | no contention |

The producer issues two atomic Release stores; the
consumer issues two atomic Acquire loads. Per the
C++20 / Rust memory model, any `load(Acquire)` that
observes the value of a `store(Release)`
synchronizes-with that store, establishing
happens-before from every write the producer made
before the Release to every read the consumer makes
after the Acquire. The cell's `Mutex` provides the
analogous synchronizes-with edge for the value
itself.

---

## 7. Correctness


**Claim.** For any producer write `W` on cell `C`
that completes its `intent_dirty.fetch_or(Release)`
before fiber `F`'s `check_cell_clean` issues its
`intent_dirty.load(Acquire)` on `C`'s scope-word:
either `F` re-evaluates the affected cone, or
`F.last_seen[C_ptr]` already reflects a revision ≥
`W`'s post-bump revision.

**Proof sketch.** `W`'s `intent_dirty.fetch_or`
happens-after `W`'s `revision.fetch_add` in program
order on the producer fiber. If `F`'s
`intent_dirty.load(Acquire)` observes the bit set by
`W`, the load synchronizes-with `W`'s `fetch_or`
Release, so `F` is guaranteed to also observe `W`'s
revision bump on its subsequent `revision.load(Acquire)`
— the compare `r != prev` returns true and the cone
re-evaluates. If `F`'s load does not observe the bit
set, either `F.last_seen` already reflects a
revision ≥ `W`'s (a prior check captured it; the
bit is sticky), or `F`'s next `check_cell_clean`
will observe it (memory propagation is bounded).

**Multi-consumer independence.** No consumer's
update to its own `last_seen` affects any other
consumer. Each `EngineCore` owns its own map; only
the producer-side atomic state is shared.

**Sticky-bit semantics.** Intent bits are never
cleared. A scope-word that has ever published a
write reports its bit as set for the remainder of
the scope's lifetime. The bulk-mask check therefore
forces the consumer into the per-cell drill-down,
where the revision compare correctly returns clean
for cells whose `last_seen` already matches. The
cost is bounded by the number of cells in the cone,
not the number of writes.

---

## 8. Parent-child composition


A child scope's intent words are independent of its
parent's — no propagation. The "logical composition"
of parent and child masks the substrate exposes to a
consumer is realized by §5's loop over
`cone.groups`. The lazy `build_cell_cone` walker
enumerates every scope (every distinct
`Arc<AtomicU64>` word) whose cells the cone reads —
parent, child, sibling-of-ancestor, any depth — and
emits one group per scope-word.

Equivalent to one combined bitmask check:

```text
(parent.intent_dirty & parent_interest)
    | (child.intent_dirty & child_interest)
    | (other.intent_dirty & other_interest)
    | ...   != 0
```

decomposed into a per-scope loop that early-outs
per scope and avoids contention on any single mask.

---

## 9. Bit allocation


### 9.1 Per-scope capacity


The scope holds `Vec<Arc<AtomicU64>>` — one
`AtomicU64` word per 64 cells, appended on demand by
the allocator. Each cell carries a clone of the
specific `Arc<AtomicU64>` for its word plus the
bit-within-word, so the cell needs only a single
`Arc` indirection (not a `Vec` lookup) on every
publish.

Growing the scope's word list is non-disruptive:
every cell already allocated keeps its
`Arc<AtomicU64>` reference, and a new cell that
needs a new word simply appends. There is no upper
bound on cells per scope.

Cone grouping (§5) groups cells by `Arc::ptr_eq` of
the word: cells in the same scope's same word share
one group; cells in different words (even in the
same scope) form separate groups, each with its own
bulk-mask early-out.

### 9.2 Allocation policy


First-fit, monotonic. Bits are not reused within a
scope's lifetime. Bit positions are stable for the
cell's lifetime in this scope.

### 9.3 Per-cell, not per-consumer


Bits are allocated per cell (producer-side), so the
vector bound is the scope's cell count — known at
scope construction. Per-consumer allocation would
require registration at sub-context construction, an
unbounded vector that grows with the consumer
population, and a registration-time fan-out. The
per-cell scheme avoids all three.

---

## 10. Worked example


A scenario covering cells at multiple scope levels,
two consumer fibers, and a concurrent producer.

### 10.1 Scene


```
                    Scope tree
                   ────────────
                                              ╔════════════════════════════════╗
                       Root R                 ║ Cells & their bit assignments  ║
                      ╱      ╲                ╠════════════════════════════════╣
                     ╱        ╲               ║ pop_size                       ║
                    ▼          ▼              ║   defined in R                 ║
              Child C1      Child C2          ║   word = R.intent_words[0]     ║
                  │            │              ║   bit = 0                      ║
                  ▼            ▼              ║                                ║
              Fiber FA      Fiber FB          ║ c1_counter                     ║
            (consumer)    (consumer)          ║   defined in C1                ║
                                              ║   word = C1.intent_words[0]    ║
                                              ║   bit = 0                      ║
       Cone deps per fiber                    ║                                ║
      ─────────────────────                   ║ c2_counter                     ║
                                              ║   defined in C2                ║
      FA's output node reads:                 ║   word = C2.intent_words[0]    ║
        pop_size    (slot 5 on FA)            ║   bit = 0                      ║
        c1_counter  (slot 7 on FA)            ║                                ║
                                              ║ Producer FW:                   ║
      FB's output node reads:                 ║   writes pop_size from R       ║
        pop_size    (slot 5 on FB)            ║   writes c1_counter from C1    ║
        c2_counter  (slot 7 on FB)            ║   writes c2_counter from C2    ║
                                              ╚════════════════════════════════╝
```

### 10.2 Cone metadata


`build_cell_cone` groups each fiber's cell-bound
deps by `Arc::ptr_eq` of `scope_intent_dirty`:

```
FA's CellCone for its output node:
┌─────────────────────────────────────────────────────┐
│ Group #0                                            │
│   intent_dirty: ─► R.intent_words[0]                │
│   interest_mask: 0b0000_0001  (bit 0 → pop_size)    │
│   cells: [ (bit=0, input_slot=5) ]                  │
├─────────────────────────────────────────────────────┤
│ Group #1                                            │
│   intent_dirty: ─► C1.intent_words[0]               │
│   interest_mask: 0b0000_0001  (bit 0 → c1_counter)  │
│   cells: [ (bit=0, input_slot=7) ]                  │
└─────────────────────────────────────────────────────┘

FB's CellCone for its output node:
┌─────────────────────────────────────────────────────┐
│ Group #0                                            │
│   intent_dirty: ─► R.intent_words[0]   ← SAME Arc!  │
│   interest_mask: 0b0000_0001  (bit 0 → pop_size)    │
│   cells: [ (bit=0, input_slot=5) ]                  │
├─────────────────────────────────────────────────────┤
│ Group #1                                            │
│   intent_dirty: ─► C2.intent_words[0]               │
│   interest_mask: 0b0000_0001  (bit 0 → c2_counter)  │
│   cells: [ (bit=0, input_slot=7) ]                  │
└─────────────────────────────────────────────────────┘
```

FA's Group #0 and FB's Group #0 point to the same
`Arc<AtomicU64>` — both cones share read access to
R's intent word. FA's Group #1 and FB's Group #1
point to different Arcs (C1's and C2's). This is
what makes scope isolation cheap: when C1 publishes
dirty intent on `C1.intent_words[0]`, FB's cone walk
never loads that atomic — its scope chain doesn't
include C1.

### 10.3 Timeline


Notation: cells in form `name [value | rev]`; intent
words as `word: bitmask`; `last_seen` per fiber as a
sparse map.

```
═══════════════════════════════════════════════════════════════════════════════
T0 — Initial state
═══════════════════════════════════════════════════════════════════════════════
  pop_size      [ 10 | rev=0 ]      R.intent_words[0]:  0
  c1_counter    [  0 | rev=0 ]      C1.intent_words[0]: 0
  c2_counter    [  0 | rev=0 ]      C2.intent_words[0]: 0
  FA.last_seen = {}                  node_clean[FA.output] = false
  FB.last_seen = {}                  node_clean[FB.output] = false

═══════════════════════════════════════════════════════════════════════════════
T1 — FA evaluates output (first pull)
═══════════════════════════════════════════════════════════════════════════════
  node_clean is false → no early return → normal eval
  read_input(5): pop_size.value.lock() → 10
  read_input(7): c1_counter.value.lock() → 0
  Node evaluates; buffer = f(10, 0) = (say) 10.
  node_clean[FA.output] := true

═══════════════════════════════════════════════════════════════════════════════
T2 — FW (producer fiber) writes pop_size = 20  via R
═══════════════════════════════════════════════════════════════════════════════
  cell.value.lock() → *value = 20 → mutex released
  cell.revision.fetch_add(1, Release)            → rev = 1   ◄┐ three
  cell.scope_intent_dirty.fetch_or(0b1, Release) │            ├ Release
                                                 │            │ stores,
  ─── R.intent_words[0] now = 0b1               ◄┘            │ all happens-
                                                              │ after the
                                                              │ value swap

═══════════════════════════════════════════════════════════════════════════════
T3 — FA evaluates output again (second pull)
═══════════════════════════════════════════════════════════════════════════════
  node_clean[FA.output] is true → check_cell_clean(FA.output):

    Group #0 (R.intent_words[0]):
      intent.load(Acquire) = 0b1                              ◄── synchronizes
      0b1 AND 0b1 = 0b1   → DRILL DOWN                            with T2's
        bit 0 → pop_size at slot 5:                               Release
          pop_size.revision.load(Acquire) = 1                 ◄── synchronizes
          FA.last_seen[&pop_size] = (none → treat as 0)           with T2's
          1 != 0 → DIRTY                                          revision
          FA.last_seen[&pop_size] := 1                            Release

    Group #1 (C1.intent_words[0]):
      intent.load(Acquire) = 0                                ◄── clean
      0 AND 0b1 = 0 → skip

  → returns false (dirty)
  node_clean[FA.output] := false → fall through to re-eval
  read_input(5): pop_size.value.lock() → 20                   ◄── synchronizes
  read_input(7): c1_counter.value.lock() → 0                      with T2's
  Node evaluates; buffer = f(20, 0) = 20.                          mutex release
  node_clean[FA.output] := true

═══════════════════════════════════════════════════════════════════════════════
T4 — FB evaluates output (parallel with T3, independent)
═══════════════════════════════════════════════════════════════════════════════
  check_cell_clean(FB.output):
    Group #0 (R.intent_words[0]):
      intent.load(Acquire) = 0b1 (set at T2)                  ◄── same atomic
      0b1 AND 0b1 = 0b1 → DRILL DOWN                              FA read at T3
        pop_size.revision = 1
        FB.last_seen[&pop_size] = (none → 0)                  ◄── independent
        1 != 0 → DIRTY                                            of FA's
        FB.last_seen[&pop_size] := 1                              last_seen
    Group #1 (C2.intent_words[0]):
      intent.load(Acquire) = 0 → clean
  → returns false; re-eval; FB sees pop_size=20.

═══════════════════════════════════════════════════════════════════════════════
T5 — C1 writes c1_counter = 5
═══════════════════════════════════════════════════════════════════════════════
  cell.value.lock() → *value = 5 → release
  cell.revision.fetch_add(1, Release)            → rev = 1
  cell.scope_intent_dirty.fetch_or(0b1, Release)
  ─── C1.intent_words[0] now = 0b1
  ─── R.intent_words[0] still = 0b1 (untouched; c1_counter doesn't live in R)

═══════════════════════════════════════════════════════════════════════════════
T6 — FA evaluates output again
═══════════════════════════════════════════════════════════════════════════════
  check_cell_clean(FA.output):
    Group #0 (R.intent_words[0]):
      intent.load = 0b1 (sticky from T2)
      0b1 AND 0b1 = 0b1 → DRILL DOWN
        pop_size.revision = 1
        FA.last_seen[&pop_size] = 1 → MATCH (sticky-bit false-positive
                                              caught by per-cell compare)
    Group #1 (C1.intent_words[0]):
      intent.load = 0b1
      0b1 AND 0b1 = 0b1 → DRILL DOWN
        c1_counter.revision = 1
        FA.last_seen[&c1_counter] = (none → 0)
        1 != 0 → DIRTY
        FA.last_seen[&c1_counter] := 1
  → returns false; re-eval; FA reads c1_counter=5.

═══════════════════════════════════════════════════════════════════════════════
T7 — FB evaluates output again
═══════════════════════════════════════════════════════════════════════════════
  check_cell_clean(FB.output):
    Group #0 (R.intent_words[0]):
      intent.load = 0b1 (sticky)  → drill: pop_size.rev=1, last_seen=1 → match
    Group #1 (C2.intent_words[0]):
      intent.load = 0  → 0 AND 0b1 = 0 → CLEAN (early-out, no per-cell work)
  → returns true → CLEAN, return cached value (no re-eval).
```

T2/T3/T4 show multi-fiber independence (one cell,
two consumers, each tracks its own last_seen). T5/T6
show sticky-bit semantics (R's bit remains set after
T2; the per-cell compare correctly catches it).
T5/T7 show scope isolation (C1's write at T5 has no
effect on FB's eval — FB's cone never loads
`C1.intent_words[0]` because C1 isn't in FB's scope
chain).

### 10.4 Happens-before for cell publication


The producer's three-store publish + the consumer's
two-load check form a synchronization pattern that
delivers the appearance of a single atomic
`(value, revision, intent_bit)` triple, even though
it is six separate atomic operations:

```
                Producer fiber FW                Consumer fiber FA
              ──────────────────────           ──────────────────────

  P1: mutex.lock() ─────╮
  P2: *value = V_new    │ value
  P3: mutex.unlock() ───╯ publication

  P4: revision.fetch_add(1, Release) ════╗
  P5: intent.fetch_or(bit, Release) ════╗║
                                        ║║
                                        ║║          C1: intent.load(Acquire)
                                        ║╠════════► ──synchronizes-with P5──
                                        ║              ↓ happens-before edge:
                                        ║              every store FW made
                                        ║              before P5 is visible
                                        ║              to FA after C1
                                        ║              (bulk-mask dirty;
                                        ║               drill down)
                                        ║           C2: revision.load(Acquire)
                                        ╚═════════► ──synchronizes-with P4──
                                                       ↓ happens-before edge:
                                                       every store FW made
                                                       before P4 is visible
                                                       to FA after C2

                                                    (revision mismatched;
                                                     mark node dirty;
                                                     fall through to re-eval)

                                                    C3: read_input(slot)
                                                        → mutex.lock()
                                                        ──synchronizes-with P3──
                                                          mutex acquire-release
                                                          carries the new value
                                                          across the barrier
                                                        ← V_new
```

If the consumer's bulk-mask check observes the
intent bit set (C1 sees P5's store), the consumer's
subsequent `revision.load(Acquire)` (C2) is
guaranteed to observe at least the revision P4 set,
and the subsequent `value.lock()` (C3) is guaranteed
to observe at least the value P2 wrote. If the
consumer's bulk-mask check observes the intent bit
clear (C1 sees P5's store as not-yet-published), the
consumer's cached value and `last_seen` reflect the
prior revision consistently; the next
`check_cell_clean` after P5 propagates will detect
the change.

There is no observable interleaving where the
consumer reads a torn `(value, revision, intent_bit)`
triple — i.e., sees a new intent_bit but an old
revision, or a new revision but the old value.

### 10.5 Per-event cost


| Event | Producer cost | Consumer cost (clean) | Consumer cost (dirty) |
| --- | --- | --- | --- |
| `pop_size` write (T2) | 1 mutex + 1 fetch_add + 1 fetch_or | n/a | n/a |
| FA pull, all clean | n/a | 2 Acquire loads + 2 ANDs | n/a |
| FA pull, after T2 write | n/a | 2 Acquire loads + 2 ANDs | + 1 revision load + 1 HashMap lookup + 1 insert |
| `c1_counter` write (T5) | 1 mutex + 1 fetch_add + 1 fetch_or | n/a | n/a |
| FB pull, after T5 (scope iso) | n/a | 2 Acquire loads + 2 ANDs | none (C2 bulk check is zero → early-out) |

The fast path — "every cell in every scope this
fiber cares about is at the last revision I
observed" — costs one Acquire load + one AND per
scope group, typically ≤4 ops per pull regardless of
how many cells exist substrate-wide. The dirty path
costs extra only for the cells that actually
changed, not for the rest of the cone.

Measured numbers from `polydat/benches/cell_throughput.rs`
(steady state, release build): `cell.publish`
uncontended ≈ 25 ns; `cell.snapshot` ≈ 24 ns;
clean-path pull ≈ 45 ns regardless of cells-per-
scope (1, 8, 32, 64) or cells-per-multi-word-scope
(64, 128, 256); each additional scope-group in the
cone adds ≈ 1.5 ns.

---

## 11. Out of scope


- **Volatile-node handling.** Nodes declared
  `Purity::Nondeterministic` re-evaluate every cycle
  unconditionally; that path is independent of cell
  validity tracking. Shared and volatile are
  orthogonal.
- **Cell value-read atomicity primitive.** The
  `Mutex<Value>` (or `ArcSwap<Value>`) provides
  single-value atomicity. The choice of value-
  publication primitive is orthogonal to validity
  tracking.
- **Intent-bit clearing.** Bits are sticky; the
  per-cell revision compare handles the
  consequence (§7 "Sticky-bit semantics").
- **Cross-process / distributed cells.** polydat
  is single-process; the substrate makes no claim
  beyond that boundary.

---

## 12. Bounds and invariants


- `revision: u64` — wraparound is treated as a
  non-event (would require 2⁶⁴ writes per cell).
- `scope_intent_words` grows monotonically with
  cell allocations; cells per scope are unbounded.
- `last_seen` per fiber is unbounded by type; in
  practice bounded by the distinct cell handles the
  fiber has read.
- Cell handles (`*const SharedCellInner` =
  `Arc::as_ptr`) are stable for the cell's Arc
  lifetime; a freed cell's pointer is never
  reissued (Arc allocation is unique).
- Per-write cost is O(1); per-clean-read cost is
  O(scopes_in_cone); per-dirty-read cost is
  O(dirty_cells_in_cone).
- `attach_shared_cell` to a coordinate input slot
  is a precondition violation — coordinate slots
  have an independent write path (`set_inputs`)
  that bypasses cells, producing dual writers.
  The invariant is maintained by convention (see
  composition_substrate.md §12.1 "A latent
  invariant worth naming").