cranpose-core 0.1.108

Core runtime for a Jetpack Compose inspired UI framework in Rust
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
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
1001
1002
1003
1004
1005
1006
1007
1008
1009
1010
1011
1012
1013
1014
1015
1016
1017
1018
1019
1020
1021
1022
1023
1024
1025
1026
1027
1028
1029
1030
1031
1032
1033
1034
1035
1036
1037
1038
1039
1040
1041
1042
1043
1044
1045
1046
1047
1048
1049
1050
1051
1052
1053
1054
1055
1056
1057
1058
1059
1060
1061
1062
1063
1064
1065
1066
1067
1068
1069
1070
1071
1072
1073
1074
1075
1076
1077
1078
1079
1080
1081
1082
1083
1084
1085
1086
1087
1088
1089
1090
1091
1092
1093
1094
1095
1096
1097
1098
1099
1100
1101
1102
1103
1104
1105
1106
1107
1108
1109
1110
1111
1112
1113
1114
1115
1116
1117
1118
1119
1120
1121
1122
1123
1124
1125
1126
1127
1128
1129
1130
1131
1132
1133
1134
1135
1136
1137
1138
1139
1140
1141
1142
1143
1144
1145
1146
1147
1148
1149
1150
1151
1152
1153
1154
1155
1156
1157
1158
1159
1160
1161
1162
1163
1164
1165
1166
1167
1168
1169
1170
1171
1172
1173
1174
1175
1176
1177
1178
1179
1180
1181
1182
1183
1184
1185
1186
1187
1188
1189
1190
1191
1192
1193
1194
1195
1196
1197
1198
1199
1200
1201
1202
1203
1204
1205
1206
1207
1208
1209
1210
1211
1212
1213
1214
1215
1216
1217
1218
1219
1220
1221
1222
1223
1224
1225
1226
1227
1228
1229
1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
1243
1244
1245
1246
1247
1248
1249
1250
1251
1252
1253
1254
1255
1256
1257
1258
1259
1260
1261
1262
1263
1264
1265
1266
1267
1268
1269
1270
1271
1272
1273
1274
1275
1276
1277
1278
1279
1280
1281
1282
1283
1284
1285
1286
1287
1288
1289
1290
1291
1292
1293
1294
1295
1296
1297
1298
1299
1300
1301
1302
1303
1304
1305
1306
1307
1308
1309
1310
1311
1312
1313
1314
1315
1316
1317
1318
1319
1320
1321
1322
1323
1324
1325
1326
1327
1328
1329
1330
1331
1332
1333
1334
1335
1336
1337
1338
1339
1340
1341
1342
1343
1344
1345
1346
1347
1348
1349
1350
1351
1352
1353
1354
1355
1356
1357
1358
1359
1360
1361
1362
1363
1364
1365
1366
1367
1368
1369
1370
1371
1372
1373
1374
1375
1376
1377
1378
1379
1380
1381
1382
1383
1384
1385
1386
1387
1388
1389
1390
1391
1392
1393
1394
1395
1396
1397
1398
1399
1400
1401
1402
1403
1404
1405
1406
1407
1408
1409
1410
1411
1412
1413
1414
1415
1416
1417
1418
1419
1420
1421
1422
1423
1424
1425
1426
1427
1428
1429
1430
1431
1432
1433
1434
1435
1436
1437
1438
1439
1440
1441
1442
1443
1444
1445
1446
1447
1448
1449
1450
1451
1452
1453
1454
1455
1456
1457
1458
1459
1460
1461
1462
1463
1464
1465
1466
1467
1468
1469
1470
1471
1472
1473
1474
1475
1476
1477
1478
1479
1480
1481
1482
1483
1484
1485
1486
1487
1488
1489
1490
1491
1492
1493
1494
1495
1496
1497
1498
1499
1500
1501
1502
1503
1504
1505
1506
1507
1508
1509
1510
1511
1512
1513
1514
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525
1526
1527
1528
1529
1530
1531
1532
1533
1534
1535
1536
1537
1538
1539
1540
1541
1542
1543
1544
1545
1546
1547
1548
1549
1550
1551
1552
1553
1554
1555
# Cranpose Core

The core runtime engine for Cranpose. This crate implements the fundamental algorithms for managing the composition tree, state snapshots, and change detection.

## When to Use

Use `cranpose-core` directly if you are:
-   Building a custom tree management system unrelated to UI (e.g., a reactive scene graph).
-   Implementing low-level state primitives.
-   Developing a renderer for a strictly non-standard environment where the higher-level `cranpose-ui` might be too opinionated.

## Key Concepts

-   **Slot Table**: The active slot-table runtime. It is a preorder group table backed by separate group, payload, and node storage. Active structure stays in the table; inactive retained branches are detached into explicit `DetachedSubtree` objects instead of being encoded as gaps.
-   **Composer**: The primary interface for building and updating the slot table. It coordinates scopes, retention/disposal policy, remembered values, and node lifecycle on top of the semantic slot-storage API.
-   **Snapshot System**: A multi-version concurrency control (MVCC) system for state. It allows `MutableState` to be read and written transactions, enabling atomic updates and ensuring UI consistency during concurrent operations.
-   **Recomposition**: The process of re-executing composable functions when their dependencies change. The runtime tracks dependencies at a fine-grained level (scopes) to minimize re-execution.

For the short invariant checklist the slot table must uphold, see [`docs/slot_table_invariants.md`](../../docs/slot_table_invariants.md). For the design history behind the current architecture, see [`docs/cranpose_slot_table_v2_design.md`](../../docs/cranpose_slot_table_v2_design.md) (historical). Gap-table material is historical rationale only.

## Example: Manual State Transaction

The following example shows how to perform atomic state updates using the snapshot system explicitly.

```rust,no_run
use cranpose_core::{mutableStateOf, run_in_mutable_snapshot};

fn main() {
    let count = mutableStateOf(0);

    // Any modification to state must happen within a snapshot.
    // This ensures that observers (like the UI recomposer) see a consistent view of data.
    run_in_mutable_snapshot(|| {
        let current = count.value();
        count.set(current + 1);
    });

    assert_eq!(count.value(), 1);
}
```

## Internals: Snapshots and Slot Table System

This section documents the internals of the Snapshot and Slot Table systems, which form the foundation of the composition runtime.

### Table of Contents

1. [Overview]#overview
2. [Snapshot System]#snapshot-system
3. [Slot Table System]#slot-table-system
4. [Integration Points]#integration-points
5. [Key Algorithms]#key-algorithms
6. [Design Patterns]#design-patterns

---

### Overview

The cranpose runtime is built on two fundamental subsystems:

