Skip to content

Commit d773dd6

Browse files
committed
mudlark: introduce generational GNodeId handle (ADR-M-040)
The G-node arena reuses slots via a free list. A handle obtained before an eviction could silently bind to a new, unrelated node after the slot is reallocated (ABA problem). This is not hypothetical — the sentinel stores handles across observation cycles where evict-then-split sequences can recycle slots. Introduce a two-tier handle design: - GNodeId (pub, 8 bytes): slot index + generation counter. Used on all public API surfaces. Consuming methods (decay, gnode_info, is_ancestor_of) validate the generation and panic on mismatch. - GSlotPointer (pub(crate), 4 bytes): generation-free handle for internal tree walks where no interleaving eviction can occur. Renamed from the former GNodeId. - VSlotPointer (pub(crate), 4 bytes): renamed from VNodeId for consistency with the new naming convention. Arena changes: - alloc() returns (index, generation) tuple - dealloc() increments the per-slot generation counter - New generation() accessor for handle validation The generation Vec is cold data — touched only on alloc/dealloc, never on hot read/write paths. Internal hot paths continue to use 4-byte GSlotPointer; the 8-byte GNodeId exists only at the API boundary.
1 parent 5227ea5 commit d773dd6

66 files changed

Lines changed: 1327 additions & 845 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

packages/mudlark/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -536,7 +536,7 @@ canonical path per type.
536536
| `ContourRange<C, V>` | 1 | Full contour-range decomposition of a lattice-aligned interval |
537537
| `ContourRangeEnergy<V>` | 1 | Energy-only result of a contour range query |
538538
| `BasisElement<C, V>` | 1 | One element of the minimal G-node cover of a contour range |
539-
| `GNodeId` | 1 | Opaque arena handle |
539+
| `GNodeId` | 1 | Opaque generational arena handle (ADR-M-040) |
540540

541541
### Key operations on `GvGraph`
542542

packages/mudlark/adr/001-node-storage.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -40,8 +40,8 @@ and invalidation safety.
4040

4141
**Option B — Generational `Vec` arenas**, refined with:
4242

43-
- **`NonZeroU32` niche-optimized handles**`Option<GNodeId>` and
44-
`Option<VNodeId>` are exactly 4 bytes (no discriminant overhead).
43+
- **`NonZeroU32` niche-optimized handles**`Option<GSlotPointer>` and
44+
`Option<VSlotPointer>` are exactly 4 bytes (no discriminant overhead).
4545
- **Separate `Vec<u64>` bitset** for occupancy tracking instead of
4646
per-slot generation counters, keeping node structs cache-line sized.
4747
- **`debug_assert!`** on occupancy for stale-handle detection — catches
@@ -74,11 +74,11 @@ occupancy, compiles to bare array access in release.
7474
```rust
7575
use std::num::NonZeroU32;
7676

77-
struct GNodeId(NonZeroU32); // 4 bytes, Option = 4 bytes
78-
struct VNodeId(NonZeroU32); // 4 bytes, Option = 4 bytes
77+
struct GSlotPointer(NonZeroU32); // 4 bytes, Option = 4 bytes
78+
struct VSlotPointer(NonZeroU32); // 4 bytes, Option = 4 bytes
7979
```
8080

81-
Type-safe: `GNodeId` cannot index the V-node arena (compile error).
81+
Type-safe: `GSlotPointer` cannot index the V-node arena (compile error).
8282
Opaque outside the crate — `from_index()` and `index()` are
8383
currently `pub` (§API M-4.4 notes this as potential future
8484
tightening).

packages/mudlark/adr/002-vtree-node-enum.md

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ module — `VNode<V>`, `VKind<V>`, `PackedChildren<V>`)
1515
**Related ADRs:**
1616

