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
//! Identifier newtypes — the handle-protocol's core type vocabulary.
//!
//! Mirrors `~/src/graphrefly-ts/src/__experiments__/handle-core/core.ts:52–57`
//! under Rust's stricter type discipline (CLAUDE.md Rust invariant 8 — no raw
//! integers in public APIs).
//!
//! # Cleaving plane
//!
//! - [`NodeId`] — identifies a node in the graph. Allocated by the Core; opaque
//! to bindings.
//! - [`HandleId`] — identifies a user value `T` in the binding-side registry.
//! The Core never sees `T`; equals-substitution under `equals: identity` is
//! a `u64` compare on `HandleId` (zero FFI). Custom equals crosses the
//! boundary explicitly via [`crate::boundary::BindingBoundary::custom_equals`].
//! - [`FnId`] — identifies a user function (or a custom-equals oracle) in the
//! binding-side registry.
//! - [`LockId`] — identifies a pause-lock. Multiple pausers can hold distinct
//! locks on the same node; the node remains paused until the lockset is
//! empty (R1.2.6, R2.6).
//!
//! All four are `u64` newtypes for cheap hashing, atomic increments, and
//! lock-free version counters. They are intentionally NOT
//! interconvertible — `NodeId(7)` and `HandleId(7)` are different things.
/// Identifier for a node in the Core's dispatcher.
;
/// Identifier for a user value `T` in the binding-side registry.
///
/// The Core stores `HandleId` everywhere user values would otherwise sit:
/// `cache`, `prevData`, `Data`/`Error` payloads, dep records. Equals-substitution
/// under `EqualsMode::Identity` compares `HandleId` directly (a `u64` ==).
;
/// Sentinel "no handle" — distinct from any valid handle (which start at 1).
///
/// Mirrors `core.ts:57` `NO_HANDLE = 0`. Used for:
/// - `cache` of compute nodes that haven't fired yet.
/// - `prevData[i]` for deps that haven't delivered DATA yet.
/// - First-run gate condition (R2.5.3).
///
/// Per the handle-protocol cleaving plane, the binding-side registry refuses to
/// intern `undefined`/`None` (the global SENTINEL per R1.2.4 / Lock 5.A);
/// no real handle ever collides with this.
pub const NO_HANDLE: HandleId = HandleId;
/// Identifier for a user function (or a custom-equals oracle) in the
/// binding-side registry.
///
/// The Core invokes user code by sending `(node_id, fn_id, dep_handles)` across
/// the [`crate::boundary::BindingBoundary`]; the binding-side dereferences
/// `fn_id` to a callable and `dep_handles` to user values, then registers the
/// fn's output as a new handle and returns it.
;
/// Identifier for a pause-lock.
///
/// Per R1.2.6, `[PAUSE, lockId]` and `[RESUME, lockId]` MUST carry a lock id.
/// Each node maintains a lockset (`HashSet<LockId>`); `paused` derives from
/// `lockset.is_empty()`. Unknown-lockId `Resume` is a no-op (idempotent
/// dispose).
///
/// # Allocation ranges (Slice F, A4 — 2026-05-07)
///
/// To prevent collision between user-supplied and dispatcher-allocated lock
/// ids:
///
/// - `[0, 1<<32)` — **user range.** Direct callers of [`LockId::new`] (and
/// the napi-rs binding's `u32 → LockId` marshalling) live here. Pick any
/// value you like; the dispatcher will not allocate any id in this range.
/// - `[1<<32, u64::MAX]` — **dispatcher range.** [`crate::Core::alloc_lock_id`]
/// draws from this range, starting at `1<<32` and incrementing. Allocation
/// is monotonic; no recycling.
///
/// Both constructors are public — the range convention is by construction at
/// the dispatcher, not by visibility on the type. If a binding marshals lock
/// ids beyond `u32::MAX` from user-facing input, raise the dispatcher floor
/// (`CoreState::next_lock_id`) at construction time.
;