- **Snapshot System**: Provides Multi-Version Concurrency Control (MVCC) for state isolation, conflict detection, and optimistic merging
- **Slot Table System**: Manages the composition tree structure, enabling efficient recomposition and structural preservation

These systems work together but serve distinct purposes:
- Snapshots manage **state values** (what data is visible)
- Slot tables manage **composition structure** (where data is stored in the UI tree)

---

### Snapshot System

#### Architecture Overview

The snapshot system implements a sophisticated MVCC mechanism that allows:
- Isolated views of mutable state
- Concurrent modifications without locks
- Optimistic conflict detection and merging
- Efficient garbage collection of obsolete records

#### Core Files

```text
crates/cranpose-core/src/snapshot_v2/
├── mod.rs              - Main types and coordination
├── runtime.rs          - Global runtime state
├── mutable.rs          - Mutable snapshot implementation
├── readonly.rs         - Read-only snapshot implementation
├── nested.rs           - Nested snapshot support
├── global.rs           - Global snapshot
└── transparent.rs      - Transparent observer snapshots

Supporting files:
├── state.rs                          - State objects and records
├── snapshot_id_set.rs                - Optimized bit-set for IDs
├── snapshot_pinning.rs               - Snapshot GC pinning
├── snapshot_weak_set.rs              - Weak references to state
├── snapshot_double_index_heap.rs     - Heap for pinning
└── snapshot_state_observer.rs        - State observation
```

#### Data Structures

##### SnapshotIdSet

An optimized immutable bit-set for tracking snapshot IDs with O(1) access for recent snapshots:

```rust,ignore
pub struct SnapshotIdSet {
    upper_set: u64,                    // IDs [lower_bound+64..lower_bound+127]
    lower_set: u64,                    // IDs [lower_bound..lower_bound+63]
    lower_bound: usize,                // Base offset
    below_bound: Box<[SnapshotId]>,    // Sorted array for older IDs
}
```

**Key Properties:**
- **Recent snapshots** (128 most recent): O(1) bit operations
- **Older snapshots**: O(log N) binary search
- **Immutable**: All modifications create new instances (copy-on-write)
- **Memory efficient**: Two 64-bit integers cover 128 IDs

**Operations:**
```rust,ignore
get(id)           // O(1) for recent, O(log N) for old
set(id)           // O(1) for recent, O(N) for old (copy-on-write)
or(other)         // Combine two sets
and_not(other)    // Set difference
lowest()          // Find minimum ID
```

##### StateRecord

The fundamental unit of state versioning - a linked list node containing one version of a state value:

```rust,ignore
pub struct StateRecord {
    snapshot_id: Cell<SnapshotId>,           // Which snapshot owns this
    tombstone: Cell<bool>,                   // Marked for deletion
    next: Cell<Option<Arc<StateRecord>>>,    // Chain to older records
    value: RwLock<Option<Box<dyn Any>>>,     // Type-erased value
}
```

**Record Chain Example:**
```text
SnapshotMutableState<i32>
    head → [id=10, value=100, next] → [id=8, value=50, next] → [id=5, value=0, next] → None
           ↑                          ↑                         ↑
           Latest                     Older                     Oldest
```

**Special IDs:**
- `INVALID_SNAPSHOT` (SnapshotId::MAX): Marks records available for reuse
- Valid IDs: Used to determine visibility to each snapshot

##### SnapshotMutableState&lt;T&gt;

The primary state object that applications interact with:

```rust,ignore
pub struct SnapshotMutableState<T> {
    head: RwLock<Arc<StateRecord>>,          // Head of record chain
    policy: Arc<dyn MutationPolicy<T>>,      // Equality/merge policy
    id: ObjectId,                             // Unique object ID
    weak_self: Weak<Self>,                    // Self-reference for callbacks
    apply_observers: Vec<Box<dyn Fn()>>,     // Applied change observers
}
```

**Usage:**
```rust,ignore
let state = SnapshotMutableState::new(42, StructuralEqualityPolicy::new());
let value = state.read(&snapshot);  // Read with snapshot isolation
state.write(&snapshot, 100);         // Write creates new record
```

**MutationPolicy:**
Defines how values are compared and merged:
- `StructuralEqualityPolicy`: Uses `PartialEq`
- `ReferentialEqualityPolicy`: Uses `Arc` pointer equality
- Custom policies can implement three-way merging

##### MutableSnapshot

A snapshot that can track writes and be applied to its parent:

```rust,ignore
pub struct MutableSnapshot {
    state: SnapshotState,
    base_parent_id: SnapshotId,         // Parent ID when created
    nested_count: Cell<usize>,          // Active nested snapshots
    applied: Cell<bool>,                // Applied flag
}
```

**SnapshotState** (shared between snapshot types):
```rust,ignore
pub struct SnapshotState {
    id: Cell<SnapshotId>,
    invalid: RefCell<SnapshotIdSet>,                    // Invalid snapshot IDs
    pin_handle: Cell<PinHandle>,                        // Keep alive for GC
    disposed: Cell<bool>,
    read_observer: Option<ReadObserver>,                // Track reads
    write_observer: Option<WriteObserver>,              // Track writes
    modified: RefCell<HashMap<StateObjectId, (Arc<dyn StateObject>, SnapshotId)>>,
    pending_children: RefCell<HashSet<SnapshotId>>,
}
```

**The `modified` map** tracks all state objects written to in this snapshot:
- Key: `StateObjectId` (unique object identifier)
- Value: `(Arc<StateObject>, SnapshotId)` - the object and writer snapshot ID

#### Snapshot Lifecycle

##### 1. Creation

```rust,ignore
// In runtime.rs
pub fn allocate_snapshot() -> (SnapshotId, SnapshotIdSet) {
    let id = next_snapshot_id.fetch_add(1);
    let invalid = open_snapshots.clone();
    open_snapshots.set(id);
    (id, invalid)
}
```

**Steps:**
1. Allocate new monotonically increasing ID
2. Capture current `open_snapshots` as the `invalid` set
3. Add new ID to `open_snapshots`
4. Pin the snapshot ID to prevent GC

**Why capture open_snapshots?**
Any snapshot currently open might write to state objects, so their writes should be invisible to this new snapshot until they're applied.

##### 2. Reading State

```rust,ignore
pub fn read(&self, snapshot: &dyn Snapshot) -> T {
    snapshot.record_read(self);  // Observer notification

    let head = self.head.read();
    let record = readable_record_for(
        &head,
        snapshot.id(),
        &snapshot.invalid()
    );

    // Read value from record
}
```