1717
- [ADR-M-001](001-node-storage.md): arena topology — determines that
18-
`VNode<V>` lives in a single `Arena<VNode<V>>` with `VNodeId`
18+
`VNode<V>` lives in a single `Arena<VNode<V>>` with `VSlotPointer`
1919
handles
2020
- [ADR-M-013](013-eviction-eligibility.md): eviction eligibility uses
2121
`is_evictable` on entries and `has_evictable` on structural nodes
@@ -37,7 +37,7 @@ pattern-matching ergonomics, arena design, and traversal code.
3737

3838
1. **Separate types + trait:** `VEntry<V>` and `VStructural<V>` in
3939
separate arenas, unified by a `VNode` trait. Requires two arena
40-
pools and a `VNodeId` enum to distinguish which pool to index.
40+
pools and a `VSlotPointer` enum to distinguish which pool to index.
4141

4242
2. **Single enum:** `VNode<V>` with a `VKind<V>` enum. One arena,
4343
one handle type. Pattern match on `node.kind` at each use site.
@@ -49,14 +49,14 @@ pattern-matching ergonomics, arena design, and traversal code.
4949
```rust
5050
struct VNode<V> { // 64 bytes for V = u64 — one cache line
5151
intensity: V, // 8 bytes: sum for structural, own for entry
52-
parent: Option<VNodeId>, // 4 bytes
52+
parent: Option<VSlotPointer>, // 4 bytes
5353
cached_depth: AtomicU32, // 4 bytes: padding gap (ADR-M-029)
5454
kind: VKind<V>, // 48 bytes
5555
}
5656

5757
enum VKind<V> {
5858
Entry {
59-
gnode: GNodeId, // backing G-node (V-I4)
59+
gnode: GSlotPointer, // backing G-node (V-I4)
6060
is_exposed: bool, // on contour? (V-I6)
6161
is_evictable: bool, // zero G-children? (V-I6b)
6262
},
@@ -103,7 +103,7 @@ Two booleans on `VKind::Entry`, each serving a distinct purpose:
103103
```rust
104104
struct PackedChildren<V> { // 40 bytes for V = u64
105105
intensities: [V; 3], // 24 bytes — hot: one scan per sampling step
106-
ids: [Option<VNodeId>; 3], // 12 bytes — cold: read only after winner chosen
106+
ids: [Option<VSlotPointer>; 3], // 12 bytes — cold: read only after winner chosen
107107
len: u8, // 1 byte — 2 or 3
108108
}
109109
```
@@ -119,7 +119,7 @@ visits the parent), so maintenance cost is zero.
119119

120120
## Consequences
121121

122-
- One arena (`Arena<VNode<V>>`) and one handle type (`VNodeId`) for
122+
- One arena (`Arena<VNode<V>>`) and one handle type (`VSlotPointer`) for
123123
all V-Tree nodes. Simpler than two pools + enum handle.
124124
- Pattern matching on `node.kind` is ergonomic and exhaustive —
125125
the compiler enforces that both variants are handled.

packages/mudlark/adr/003-violation-tracking.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ entry's intensity grows past its max uncle. The rebalance loop
2525

2626
## Decision
2727

28-
**Eager tracking with a `Vec<VNodeId>` work queue.**
28+
**Eager tracking with a `Vec<VSlotPointer>` work queue.**
2929

3030
### Rationale
3131

@@ -34,7 +34,7 @@ during sum propagation. The uncle check reads the grandparent's
3434
`PackedChildren` — which is in the same cache line, already loaded.
3535
The violation check is **free in cache terms**.
3636

37-
Pushing a `VNodeId` onto a `Vec` is a single write. Scanning the
37+
Pushing a `VSlotPointer` onto a `Vec` is a single write. Scanning the
3838
entire V-Tree to find the same violations would re-read nodes
3939
already in hand.
4040

@@ -49,7 +49,7 @@ struct GvGraph<C, V, const N: u32> {
4949

5050
/// Work queue — scoped to one mutation batch (observe, evict, decay).
5151
/// Built during propagation, drained by rebalance(), empty on return.
52-
violations: Vec<VNodeId>,
52+
violations: Vec<VSlotPointer>,
5353
}
5454
```
5555

