graphrefly_core/handle.rs
1//! Identifier newtypes — the handle-protocol's core type vocabulary.
2//!
3//! Mirrors `~/src/graphrefly-ts/src/__experiments__/handle-core/core.ts:52–57`
4//! under Rust's stricter type discipline (CLAUDE.md Rust invariant 8 — no raw
5//! integers in public APIs).
6//!
7//! # Cleaving plane
8//!
9//! - [`NodeId`] — identifies a node in the graph. Allocated by the Core; opaque
10//! to bindings.
11//! - [`HandleId`] — identifies a user value `T` in the binding-side registry.
12//! The Core never sees `T`; equals-substitution under `equals: identity` is
13//! a `u64` compare on `HandleId` (zero FFI). Custom equals crosses the
14//! boundary explicitly via [`crate::boundary::BindingBoundary::custom_equals`].
15//! - [`FnId`] — identifies a user function (or a custom-equals oracle) in the
16//! binding-side registry.
17//! - [`LockId`] — identifies a pause-lock. Multiple pausers can hold distinct
18//! locks on the same node; the node remains paused until the lockset is
19//! empty (R1.2.6, R2.6).
20//!
21//! All four are `u64` newtypes for cheap hashing, atomic increments, and
22//! lock-free version counters. They are intentionally NOT
23//! interconvertible — `NodeId(7)` and `HandleId(7)` are different things.
24
25/// Identifier for a node in the Core's dispatcher.
26#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
27pub struct NodeId(u64);
28
29impl NodeId {
30 /// Wrap a raw `u64` as a `NodeId`. Production code allocates via the Core's
31 /// id counter; this constructor exists for tests and for the binding-side
32 /// registry to round-trip ids through serialization.
33 #[must_use]
34 pub const fn new(raw: u64) -> Self {
35 Self(raw)
36 }
37
38 /// Unwrap to the underlying `u64`. Use for hashing, logging, or
39 /// FFI marshalling — never for arithmetic that mixes id spaces.
40 #[must_use]
41 pub const fn raw(self) -> u64 {
42 self.0
43 }
44}
45
46/// Identifier for a user value `T` in the binding-side registry.
47///
48/// The Core stores `HandleId` everywhere user values would otherwise sit:
49/// `cache`, `prevData`, `Data`/`Error` payloads, dep records. Equals-substitution
50/// under `EqualsMode::Identity` compares `HandleId` directly (a `u64` ==).
51#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
52pub struct HandleId(u64);
53
54impl HandleId {
55 /// Wrap a raw `u64`. Real handles come from the binding-side registry
56 /// (`bindings.ts` `valueRegistry.intern(value)`); this constructor is for
57 /// tests and FFI round-tripping.
58 #[must_use]
59 pub const fn new(raw: u64) -> Self {
60 Self(raw)
61 }
62
63 /// Unwrap to the underlying `u64`.
64 #[must_use]
65 pub const fn raw(self) -> u64 {
66 self.0
67 }
68
69 /// True if this is the [`NO_HANDLE`] sentinel (handle id 0 is never
70 /// assigned to a real value). Equivalent to `self == NO_HANDLE`.
71 #[must_use]
72 pub const fn is_sentinel(self) -> bool {
73 self.0 == NO_HANDLE.0
74 }
75}
76
77impl std::fmt::Display for HandleId {
78 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
79 write!(f, "HandleId({})", self.0)
80 }
81}
82
83/// Sentinel "no handle" — distinct from any valid handle (which start at 1).
84///
85/// Mirrors `core.ts:57` `NO_HANDLE = 0`. Used for:
86/// - `cache` of compute nodes that haven't fired yet.
87/// - `prevData[i]` for deps that haven't delivered DATA yet.
88/// - First-run gate condition (R2.5.3).
89///
90/// Per the handle-protocol cleaving plane, the binding-side registry refuses to
91/// intern `undefined`/`None` (the global SENTINEL per R1.2.4 / Lock 5.A);
92/// no real handle ever collides with this.
93pub const NO_HANDLE: HandleId = HandleId(0);
94
95/// Identifier for a user function (or a custom-equals oracle) in the
96/// binding-side registry.
97///
98/// The Core invokes user code by sending `(node_id, fn_id, dep_handles)` across
99/// the [`crate::boundary::BindingBoundary`]; the binding-side dereferences
100/// `fn_id` to a callable and `dep_handles` to user values, then registers the
101/// fn's output as a new handle and returns it.
102#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
103pub struct FnId(u64);
104
105impl FnId {
106 #[must_use]
107 pub const fn new(raw: u64) -> Self {
108 Self(raw)
109 }
110
111 #[must_use]
112 pub const fn raw(self) -> u64 {
113 self.0
114 }
115}
116
117/// Identifier for a pause-lock.
118///
119/// Per R1.2.6, `[PAUSE, lockId]` and `[RESUME, lockId]` MUST carry a lock id.
120/// Each node maintains a lockset (`HashSet<LockId>`); `paused` derives from
121/// `lockset.is_empty()`. Unknown-lockId `Resume` is a no-op (idempotent
122/// dispose).
123///
124/// # Allocation ranges (Slice F, A4 — 2026-05-07)
125///
126/// To prevent collision between user-supplied and dispatcher-allocated lock
127/// ids:
128///
129/// - `[0, 1<<32)` — **user range.** Direct callers of [`LockId::new`] (and
130/// the napi-rs binding's `u32 → LockId` marshalling) live here. Pick any
131/// value you like; the dispatcher will not allocate any id in this range.
132/// - `[1<<32, u64::MAX]` — **dispatcher range.** [`crate::Core::alloc_lock_id`]
133/// draws from this range, starting at `1<<32` and incrementing. Allocation
134/// is monotonic; no recycling.
135///
136/// Both constructors are public — the range convention is by construction at
137/// the dispatcher, not by visibility on the type. If a binding marshals lock
138/// ids beyond `u32::MAX` from user-facing input, raise the dispatcher floor
139/// (`CoreState::next_lock_id`) at construction time.
140#[derive(Copy, Clone, Eq, PartialEq, Hash, Debug, Ord, PartialOrd)]
141pub struct LockId(u64);
142
143impl LockId {
144 #[must_use]
145 pub const fn new(raw: u64) -> Self {
146 Self(raw)
147 }
148
149 #[must_use]
150 pub const fn raw(self) -> u64 {
151 self.0
152 }
153}
154
155#[cfg(test)]
156mod tests {
157 use super::*;
158
159 #[test]
160 fn ids_are_distinct_types() {
161 // Compile-time: these would not coexist if NodeId/HandleId/FnId/LockId
162 // were aliases. Run-time: just confirm the round-trip works.
163 let n = NodeId::new(42);
164 let h = HandleId::new(42);
165 let f = FnId::new(42);
166 let l = LockId::new(42);
167 assert_eq!(n.raw(), 42);
168 assert_eq!(h.raw(), 42);
169 assert_eq!(f.raw(), 42);
170 assert_eq!(l.raw(), 42);
171 }
172
173 #[test]
174 fn no_handle_is_sentinel() {
175 assert!(NO_HANDLE.is_sentinel());
176 assert_eq!(NO_HANDLE.raw(), 0);
177 assert!(!HandleId::new(1).is_sentinel());
178 }
179
180 #[test]
181 fn copy_eq_hash() {
182 use std::collections::HashSet;
183 // u64 newtypes round-trip through Copy + Eq + Hash for use in
184 // HashMap / HashSet / DashMap keys.
185 let a = HandleId::new(7);
186 let b = a;
187 assert_eq!(a, b);
188
189 let mut set = HashSet::new();
190 set.insert(NodeId::new(1));
191 set.insert(NodeId::new(2));
192 assert!(set.contains(&NodeId::new(1)));
193 assert!(!set.contains(&NodeId::new(99)));
194 }
195}