**Finding the readable record:**
```rust,ignore
fn readable_record_for(
    head: &Arc<StateRecord>,
    snapshot_id: SnapshotId,
    invalid: &SnapshotIdSet
) -> Arc<StateRecord> {
    let mut current = head.clone();
    let mut best: Option<Arc<StateRecord>> = None;

    loop {
        let id = current.snapshot_id.get();

        // Skip tombstones and invalid records
        if !current.tombstone.get()
            && id <= snapshot_id
            && !invalid.get(id) {

            // Keep highest valid ID ≤ snapshot_id
            if best.is_none() || id > best.as_ref().unwrap().snapshot_id.get() {
                best = Some(current.clone());
            }
        }

        match current.next.get() {
            Some(next) => current = next,
            None => break,
        }
    }

    best.expect("No readable record found")
}
```

**Key insight:** Walk the chain, skip invalid/tombstone records, return the record with the highest valid ID ≤ snapshot_id.

##### 3. Writing State

```rust,ignore
pub fn write(&self, snapshot: &dyn Snapshot, value: T) {
    snapshot.record_write(self);  // Observer + track in modified map

    let writable = self.writable_record(
        snapshot.id(),
        snapshot.reuse_limit()
    );

    *writable.value.write() = Some(Box::new(value));
}
```

**Creating/reusing writable records:**
```rust,ignore
fn writable_record(&self, snapshot_id: SnapshotId, reuse_limit: SnapshotId)
    -> Arc<StateRecord> {

    let head = self.head.read();

    // Fast path: reuse existing record with this snapshot's ID
    if head.snapshot_id.get() == snapshot_id {
        return head.clone();
    }

    // Try to reuse INVALID records below reuse_limit
    if let Some(reusable) = find_reusable_record(&head, reuse_limit) {
        reusable.snapshot_id.set(snapshot_id);
        reusable.tombstone.set(false);
        return reusable;
    }

    // Create new record and prepend to chain
    let new_record = Arc::new(StateRecord {
        snapshot_id: Cell::new(snapshot_id),
        tombstone: Cell::new(false),
        next: Cell::new(Some(head.clone())),
        value: RwLock::new(None),
    });

    *self.head.write() = new_record.clone();
    new_record
}
```

**Record reuse** is critical for performance - instead of creating infinite records, we reuse ones that are no longer visible to any snapshot.

##### 4. Applying (Merging)

The most complex operation - merging a child snapshot's changes into its parent:

```rust,ignore
pub fn apply(self) -> SnapshotApplyResult {
    // Collect all modified objects
    let modified = self.state.modified.borrow();

    for (obj_id, (state_obj, writer_id)) in modified.iter() {
        // 1. Find three records for three-way merge
        let applied = find_applied_record(state_obj, writer_id);
        let current = find_current_record(state_obj, parent_snapshot);
        let previous = find_previous_record(state_obj, base_parent_id);

        // 2. Detect conflicts
        let last_write_id = RUNTIME.last_writes.get(obj_id);

        if last_write_id != self.base_parent_id {
            // Another snapshot modified this object!

            // 3. Attempt merge
            match merge_records(previous, current, applied) {
                Some(merged) => {
                    // Merge succeeded
                    commit_merged_record(state_obj, merged);
                }
                None => {
                    // Merge failed - conflict!
                    return SnapshotApplyResult::Failure;
                }
            }
        } else {
            // No conflict - promote child's record
            promote_child_record(state_obj, applied);
        }
    }

    // 4. Update runtime state
    advance_global_snapshot();
    notify_observers();

    SnapshotApplyResult::Success
}
```

**Three-way merge visualization:**
```text
Timeline:
t0: Create snapshot S1 (base_parent_id = G0)
    previous = state.read(G0) = "A"

t1: Snapshot S1 writes "B"
    applied = "B"

t2: Snapshot S2 writes "C" and applies
    current = state.read(G1) = "C"

t3: Snapshot S1 tries to apply
    previous = "A"
    current = "C"  (≠ previous, conflict detected!)
    applied = "B"

    Merge attempt: Can we merge "A" → "C" and "A" → "B"?
```

**Merge strategies** (from MutationPolicy):
- **PromoteChild**: No conflict (current == previous), use applied
- **PromoteExisting**: Merged value equals current, use current
- **CommitMerged**: Create new merged record

##### 5. Disposal

```rust,ignore
impl Drop for MutableSnapshot {
    fn drop(&mut self) {
        if !self.applied.get() {
            self.state.dispose();
        }
    }
}

fn dispose(&self) {
    self.disposed.set(true);

    // Release pin (allow GC)
    let pin = self.pin_handle.take();
    RUNTIME.release_pin(pin);

    // Close snapshot ID
    RUNTIME.close_snapshot(self.id.get());

    // Decrement parent nested count
    if let Some(parent) = self.parent {
        parent.decrement_nested();
    }

    // Trigger on_dispose callbacks
    for callback in &self.on_dispose {
        callback();
    }
}
```

#### Garbage Collection (Record Reuse System)

**IMPORTANT**: This is NOT traditional garbage collection. Rust's `Arc` already provides automatic memory management. This system is about **record chain cleanup** and **reuse optimization**.

##### Why Needed in Rust (Not Just Copy-Paste from Kotlin)

**The Problem:**
```rust,ignore
let state = SnapshotMutableState::new(0);

// Without record reuse:
for i in 0..1000 {
    state.set(i);  // Creates new record each time
}
// Result: 1000 records in chain, even though only latest matters!
// Memory: ~64KB for records that will never be read
```

**Why Rust's Arc Doesn't Help:**
- **Arc keeps records alive**: Each record has `next: Cell<Option<Arc<StateRecord>>>`
- **Chain references prevent collection**: Head → Record1 → Record2 → Record3...
- **Arc only frees when refcount = 0**, but head always holds reference to entire chain
- **Without cleanup**: Infinite record chain growth = memory leak

**What This System Actually Does:**
1. **Identifies obsolete records**: Records older than `lowest_pinned_snapshot` can't be read
2. **Marks for reuse**: Set `snapshot_id = INVALID_SNAPSHOT` instead of dropping
3. **Reuses on next write**: `writable_record()` checks for INVALID records first
4. **Prevents chain growth**: Bounded memory regardless of write count

##### Real-World Impact

**Without record reuse** (hypothetical):
```rust,ignore
// UI counter that updates every frame (60 FPS)
let counter = SnapshotMutableState::new(0);

for frame in 0..3600 {  // 1 minute at 60 FPS
    counter.set(frame);
}

// Memory: 3600 records × 64 bytes = ~230 KB
// After 1 hour: ~13 MB just for one counter!
```

**With record reuse**:
```text
// Same scenario, but records are reused
// Memory: ~3-10 records (bounded by concurrent snapshot count)
// Memory: ~200-640 bytes regardless of time
```