@@ -303,7 +303,7 @@ The implementation includes a safety-net iteration limit proportional
303303
to arena size (`20 × node_count`, floor 10 000). In debug builds
304304
the limit panics; in release builds it breaks out with an error log.
305305

306-
`rebalance()` returns `Vec<GNodeId>` — the G-nodes created by
306+
`rebalance()` returns `Vec<GSlotPointer>` — the G-nodes created by
307307
legacy promotions (§IDEA M-11.6) during the drain, so the caller can handle
308308
plateau and depth-control bookkeeping.
309309

packages/mudlark/adr/006-generic-parameters.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -495,7 +495,7 @@ over `O: Observation<V>`. See [ADR-M-024](024-decay-semantics.md) for
495495
the full design rationale.
496496

497497
```rust
498-
pub fn decay(&mut self, root: GNodeId, attenuation: f64, q: f64);
498+
pub fn decay(&mut self, root: GSlotPointer, attenuation: f64, q: f64);
499499
```
500500

501501
The original design made `decay` generic over `O: Observation<V>`.

packages/mudlark/adr/007-thread-safety.md

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -56,10 +56,10 @@ Rust auto-derives `Send + Sync` for the entire struct.
5656
| ------------------- | --------------------------------------- | ----------------------------- |
5757
| `gnodes` | `Arena<GNode<C, V>>` | `Send + Sync` when `C, V` are |
5858
| `vnodes` | `Arena<VNode<V>>` | `Send + Sync` when `V` is ¹ |
59-
| `g_root` | `GNodeId` (`NonZeroU32`) | `Copy + Send + Sync` |
60-
| `v_root` | `Option<VNodeId>` (`NonZeroU32` niche) | `Copy + Send + Sync` |
59+
| `g_root` | `GSlotPointer` (`NonZeroU32`) | `Copy + Send + Sync` |
60+
| `v_root` | `Option<VSlotPointer>` (`NonZeroU32` niche) | `Copy + Send + Sync` |
6161
| `config` | `Config<V>` | `Send + Sync` when `V` is |
62-
| `violations` | `Vec<VNodeId>` | `Send + Sync` |
62+
| `violations` | `Vec<VSlotPointer>` | `Send + Sync` |
6363
| `node_count` | `u32` | `Send + Sync` |
6464
| `terminal_count` | `u32` | `Send + Sync` |
6565
| `live_depth_evict` | `u32` | `Send + Sync` |
@@ -68,7 +68,7 @@ Rust auto-derives `Send + Sync` for the entire struct.
6868
| `headroom` | `usize` | `Send + Sync` |
6969
| `soft_limit` | `Option<usize>` | `Send + Sync` |
7070
| `plateaus`| `BTreeMap<BasisEdge<C>, Plateau<C, V>>` | `Send + Sync` when `C, V` are |
71-
| `pending_p_i4`| `Vec<(GNodeId, BasisEdge<C>)>` | `Send + Sync` when `C` is |
71+
| `pending_p_i4`| `Vec<(GSlotPointer, BasisEdge<C>)>` | `Send + Sync` when `C` is |
7272
| `plateau_basis`| `PlateauBasis<C>` ² | `Send + Sync` when `C` is |
7373
| `plateaus_dirty`| `bool` | `Send + Sync` |
7474

@@ -79,7 +79,7 @@ Rust auto-derives `Send + Sync` for the entire struct.
7979
auto-trait depends only on `V`.
8080

8181
² `PlateauBasis<C>` is backed by a `BTreeMap<BasisEdge<C>,
82-
HashSet<GNodeId>>` (forward map) and a `HashMap<GNodeId,
82+
HashSet<GSlotPointer>>` (forward map) and a `HashMap<GSlotPointer,
8383
BasisEdge<C>>` (back map). All three collections are `Send + Sync`
8484
when their element types are — satisfied when `C: Send + Sync`.
8585