##### Actual Usage in Code

The cleanup runs automatically on every global write:

```text
// state.rs:647
state.set(new_value);
  ↓
advance_global_snapshot(new_id);  // state.rs:647
  ↓
check_and_overwrite_unused_records_locked();  // global.rs:190
  ↓
EXTRA_STATE_OBJECTS.remove_if(|state| {
    state.overwrite_unused_records()  // Cleanup happens here
});
```

**Frequency:** Every write to global snapshot (most common case).

##### The Algorithm Explained

##### Kotlin vs Rust: Why Both Need This

**Kotlin (Original Compose):**
- JVM GC collects unreferenced objects automatically
- **Still needs record reuse** because record chains hold strong references
- JVM GC won't collect records still referenced by chain
- Same problem: unbounded chain growth without manual cleanup

**Rust (This Implementation):**
- `Arc` provides automatic reference counting
- **Same problem as Kotlin**: chain references prevent automatic cleanup
- `Arc` only drops when refcount = 0, but chain maintains references
- **Not a copy-paste bug**: Genuinely required for memory bounds

**Key Insight:** This isn't about memory safety (Rust guarantees that). It's about **memory efficiency**. Without this system, memory usage grows O(n) with write count instead of O(1).

##### Visual Example: Why Arc Alone Fails

```text
// Initial state
state.head -> [id=1, value=0, next=None]
             Arc::strong_count = 1

// After state.set(10)
state.head -> [id=2, value=10, next] -> [id=1, value=0, next=None]
             Arc::strong_count = 1    Arc::strong_count = 1 ← Still alive!

// After state.set(20)
state.head -> [id=3, value=20, next] -> [id=2, value=10, next] -> [id=1, value=0, next=None]
                                        ↑ Can't drop: still referenced by id=3

// After 1000 writes: Chain of 1000 records, all kept alive by next pointers
// Arc can't help because references form a chain

// WITH record reuse:
state.head -> [id=1003, value=1000, next] -> [id=INVALID, reusable] -> [id=1, value=0, next=None]
             ↑ Latest                       ↑ Marked for reuse        ↑ PREEXISTING (kept)

// Next write reuses INVALID record instead of allocating
```

##### Pinning System

**Problem:** Records can only be reused if no snapshot can read them.

**Solution:** Track the lowest snapshot ID that might read each record:

```rust,ignore
pub struct SnapshotPinning {
    pins: DoubleIndexHeap<SnapshotId>,  // Min-heap of pinned IDs
}

pub fn pin_snapshot(id: SnapshotId) -> PinHandle {
    PINNING.pins.insert(id)
}

pub fn lowest_pinned_snapshot() -> SnapshotId {
    PINNING.pins.min().unwrap_or(SnapshotId::MAX)
}
```

**Reuse limit:** Records with `id < lowest_pinned_snapshot()` are safe to reuse.

##### Record Cleanup

```rust,ignore
fn overwrite_unused_records_locked(&self, reuse_limit: SnapshotId) {
    let head = self.head.read();
    let mut records_below: Vec<Arc<StateRecord>> = Vec::new();

    // 1. Find records below reuse limit
    let mut current = head.clone();
    loop {
        let id = current.snapshot_id.get();

        if id < reuse_limit && id != INVALID_SNAPSHOT {
            records_below.push(current.clone());
        }

        match current.next.get() {
            Some(next) => current = next,
            None => break,
        }
    }

    if records_below.len() <= 1 {
        return;  // Keep at least one historical record
    }

    // 2. Keep highest record below limit (most recent history)
    records_below.sort_by_key(|r| std::cmp::Reverse(r.snapshot_id.get()));
    let keep = records_below[0].clone();

    // 3. Mark others as INVALID, copy data to reuse
    for record in &records_below[1..] {
        let keep_value = keep.value.read().clone();
        *record.value.write() = keep_value;
        record.snapshot_id.set(INVALID_SNAPSHOT);
        record.tombstone.set(false);
    }
}
```

**Key insight:** Keep one historical record for potential rollback, mark older ones as INVALID for reuse.

#### Nested Snapshots

Snapshots can be nested to create isolation boundaries:

```rust,ignore
let outer = take_mutable_snapshot();
{
    let inner = outer.take_nested_mutable_snapshot();
    // Modifications in inner are isolated from outer
    inner.apply()?;  // Merge into outer
}
outer.apply()?;  // Merge into global
```

**Nested tracking:**
- Parent tracks `nested_count` of active children
- Parent cannot apply while children are alive
- Children's `base_parent_id` points to parent's ID at creation time

---

### Slot Table System

#### Architecture Overview

The active slot-table implementation separates active structure, payload storage, node identity, and retention into distinct layers, replacing the historical gap-buffer design.

It separates the runtime into distinct storage layers:
- **Active structure** lives in a preorder `Vec<GroupRecord>`.
- **Remembered payloads** live in a separate payload table.
- **Node identities** live in a separate node table.
- **Inactive preserved branches** live outside the active table as explicit `DetachedSubtree` values.

That gives the runtime much simpler semantics:
- There is no semantic `Gap` inside the active table.
- Retention is explicit detach/restore, not preserved free space.
- Scopes are resolved through an index, not by scanning all groups.
- All structural mutation goes through the writer session state.

The invariants it must uphold are tracked in `docs/slot_table_invariants.md`; the design history behind this architecture is `docs/cranpose_slot_table_v2_design.md` (historical).

#### Core Files

```text
crates/cranpose-core/src/
├── retention.rs                   - Detached-subtree retention bookkeeping
└── slot/
    ├── types.rs                   - Semantic handles, cursors, and operation result types
    ├── table.rs                   - SlotTable and write-session entry points
    ├── writer.rs                  - begin/finish/end/skip group traversal
    ├── table/                     - SlotTable metadata, mutation, and value helpers
    ├── writer/                    - Writer state-machine helper modules
    ├── groups.rs                  - GroupRecord helpers and child traversal
    ├── payload.rs                 - Payload storage and value-slot records
    ├── payload_anchors.rs         - Stable value-slot identity registry
    ├── nodes.rs                   - Node record storage and subtree extraction
    ├── anchors.rs                 - AnchorRegistry and anchor state tracking
    ├── scope_index.rs             - ScopeId -> active group lookup
    ├── detach.rs                  - DetachedSubtree extraction and restore
    ├── validate/                  - Structural invariant checking
    ├── debug.rs / reader.rs       - Debug snapshots and textual dumps
    └── lifecycle.rs               - Deferred payload disposal
```

#### Data Structures

##### SlotTable

The active table stores groups, payloads, nodes, stable identity registries, and scope lookup separately:

```rust,ignore
pub struct SlotTable {
    storage_id: usize,
    groups: Vec<GroupRecord>,
    payloads: Vec<PayloadRecord>,
    nodes: Vec<NodeRecord>,
    anchors: AnchorRegistry,
    payload_anchors: PayloadAnchorRegistry,
    scope_index: ScopeIndex,
    mutation_debug_stats: SlotTableMutationDebugStats,
    next_group_generation: u32,
}
```

**Key properties:**
- **Exact active structure**: `subtree_len` always means the active preorder span.
- **Stable addressing**: groups use stable `AnchorId` identities plus transient `ActiveGroupId` handles; values use anchor-based `ValueSlotId`.
- **Indexed scopes**: active scopes map directly to group anchors.
- **Explicit retention**: removed branches are detached from the table before any retain/dispose decision happens.

##### GroupRecord

Each group describes one active composable call:

```rust,ignore
pub struct GroupRecord {
    key: GroupKey,
    parent_anchor: AnchorId,
    depth: u32,
    subtree_len: u32,
    payload_start: u32,
    payload_len: u32,
    node_start: u32,
    node_len: u32,
    subtree_node_count: u32,
    generation: u32,
    anchor: AnchorId,
    scope_id: Option<ScopeId>,
}
```

`parent_anchor` stays stable even when active indexes shift. `subtree_len` and
`subtree_node_count` are validated against the actual preorder tree.

##### PayloadRecord and NodeRecord

Remembered values and emitted nodes are stored outside the structural group table:

```rust,ignore
pub struct PayloadRecord {
    owner: AnchorId,
    anchor: PayloadAnchor,
    type_id: TypeId,
    type_name: &'static str,
    kind: PayloadKind,
    value: Box<dyn Any>,
}

pub struct NodeRecord {
    owner: AnchorId,
    id: NodeId,
    parent_id: Option<NodeId>,
    generation: u32,
    lifecycle: NodeLifecycle,
}
```

Payload anchors back `ValueSlotId`, and `ValueSlotId` also stores the owning
slot-table storage id. Remembered values remain addressable when sibling
reordering moves the owning group in the active table, while stale cross-table
handles fail instead of aliasing another table.

##### DetachedSubtree

When a branch leaves the active table, storage returns an owned subtree:

```rust,ignore
pub struct DetachedSubtree {
    root_key: GroupKey,
    root_scope_id: Option<ScopeId>,
    groups: Vec<GroupRecord>,
    payloads: Vec<PayloadRecord>,
    nodes: Vec<NodeRecord>,
}
```

Detached subtrees carry remembered payloads, anchors, scope IDs, and node identities together.
The slot table itself does not decide whether they are retained or disposed.

#### Core Operations

##### begin_group() - Begin Group

Writers match groups only among siblings of the current parent:

```rust,ignore
pub fn begin_group(&mut self, input: BeginGroupInput<DetachedSubtree>) -> GroupStart<ActiveGroupId> {
    if let Some(restored) = input.restored {
        return restore_started_group(input.key, restored);
    }

    if expected_sibling_matches(parent, input.key) {
        return GroupStartKind::Reused;
    }

    if let Some(found) = find_later_sibling(parent, input.key) {
        move_subtree(found, insert_index);
        return GroupStartKind::Moved;
    }

    insert_new_group(insert_index, parent, input.key);
    GroupStartKind::Inserted
}
```

There is no fixed global search budget and no recursive rescue scan into grandchildren. Large
sibling ranges build a temporary sibling index inside the active writer frame.

##### finish_group_body() / end_group()

At the end of a group body, the writer trims payloads and direct nodes that were not visited,
detaches unvisited child subtrees, and returns them for retain-or-dispose handling:

```rust,ignore
let finish = slots.finish_group_body();
composer.handle_detached_children(parent_scope, finish.detached_children);
slots.end_group();
```

This is where inactive branches become `DetachedSubtree` values. They are no longer present in
the active table after `finish_group_body`.

##### value_slot() / remember()

Remembered state is stored in the payload table and addressed by `ValueSlotId`:

```rust,ignore
let slot = slots.value_slot(|| SnapshotMutableState::new(0, policy));
let state = slots.read_value::<SnapshotMutableState<i32>>(slot);
```

If a payload slot is revisited with a different type, the old boxed value is dropped through the
slot lifecycle coordinator and the new typed payload replaces it in place.
Public composer-held value access uses typed value-slot handles; the old untyped
composer write surface is not part of the active API.

##### begin_recompose_at_scope()

Targeted recomposition starts from the indexed scope mapping:

```rust,ignore
if let Some(group) = slots.begin_recompose_at_scope(scope_id) {
    // group handle resolves through scope_anchor_to_group -> anchors -> groups
}
```

Detached scopes are intentionally absent from that index. They stay inactive until their retained
subtree is explicitly restored.

#### Active-Table Invariants

The slot table validates a small set of invariants after mutations in debug/test builds:
- active groups form one valid preorder forest;
- every `subtree_len` and `subtree_node_count` matches the actual active subtree;
- payload and node ranges are contiguous and owner-correct;
- every active scope index entry resolves to the correct group anchor;
- retained subtree anchors are detached or invalidated, never active.
- debug stats report occupied invalidated anchor slots separately from reusable
  free anchor IDs for both group and payload anchor registries.

The active debug helpers are `SlotTable::validate()`, `SlotTable::debug_snapshot()`,
`SlotTable::debug_dump_groups()`, and `SlotTable::debug_dump_slot_entries()`.

---

### Integration Points

The snapshot and slot table systems integrate at several key points:

#### 1. State Storage in Slots

State objects are stored in slot-table payload records and accessed through composer helpers:

```rust,ignore
composer.with_group(key, |composer| {
    let state = composer.remember(|| {
        SnapshotMutableState::new(0, policy)
    });
    let value = state.value();
});
```

**Key insight:** Slot table manages **where** state is stored, snapshots manage **what** values are visible.

#### 2. Scope-Based Recomposition

Scopes are attached to groups during composition and later resolved through the active scope index:

```rust,ignore
let started = slots.begin_group(BeginGroupInput::new(group_key, restored));
slots.set_group_scope(started.group, scope_id);

// Later:
slots.begin_recompose_at_scope(scope_id);
```

**Snapshot integration:**
```rust,ignore
// Snapshot observer tracks which scopes read which state
let observer = SnapshotStateObserver::new(|scope_id| {
    invalidate_scope(scope_id);
});

snapshot.set_read_observer(Box::new(move |state_obj| {
    observer.observe_read(current_scope, state_obj);
}));
```