packages/mudlark/adr/008-span-type.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@ What should borrowed view types look like?
8585

8686
| Option | Design | Notes |
8787
| ------ | --------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- |
88-
| A | `Cell<'a, C, V>` borrows `&'a GvGraph` + `GNodeId` — deref to fields lazily | Zero-copy, minimal struct (pointer + handle). But every field access goes through a pointer chase. |
88+
| A | `Cell<'a, C, V>` borrows `&'a GvGraph` + `GSlotPointer` — deref to fields lazily | Zero-copy, minimal struct (pointer + handle). But every field access goes through a pointer chase. |
8989
| B | `Cell<'a, C, V>` contains `start: C, end: C, intensity: V, depth: u32` — snapshot at creation | Copied on construction. Self-contained. No borrow escape hazards. Can outlive the graph if `'a` is removed (but then it's just a `Span`). |
9090
| C | `Cell<'a, C, V> = &'a Span<C, V>``Cell` is a type alias for a reference | Simplest. No new struct. But can't add Cell-specific methods without a newtype. |
9191
| D | `Cell<'a, C, V>` borrows individual fields: `start: &'a C, end: &'a C, intensity: &'a V` | Fine-grained borrows. Unusual in Rust — typically borrow-of-struct patterns use a single reference. |
@@ -98,7 +98,7 @@ What should borrowed view types look like?
9898
- `Cell` should support `cell.to_span() -> Span<C, V>` for ownership
9999
transfer regardless of option chosen.
100100
- `Node` (an internal G-Tree node) could follow the same pattern as
101-
Cell, adding `children: (GNodeId, GNodeId)` for traversal. Or it
101+
Cell, adding `children: (GSlotPointer, GSlotPointer)` for traversal. Or it
102102
can be deferred — most API surfaces only expose terminal cells.
103103
- If `Cell` is just a `Span` with a lifetime, Option C is simplest
104104
and avoids type proliferation.

packages/mudlark/adr/009-trait-decomposition.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ pub trait SpatialWrite: SpatialRead {
6161
/// Temporal decay: subband-adaptive attenuation.
6262
/// Requires `Self::Accum: Attenuatable`.
6363
pub trait TemporalDecay: SpatialRead {
64-
fn decay(&mut self, root: GNodeId, attenuation: f64, q: f64);
64+
fn decay(&mut self, root: GSlotPointer, attenuation: f64, q: f64);
6565
}
6666

6767
/// Weighted random sampling over entries by intensity.

packages/mudlark/adr/012-g-sum-recomputation.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ requires subtraction internally — reduces to Option A or B.
9090
```rust
9191
pub fn recompute_g_sums<C: Coordinate, V: Accumulator>(
9292
gnodes: &mut Arena<GNode<C, V>>,
93-
start: GNodeId,
93+
start: GSlotPointer,
9494
) {
9595
let mut current = Some(start);
9696
while let Some(id) = current {

packages/mudlark/adr/015-eviction-scan-design.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -518,7 +518,7 @@ Option A).
518518

519519
```rust
520520
fn check_evictions(&mut self) -> u32 {
521-
// Phase 1: collect eligible entry VNodeIds
521+
// Phase 1: collect eligible entry VSlotPointers
522522
let candidates = evict::scan_for_candidates(self);
523523

524524
// Phase 2: evict each candidate
@@ -565,7 +565,7 @@ if soft_limit_exceeded {
565565

566566
### Recommendation: Two-phase collect-then-evict with single trailing rebalance
567567

568-
Phase 1 (scan) collects candidates into a `Vec<VNodeId>`. Phase 2
568+
Phase 1 (scan) collects candidates into a `Vec<VSlotPointer>`. Phase 2
569569
evicts each with re-verification. Violations accumulate during
570570
phase 2. Phase 3 is a single `rebalance()` call to drain all
571571
violations. This avoids mutating the tree during traversal, avoids

0 commit comments

Comments
 (0)