**Flow:**
1. Composition reads state → observer records `(scope, state_obj)` mapping
2. State changes → observer invalidates affected scopes
3. Active scope index resolves the group anchor → recompose at that group

Detached scopes are intentionally absent from the active scope index. When a retained subtree is
restored, its scope is reactivated and can re-enter normal recomposition.

#### 3. Invalidation Tracking

```rust,ignore
pub struct SnapshotStateObserver {
    observations: HashMap<ScopeId, HashSet<StateObjectId>>,
    reverse: HashMap<StateObjectId, HashSet<ScopeId>>,
}

impl SnapshotStateObserver {
    pub fn observe_read(&mut self, scope: ScopeId, obj: StateObjectId) {
        self.observations.entry(scope).or_default().insert(obj);
        self.reverse.entry(obj).or_default().insert(scope);
    }

    pub fn notify_changed(&mut self, obj: StateObjectId) -> Vec<ScopeId> {
        self.reverse.get(&obj).cloned().unwrap_or_default().collect()
    }
}
```

**Recomposition flow:**
```rust,ignore
// 1. Apply snapshot changes
snapshot.apply()?;

// 2. Get invalidated scopes
let invalid_scopes = observer.notify_changed_objects(&changed_objects);

// 3. Recompose each scope
for scope in invalid_scopes {
    composer.recompose_group(scope);
}
```

#### 4. Composition Context

The composition runtime coordinates slot passes, command application, and retention policy:

```rust,ignore
composition.render(root_key, || ui());
// inside:
// - SlotsHost begins a compose/recompose pass
// - SlotWriteSession mutates the active SlotTable
// - detached children become DetachedSubtree values
// - composer/host decide retain vs dispose
// - command queue applies node attach/move/remove work to the applier
```

---

### Key Algorithms

#### Three-Way Merge Algorithm

Used when applying snapshots with concurrent modifications:

```rust,ignore
pub fn three_way_merge<T>(
    previous: &T,
    current: &T,
    applied: &T,
    policy: &dyn MutationPolicy<T>
) -> MergeResult<T> {
    // 1. No conflict case
    if policy.equivalent(current, previous) {
        return MergeResult::PromoteChild;
    }

    // 2. Identical modification case
    if policy.equivalent(applied, current) {
        return MergeResult::PromoteExisting;
    }

    // 3. Attempt custom merge
    if let Some(merged) = policy.merge(previous, current, applied) {
        // Check if merge equals current (no-op)
        if policy.equivalent(&merged, current) {
            return MergeResult::PromoteExisting;
        }
        return MergeResult::CommitMerged(merged);
    }

    // 4. Conflict
    MergeResult::Conflict
}
```

**Example merges:**

**Structural equality (integers):**
```text
previous: 10
current:  15  (another snapshot wrote this)
applied:  20  (our snapshot wrote this)

merge:    Cannot merge conflicting integers → Conflict
```

**Set merge (additive):**
```text
previous: {A, B}
current:  {A, B, C}  (another snapshot added C)
applied:  {A, B, D}  (our snapshot added D)

merge:    {A, B, C, D}  (union) → CommitMerged
```

**List merge (operational transform):**
```text
previous: ["a", "b", "c"]
current:  ["a", "x", "b", "c"]  (inserted "x" at 1)
applied:  ["a", "b", "c", "y"]  (appended "y")

merge:    ["a", "x", "b", "c", "y"]  (apply both ops) → CommitMerged
```

#### Sibling Matching and Movement

The writer only matches direct siblings under the current parent:

```rust,ignore
pub fn begin_group(&mut self, input: BeginGroupInput<DetachedSubtree>) -> GroupStart<ActiveGroupId> {
    if let Some(restored) = input.restored {
        return restore_started_group(input.key, restored);
    }

    if expected_sibling_matches(parent, input.key) {
        return GroupStartKind::Reused;
    }

    if let Some(found) = find_later_sibling(parent, input.key) {
        move_subtree(found, insert_index);
        return GroupStartKind::Moved;
    }

    insert_new_group(insert_index, parent, input.key);
    GroupStartKind::Inserted
}
```

**Why it matters:**
- the search is parent-bounded;
- grandchildren are never treated as sibling matches;
- retention is not mixed into sibling search;
- the semantics depend on exact keys, not rescue budgets.

#### Detach and Restore

Conditional structure changes become explicit subtree extraction and reinsertion:

```rust,ignore
pub fn detach_subtree(&mut self, anchor: AnchorId) -> DetachedSubtree {
    let groups = drain_group_range(root_index, subtree_len);
    let payloads = detach_payloads_for_groups(root_index, &mut groups);
    let nodes = detach_nodes_for_groups(root_index, &mut groups);
    clear_group_indexes(&groups);
    clear_scope_index_for_groups(&groups);
    DetachedSubtree { groups, payloads, nodes, .. }
}

pub fn restore_subtree(&mut self, insert_index: usize, subtree: DetachedSubtree) -> AnchorId {
    restore_payloads_for_groups(insert_index, &mut groups, subtree.payloads);
    restore_nodes_for_groups(insert_index, &mut groups, subtree.nodes);
    groups.splice(insert_index..insert_index, subtree.groups);
    recompute_all_metadata();
}
```

**Why it matters:**
- removed structure is no longer present in the active table;
- retained state is explicit and owner-controlled;
- restoring a retained subtree preserves remembered payloads, scopes, and node identities.

---

### Design Patterns

#### Persistent/Immutable Data Structures (NOT True CoW)

**Used in:** SnapshotIdSet

**IMPORTANT CLARIFICATION:** The documentation claims "CoW" but this is **NOT true copy-on-write**. It's actually a **persistent/immutable data structure** with partial optimization.

**Actual Implementation:**
```rust,ignore
pub struct SnapshotIdSet {
    upper_set: u64,                           // Bit set (cheap to copy)
    lower_set: u64,                           // Bit set (cheap to copy)
    lower_bound: usize,                       // Cheap to copy
    below_bound: Option<Box<[SnapshotId]>>,  // FULL COPY on clone!
}

impl SnapshotIdSet {
    pub fn set(&self, id: SnapshotId) -> Self {
        // Returns NEW instance
        Self {
            upper_set: self.upper_set,
            lower_set: self.lower_set | mask,
            below_bound: self.below_bound.clone(),  // ⚠️ Box::clone() = full array copy!
        }
    }
}
```

**Usage Pattern (3 clones per snapshot!):**
```rust,ignore
// global.rs:141-143
let mut parent_invalid = self.state.invalid.borrow().clone();  // Clone 1
parent_invalid = parent_invalid.set(new_id);                   // Clone 2
self.state.invalid.replace(parent_invalid.clone());            // Clone 3
```

**What's Actually Happening:**
- **Immutable**: Can't modify in place (functional correctness)
-**Fast for recent IDs**: Only bit operations, no allocation
-**NOT true CoW**: `Box::clone()` does full array copy
-**Suboptimal**: Could use `Arc<[SnapshotId]>` for O(1) sharing

**True CoW Would Be:**
```rust,ignore
below_bound: Option<Arc<[SnapshotId]>>,  // Share via Arc
// .clone() would just bump refcount, no array copy
```

**Real-World Impact:**
- **Best case** (all recent IDs): 24 bytes copied, very fast ✅
- **Worst case** (100 old IDs): ~2.4 KB copied per snapshot creation ⚠️
- **Not dead code**: Works correctly, just not optimally

**Why It Works Despite Not Being True CoW:**
- Most snapshot IDs are recent (fit in bit sets)
- `below_bound` array is typically small or empty
- Correctness > performance (for now)

#### Object Pool

**Used in:** StateRecord reuse

**Pattern:**
```rust,ignore
// Mark object as reusable
record.snapshot_id.set(INVALID_SNAPSHOT);

// Reuse later
if record.snapshot_id.get() == INVALID_SNAPSHOT {
    record.snapshot_id.set(new_id);
    record.value.write() = new_value;
    return record;
}
```

**Benefits:**
- Reduces allocation pressure
- Maintains stable Arc pointers
- Amortizes allocation cost

#### Observer Pattern

**Used in:** Snapshot read/write tracking, invalidation

**Pattern:**
```rust,ignore
pub trait Snapshot {
    fn set_read_observer(&self, observer: Box<dyn Fn(&dyn StateObject)>);
    fn set_write_observer(&self, observer: Box<dyn Fn(&dyn StateObject)>);
}

// Usage
snapshot.set_read_observer(Box::new(|obj| {
    println!("Read: {:?}", obj.id());
    invalidation_tracker.record_read(current_scope, obj);
}));
```

**Benefits:**
- Decouple observation from core logic
- Enable multiple observation strategies
- Support transparent snapshots

#### Strategy Pattern

**Used in:** MutationPolicy, slot write sessions

**Pattern:**
```rust,ignore
pub trait MutationPolicy<T> {
    fn equivalent(&self, a: &T, b: &T) -> bool;
    fn merge(&self, previous: &T, current: &T, applied: &T) -> Option<T>;
}

// Implementations
pub struct StructuralEqualityPolicy<T>(PhantomData<T>);
pub struct ReferentialEqualityPolicy<T>(PhantomData<T>);
pub struct NeverEqualPolicy<T>(PhantomData<T>);
```

**Benefits:**
- Customize state comparison logic
- Different merge strategies per type
- Extensible without modifying core

#### Writer Frame Pattern

**Used in:** Slot table composition and recomposition sessions

**Pattern:**
```rust,ignore
pub(crate) struct SlotWriteSessionState {
    root: RootFrame,
    group_stack: Vec<GroupFrame>,
}

pub(in crate::slot) struct GroupFrame {
    group_anchor: AnchorId,
    old_children: Vec<AnchorId>,
    old_cursor: usize,
    next_child_index: usize,
    payload_cursor: usize,
    node_cursor: usize,
}
```

**Benefits:**
- Traversal state is scoped to the active writer session instead of living inside table storage
- Group reuse, movement, and restoration operate against sibling lists and anchors
- Composition code works through semantic `begin_group`/`finish_group_body`/`end_group` operations

---

### Performance Characteristics

#### Snapshot System

| Operation | Time Complexity | Notes |
|-----------|----------------|-------|
| Create snapshot | O(1) amortized | Allocate ID, copy open set |
| Read state | O(R) | R = record chain length, typically small |
| Write state | O(1) amortized | Reuse or prepend record |
| Apply snapshot | O(M × R) | M = modified objects, R = record chain |
| GC record cleanup | O(R) | Per state object |
| SnapshotIdSet get | O(1) recent, O(log N) old | Recent = last 128 IDs |
| SnapshotIdSet set | O(1) recent, O(N) old | Copy-on-write |

**Optimization opportunities:**
- Keep record chains short via aggressive GC
- Use read-only snapshots when possible (no tracking overhead)
- Batch apply operations
- Use ReferentialEqualityPolicy for cheap equality checks

#### Slot Table System

| Operation | Time Complexity | Notes |
|-----------|----------------|-------|
| `begin_group()` reuse | O(1) | Expected sibling already matches |
| `begin_group()` sibling search | O(D) | `D` = number of direct siblings examined |
| `move_subtree()` | O(G + P + N + T) | moved groups, payloads, nodes, and suffix metadata shifts |
| `finish_group_body()` | O(R + C) | trims direct payload/node tails and detaches remaining child subtrees |
| `detach_subtree()` | O(G + P + N + T) | extracts subtree records and repairs active indexes |
| `restore_subtree()` | O(G + P + N + T) | reinserts subtree records and recomputes metadata |
| `value_slot()` / `read_value()` | O(1) | payload anchor lookup plus owner-relative offset |
| `begin_recompose_at_scope()` | O(1) | scope index -> anchor -> active group |
| `validate()` | O(G + P + N + A + S) | full structural check in debug/test builds |

**Optimization opportunities:**
- Reduce temporary `Vec` cloning in detach/retention hot paths
- Profile subtree `Vec::drain` / `Vec::splice` costs before changing storage layout
- Add retained-memory instrumentation before pursuing more complex backends
- Keep validation strong while optimizing

#### Memory Usage

**Snapshot system:**
- Each StateRecord: ~64 bytes (Arc, Cell, RwLock overhead)
- SnapshotIdSet: 24 bytes + 8 bytes per old ID
- MutableSnapshot: ~200 bytes + modified map

**Slot table:**
- Group storage, payload storage, and node storage scale independently
- Anchor and scope indexes add hash-map overhead on top of active records
- Retained branches consume separate detached subtree allocations while inactive

**Scaling:**
- 10,000 UI elements no longer imply one flat slot array
- 1,000 state objects with 10 records each: ~640 KB
- Typical app: 1-10 MB for composition runtime

---

### Common Scenarios

#### Scenario 1: Simple State Update

```rust,ignore
// 1. Create state
let state = SnapshotMutableState::new(0, StructuralEqualityPolicy::new());

// 2. Read in current snapshot
let value = state.read(&current_snapshot);  // Returns 0

// 3. Create mutable snapshot
let snapshot = take_mutable_snapshot();

// 4. Write in snapshot
state.write(&snapshot, 42);

// 5. Apply snapshot
snapshot.apply()?;  // Merge into global

// 6. Read new value
let new_value = state.read(&current_snapshot);  // Returns 42
```

**Internals:**
1. State has single record: `[id=1, value=0]`
2. Write creates new record: `[id=2, value=42] → [id=1, value=0]`
3. Apply promotes child record to global visibility
4. Global snapshot now sees `id=2` as valid

#### Scenario 2: Conditional Rendering (Tabs)

```rust,ignore
// Initial composition - Tab 1 active
composer.with_group(TAB_GROUP, |composer| {
    composer.cranpose_with_reuse(TAB_1, RecomposeOptions::default(), |composer| {
        // ... tab 1 content ...
    });
});

// Hide Tab 1
composer.with_group(TAB_GROUP, |_composer| {
    // tab 1 branch omitted
});

// Result:
// - finish_group_body() detaches Tab 1 as DetachedSubtree
// - retain policy keeps it outside the active SlotTable
// - its remembered payloads, scopes, and node IDs stay owned by the detached subtree

// Show Tab 1 again
composer.with_group(TAB_GROUP, |composer| {
    composer.cranpose_with_reuse(TAB_1, RecomposeOptions::default(), |composer| {
        // ... tab 1 content restored from DetachedSubtree ...
    });
});
```

**Key benefit:** Tab 1's state is preserved by an explicit retained subtree, not by hidden gap
metadata in the active table.

#### Scenario 3: Concurrent Snapshot Conflict

```rust,ignore
// t0: Initial state
let state = SnapshotMutableState::new(10);

// t1: Create two snapshots
let snapshot1 = take_mutable_snapshot();  // base_parent_id = 1
let snapshot2 = take_mutable_snapshot();  // base_parent_id = 1

// t2: Snapshot1 writes
state.write(&snapshot1, 20);

// t3: Snapshot2 writes (concurrent)
state.write(&snapshot2, 30);

// t4: Snapshot1 applies first
snapshot1.apply()?;  // Success, now global = 20

// t5: Snapshot2 tries to apply
let result = snapshot2.apply();
// Conflict detected: last_write (snapshot1) != base_parent_id (global before)
// Merge attempt: previous=10, current=20, applied=30
// StructuralEqualityPolicy cannot merge integers
// Result: SnapshotApplyResult::Failure
```

**Handling conflicts:**
```rust,ignore
loop {
    let snapshot = take_mutable_snapshot();

    // Perform modifications
    state.write(&snapshot, new_value);

    // Try to apply
    match snapshot.apply() {
        SnapshotApplyResult::Success => break,
        SnapshotApplyResult::Failure => {
            // Retry with fresh snapshot
            continue;
        }
    }
}
```

#### Scenario 4: Nested Snapshots (Transaction-like)

```rust,ignore
let outer = take_mutable_snapshot();

// Modify state
state1.write(&outer, 10);

{
    let inner = outer.take_nested_mutable_snapshot();

    // Inner can see outer's changes
    assert_eq!(state1.read(&inner), 10);

    // Inner modifications
    state2.write(&inner, 20);

    // Apply inner to outer (not global yet)
    inner.apply()?;
}

// Outer can now see inner's changes
assert_eq!(state2.read(&outer), 20);

// Apply outer to global
outer.apply()?;

// Both changes now visible globally
assert_eq!(state1.read(&global_snapshot), 10);
assert_eq!(state2.read(&global_snapshot), 20);
```

**Use case:** Atomic multi-state updates, rollback on failure.

---

### Future Optimizations

#### Snapshot System

1. **Persistent Data Structures**: Replace record chains with persistent trees for O(log N) all operations
2. **Lock-Free Records**: Use atomic operations instead of RwLock for high-contention scenarios
3. **Compressed ID Sets**: Use roaring bitmaps for very large snapshot ID sets
4. **Lazy GC**: Defer record cleanup to background thread

#### Slot Table System

1. **Chunked subtree storage**: Replace hot `Vec::drain` / `Vec::splice` paths only if profiling proves they dominate.
2. **Denser group storage**: Pack `GroupRecord` fields or split arrays only if cache pressure matters in real traces.
3. **Retained-memory diagnostics**: Add stronger retained-subtree and anchor-capacity telemetry for leak hunting.
4. **Parallel recomposition**: Only after current invariants, retention semantics, and applier-side node ownership stay deterministic.

---

### Debugging Tips

#### Snapshot Debugging

**View record chain:**
```rust,ignore
fn debug_record_chain(state: &SnapshotMutableState<T>) {
    let head = state.head.read();
    let mut current = head.clone();

    loop {
        println!("Record {{ id: {}, tombstone: {}, value: {:?} }}",
            current.snapshot_id.get(),
            current.tombstone.get(),
            current.value.read().as_ref().map(|v| format!("{:?}", v))
        );

        match current.next.get() {
            Some(next) => current = next,
            None => break,
        }
    }
}
```

**Check snapshot visibility:**
```rust,ignore
fn is_visible_to_snapshot(
    record_id: SnapshotId,
    snapshot_id: SnapshotId,
    invalid: &SnapshotIdSet
) -> bool {
    record_id <= snapshot_id && !invalid.get(record_id)
}
```

#### Slot Table Debugging

**Dump the active slot table:**
```rust,ignore
fn debug_print_slots(composition: &Composition) {
    for entry in composition.debug_dump_slot_entries() {
        println!("{}: {}", entry.path, entry.line);
    }
}
```

**Inspect structured debug state:**
```rust,ignore
fn debug_snapshot(composition: &Composition) {
    let snapshot = composition.debug_slot_snapshot();
    println!("{snapshot:#?}");
}
```

**Force validation in debug/test paths:**
```rust,ignore
assert_eq!(slot_table.validate(), Ok(()));
```

**Enable automatic pass dumps:**
```bash
COMPOSE_DEBUG_SLOT_TABLE=1 cargo test -p cranpose-core slot::tests
```

---

### Summary

The Snapshots and Slot Table system provides a sophisticated foundation for cranpose:

**Snapshots** deliver:
- Isolated state views via MVCC
- Optimistic concurrency with conflict detection
- Flexible merge strategies
- Efficient garbage collection

**Slot Tables** deliver:
- Exact active-tree storage with separate payload and node tables
- Stable anchors and indexed scopes for targeted recomposition
- Explicit detached-subtree retention instead of semantic gaps
- Strong validation and debug snapshots for structural correctness

**Together** they enable:
- Predictable recomposition
- State preservation across structural changes
- Concurrent snapshot modifications
- High-performance UI updates

This design mirrors Jetpack Compose's battle-tested architecture while leveraging Rust's ownership model for memory safety and performance.