@harperfast/hnsw 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/graph.rs ADDED
@@ -0,0 +1,650 @@
1
+ //! Slot-level node access over the plane file, mediated by per-slot seqlocks. Hot-path
2
+ //! reads (distance, neighbor ids) are zero-copy against the mmap; full-copy read_node
3
+ //! exists for construction paths. Upper-layer adjacency lives in a fixed-entry region of
4
+ //! the same file (per-entry seqlocks), so the hierarchy persists with the graph and
5
+ //! concurrent searches share nothing mutable.
6
+
7
+ use crate::distance::{cosine_i8_i8_raw, cosine_int8_raw, Query};
8
+ use crate::format::{
9
+ PlaneFile, FLAG_DELETED, FLAG_VALID, MAX_UPPER_LEVELS, NO_UPPER, S_DEGREE, S_FLAGS, S_INV_MAG, S_LEVEL, S_SCALE,
10
+ S_UPPER_IDX, S_VECTOR, UPPER_CAP, UPPER_LEVEL_STRIDE, U_LEVELS, U_LISTS,
11
+ };
12
+ use crate::seqlock;
13
+ use crate::seqlock::Wedged;
14
+
15
+ pub struct Graph {
16
+ pub file: PlaneFile,
17
+ }
18
+
19
+ /// A consistent full copy of one node (construction paths only; search uses zero-copy).
20
+ pub struct NodeRead {
21
+ pub level: u8,
22
+ pub scale: f32,
23
+ pub inv_mag: f32,
24
+ pub vector: Vec<i8>,
25
+ pub neighbors: Vec<u32>,
26
+ }
27
+
28
+ impl Graph {
29
+ pub fn new(file: PlaneFile) -> Self {
30
+ Graph { file }
31
+ }
32
+
33
+ #[inline]
34
+ fn in_range(&self, id: u32) -> bool {
35
+ (id as u64) < self.file.id_high_water()
36
+ }
37
+
38
+ /// Sanitizer for a slot lock taken over from a dead writer: the payload is half-written,
39
+ /// so the slot must read as deleted until something rewrites it (heal-on-touch contract;
40
+ /// FLAG_DELETED rather than 0 so hosts can still free/reuse the id).
41
+ fn slot_sanitizer(&self, id: u32) -> impl Fn() + '_ {
42
+ move || unsafe {
43
+ let p = self.file.slot_ptr_mut(id);
44
+ // a dead writer's slot may hold a garbage (or zero-initialized) upper index; a
45
+ // later raw rewrite would reuse it and clobber another node's hierarchy
46
+ (p.add(S_UPPER_IDX) as *mut u32).write_unaligned(NO_UPPER);
47
+ *p.add(S_FLAGS) = FLAG_DELETED;
48
+ }
49
+ }
50
+
51
+ fn owner_dead(&self) -> impl Fn(u32) -> bool + '_ {
52
+ move |tag| self.file.tag_is_dead(tag)
53
+ }
54
+
55
+ /// Sanitizer for an upper-entry lock taken over from a dead writer.
56
+ fn upper_sanitizer(&self, idx: u32) -> impl Fn() + '_ {
57
+ move || unsafe { *self.file.upper_ptr_mut(idx).add(U_LEVELS) = 0 }
58
+ }
59
+
60
+ /// Zero-copy distance from `query` to the stored vector of `id`. None for absent/deleted.
61
+ #[inline]
62
+ pub fn distance_to(&self, id: u32, query: &Query) -> Option<f32> {
63
+ if !self.in_range(id) {
64
+ return None;
65
+ }
66
+ let seq = self.file.seq_atomic(id);
67
+ seqlock::read_consistent(seq, self.file.self_tag, || {
68
+ let p = self.file.slot_ptr(id);
69
+ unsafe {
70
+ let flags = *p.add(S_FLAGS);
71
+ if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
72
+ return None;
73
+ }
74
+ let scale = (p.add(S_SCALE) as *const f32).read_unaligned();
75
+ let inv_mag = (p.add(S_INV_MAG) as *const f32).read_unaligned();
76
+ Some(cosine_int8_raw(query, p.add(S_VECTOR) as *const i8, scale, inv_mag))
77
+ }
78
+ }, self.slot_sanitizer(id), || None, self.owner_dead())
79
+ }
80
+
81
+ /// Symmetric stored-to-stored distance (construction-time neighbor↔neighbor checks).
82
+ /// Plain unlocked reads: a torn read only perturbs a construction heuristic.
83
+ pub fn distance_between(&self, a: u32, b: u32) -> Option<f32> {
84
+ if !self.in_range(a) || !self.in_range(b) {
85
+ return None;
86
+ }
87
+ let dims = self.file.dims;
88
+ let pa = self.file.slot_ptr(a);
89
+ let pb = self.file.slot_ptr(b);
90
+ unsafe {
91
+ let fa = *pa.add(S_FLAGS);
92
+ let fb = *pb.add(S_FLAGS);
93
+ if fa & FLAG_VALID == 0 || fa & FLAG_DELETED != 0 || fb & FLAG_VALID == 0 || fb & FLAG_DELETED != 0 {
94
+ return None;
95
+ }
96
+ let scale_a = (pa.add(S_SCALE) as *const f32).read_unaligned();
97
+ let inv_a = (pa.add(S_INV_MAG) as *const f32).read_unaligned();
98
+ let scale_b = (pb.add(S_SCALE) as *const f32).read_unaligned();
99
+ let inv_b = (pb.add(S_INV_MAG) as *const f32).read_unaligned();
100
+ Some(cosine_i8_i8_raw(
101
+ pa.add(S_VECTOR) as *const i8,
102
+ scale_a,
103
+ inv_a,
104
+ pb.add(S_VECTOR) as *const i8,
105
+ scale_b,
106
+ inv_b,
107
+ dims,
108
+ ))
109
+ }
110
+ }
111
+
112
+ /// Copy layer-0 neighbor ids into `out` (cleared first). Returns the node's level,
113
+ /// or None for absent/deleted.
114
+ #[inline]
115
+ pub fn neighbors_into(&self, id: u32, out: &mut Vec<u32>) -> Option<u8> {
116
+ out.clear();
117
+ if !self.in_range(id) {
118
+ return None;
119
+ }
120
+ let seq = self.file.seq_atomic(id);
121
+ let cap = self.file.layer0_cap;
122
+ let dims = self.file.dims;
123
+ seqlock::read_consistent(seq, self.file.self_tag, || {
124
+ out.clear();
125
+ let p = self.file.slot_ptr(id);
126
+ unsafe {
127
+ let flags = *p.add(S_FLAGS);
128
+ if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
129
+ return None;
130
+ }
131
+ let level = *p.add(S_LEVEL);
132
+ let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
133
+ let base = p.add(S_VECTOR + dims) as *const u32;
134
+ for i in 0..degree.min(cap) {
135
+ out.push(u32::from_le(base.add(i).read_unaligned()));
136
+ }
137
+ Some(level)
138
+ }
139
+ }, self.slot_sanitizer(id), || None, self.owner_dead())
140
+ }
141
+
142
+ /// The node's upper-region entry index, or NO_UPPER.
143
+ #[inline]
144
+ fn upper_idx_of(&self, id: u32) -> u32 {
145
+ if !self.in_range(id) {
146
+ return NO_UPPER;
147
+ }
148
+ let seq = self.file.seq_atomic(id);
149
+ seqlock::read_consistent(seq, self.file.self_tag, || {
150
+ let p = self.file.slot_ptr(id);
151
+ unsafe {
152
+ let flags = *p.add(S_FLAGS);
153
+ if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
154
+ return NO_UPPER;
155
+ }
156
+ (p.add(S_UPPER_IDX) as *const u32).read_unaligned()
157
+ }
158
+ }, self.slot_sanitizer(id), || NO_UPPER, self.owner_dead())
159
+ }
160
+
161
+ /// Copy `id`'s neighbor ids at upper `level` (1-based) into `out`. False when the node
162
+ /// has no upper entry or no such level.
163
+ pub fn upper_neighbors_into(&self, id: u32, level: u8, out: &mut Vec<u32>) -> bool {
164
+ out.clear();
165
+ debug_assert!(level >= 1);
166
+ let idx = self.upper_idx_of(id);
167
+ if idx == NO_UPPER || (idx as u64) >= self.file.upper_capacity || level as usize > MAX_UPPER_LEVELS {
168
+ return false;
169
+ }
170
+ let seq = self.file.upper_seq_atomic(idx);
171
+ seqlock::read_consistent(seq, self.file.self_tag, || {
172
+ out.clear();
173
+ let p = self.file.upper_ptr(idx);
174
+ unsafe {
175
+ let levels = *p.add(U_LEVELS);
176
+ if level > levels {
177
+ return false;
178
+ }
179
+ let lp = p.add(U_LISTS + (level as usize - 1) * UPPER_LEVEL_STRIDE);
180
+ let degree = u16::from_le((lp as *const u16).read_unaligned()) as usize;
181
+ let base = lp.add(2) as *const u32;
182
+ for i in 0..degree.min(UPPER_CAP) {
183
+ out.push(u32::from_le(base.add(i).read_unaligned()));
184
+ }
185
+ true
186
+ }
187
+ }, self.upper_sanitizer(idx), || false, self.owner_dead())
188
+ }
189
+
190
+ /// Write a node's full upper adjacency into a fresh region entry; returns the entry
191
+ /// index to store in the slot (NO_UPPER when the region is exhausted or levels is empty).
192
+ pub fn write_upper(&self, levels: &[Vec<u32>]) -> Result<u32, Wedged> {
193
+ if levels.is_empty() {
194
+ return Ok(NO_UPPER);
195
+ }
196
+ let idx = self.file.allocate_upper();
197
+ if idx == NO_UPPER {
198
+ return Ok(NO_UPPER);
199
+ }
200
+ let seq = self.file.upper_seq_atomic(idx);
201
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.upper_sanitizer(idx), self.owner_dead())?;
202
+ let p = self.file.upper_ptr_mut(idx);
203
+ unsafe {
204
+ let n = levels.len().min(MAX_UPPER_LEVELS);
205
+ *p.add(U_LEVELS) = n as u8;
206
+ for (l, list) in levels.iter().take(n).enumerate() {
207
+ let lp = p.add(U_LISTS + l * UPPER_LEVEL_STRIDE);
208
+ let deg = list.len().min(UPPER_CAP);
209
+ (lp as *mut u16).write_unaligned((deg as u16).to_le());
210
+ let base = lp.add(2) as *mut u32;
211
+ for (i, id) in list.iter().take(deg).enumerate() {
212
+ base.add(i).write_unaligned(id.to_le());
213
+ }
214
+ }
215
+ }
216
+ Ok(idx)
217
+ }
218
+
219
+ /// Rewrite an existing upper entry in place (full state). Used by the raw mirroring
220
+ /// path so repeated updates to a high-level node reuse its entry instead of leaking one
221
+ /// per rewrite.
222
+ pub fn rewrite_upper(&self, idx: u32, levels: &[Vec<u32>]) -> Result<(), Wedged> {
223
+ let seq = self.file.upper_seq_atomic(idx);
224
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.upper_sanitizer(idx), self.owner_dead())?;
225
+ let p = self.file.upper_ptr_mut(idx);
226
+ unsafe {
227
+ let n = levels.len().min(MAX_UPPER_LEVELS);
228
+ *p.add(U_LEVELS) = n as u8;
229
+ for (l, list) in levels.iter().take(n).enumerate() {
230
+ let lp = p.add(U_LISTS + l * UPPER_LEVEL_STRIDE);
231
+ let deg = list.len().min(UPPER_CAP);
232
+ (lp as *mut u16).write_unaligned((deg as u16).to_le());
233
+ let base = lp.add(2) as *mut u32;
234
+ for (i, id) in list.iter().take(deg).enumerate() {
235
+ base.add(i).write_unaligned(id.to_le());
236
+ }
237
+ }
238
+ }
239
+ Ok(())
240
+ }
241
+
242
+ /// Whether a slot has ever been written (valid or deleted) — the builder scan's
243
+ /// skip-if-touched check.
244
+ pub fn node_touched(&self, id: u32) -> bool {
245
+ if !self.in_range(id) {
246
+ return false;
247
+ }
248
+ let seq = self.file.seq_atomic(id);
249
+ seqlock::read_consistent(seq, self.file.self_tag, || unsafe { *self.file.slot_ptr(id).add(S_FLAGS) != 0 }, self.slot_sanitizer(id), || true, self.owner_dead())
250
+ }
251
+
252
+ /// The slot's stored upper idx regardless of valid/deleted flags — the raw mirroring
253
+ /// path reuses a cleared node's entry when the host rewrites the same id.
254
+ fn upper_idx_raw(&self, id: u32) -> u32 {
255
+ if !self.in_range(id) {
256
+ return NO_UPPER;
257
+ }
258
+ let seq = self.file.seq_atomic(id);
259
+ seqlock::read_consistent(seq, self.file.self_tag, || {
260
+ let p = self.file.slot_ptr(id);
261
+ unsafe {
262
+ if *p.add(S_FLAGS) == 0 {
263
+ return NO_UPPER; // never written
264
+ }
265
+ (p.add(S_UPPER_IDX) as *const u32).read_unaligned()
266
+ }
267
+ }, self.slot_sanitizer(id), || NO_UPPER, self.owner_dead())
268
+ }
269
+
270
+ /// Mirror a host-maintained node into the plane: full state per call, host-allocated id
271
+ /// (high-water is raised, the plane allocator is bypassed), upper entry reused in place
272
+ /// when present. This is the dual-write phase-1 write path.
273
+ pub fn write_node_raw(
274
+ &self,
275
+ id: u32,
276
+ level: u8,
277
+ vector: &[i8],
278
+ scale: f32,
279
+ inv_mag: f32,
280
+ neighbors: &[u32],
281
+ upper_levels: &[Vec<u32>],
282
+ ) -> Result<(), Wedged> {
283
+ self.file.ensure_high_water(id);
284
+ let existing = match self.upper_idx_raw(id) {
285
+ idx if idx != NO_UPPER && (idx as u64) >= self.file.upper_capacity => NO_UPPER, // corrupt stored index
286
+ idx => idx,
287
+ };
288
+ let upper_idx = if upper_levels.is_empty() {
289
+ existing // keep an existing entry bound (level never shrinks in practice)
290
+ } else if existing != NO_UPPER {
291
+ self.rewrite_upper(existing, upper_levels)?;
292
+ existing
293
+ } else {
294
+ self.write_upper(upper_levels)?
295
+ };
296
+ let mut l0 = neighbors.to_vec();
297
+ l0.truncate(self.file.layer0_cap);
298
+ self.write_node(id, level, vector, scale, inv_mag, &l0, upper_idx)?;
299
+ Ok(())
300
+ }
301
+
302
+ /// Mark deleted WITHOUT returning the id to the plane freelist — dual-write mode, where
303
+ /// the host owns id allocation and may re-mint or reuse ids on its own schedule.
304
+ pub fn clear_node(&self, id: u32) -> Result<(), Wedged> {
305
+ if (id as u64) >= self.file.max_nodes {
306
+ return Ok(());
307
+ }
308
+ // extend the high-water rather than skipping: a delete mirrored while a backfill
309
+ // scan runs must leave a touched (deleted) slot behind, or the scan's older
310
+ // snapshot would resurrect the node when its cursor reaches this id
311
+ self.file.ensure_high_water(id);
312
+ let seq = self.file.seq_atomic(id);
313
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
314
+ let p = self.file.slot_ptr_mut(id);
315
+ unsafe {
316
+ if *p.add(S_FLAGS) == 0 {
317
+ // tombstoning a never-written slot: its zero-initialized upper_idx would
318
+ // otherwise read as the VALID index 0, and a later raw rewrite of this id
319
+ // would clobber upper entry 0 — another node's hierarchy
320
+ (p.add(S_UPPER_IDX) as *mut u32).write_unaligned(NO_UPPER);
321
+ }
322
+ *p.add(S_FLAGS) = FLAG_DELETED;
323
+ }
324
+ Ok(())
325
+ }
326
+
327
+ /// Atomic read-modify-write of `id`'s upper adjacency at `level` (1-based). Returns
328
+ /// false when the node has no entry or level. `f` may read other slots.
329
+ pub fn update_upper_level<F: FnOnce(&mut Vec<u32>)>(&self, id: u32, level: u8, f: F) -> Result<bool, Wedged> {
330
+ let idx = self.upper_idx_of(id);
331
+ if idx == NO_UPPER || (idx as u64) >= self.file.upper_capacity || level as usize > MAX_UPPER_LEVELS {
332
+ return Ok(false);
333
+ }
334
+ let seq = self.file.upper_seq_atomic(idx);
335
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.upper_sanitizer(idx), self.owner_dead())?;
336
+ let p = self.file.upper_ptr_mut(idx);
337
+ unsafe {
338
+ let levels = *p.add(U_LEVELS);
339
+ if level > levels {
340
+ return Ok(false);
341
+ }
342
+ let lp = p.add(U_LISTS + (level as usize - 1) * UPPER_LEVEL_STRIDE);
343
+ let degree = u16::from_le((lp as *const u16).read_unaligned()) as usize;
344
+ let base = lp.add(2) as *mut u32;
345
+ let mut list: Vec<u32> = (0..degree.min(UPPER_CAP)).map(|i| u32::from_le(base.add(i).read_unaligned())).collect();
346
+ f(&mut list);
347
+ list.truncate(UPPER_CAP);
348
+ (lp as *mut u16).write_unaligned((list.len() as u16).to_le());
349
+ for (i, id) in list.iter().enumerate() {
350
+ base.add(i).write_unaligned(id.to_le());
351
+ }
352
+ }
353
+ Ok(true)
354
+ }
355
+
356
+ /// Seqlock-consistent full copy (construction paths).
357
+ pub fn read_node(&self, id: u32) -> Option<NodeRead> {
358
+ if !self.in_range(id) {
359
+ return None;
360
+ }
361
+ let seq = self.file.seq_atomic(id);
362
+ let dims = self.file.dims;
363
+ let cap = self.file.layer0_cap;
364
+ seqlock::read_consistent(seq, self.file.self_tag, || {
365
+ let p = self.file.slot_ptr(id);
366
+ unsafe {
367
+ let flags = *p.add(S_FLAGS);
368
+ if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
369
+ return None;
370
+ }
371
+ let level = *p.add(S_LEVEL);
372
+ let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
373
+ let scale = (p.add(S_SCALE) as *const f32).read_unaligned();
374
+ let inv_mag = (p.add(S_INV_MAG) as *const f32).read_unaligned();
375
+ let vector = std::slice::from_raw_parts(p.add(S_VECTOR) as *const i8, dims).to_vec();
376
+ let nbase = p.add(S_VECTOR + dims) as *const u32;
377
+ let neighbors = (0..degree.min(cap)).map(|i| u32::from_le(nbase.add(i).read_unaligned())).collect();
378
+ Some(NodeRead { level, scale, inv_mag, vector, neighbors })
379
+ }
380
+ }, self.slot_sanitizer(id), || None, self.owner_dead())
381
+ }
382
+
383
+ /// Write a full slot under its seqlock. `neighbors` is pruned to layer0_cap by the
384
+ /// caller; `upper_idx` is a write_upper() result (NO_UPPER for level-0 nodes).
385
+ pub fn write_node(&self, id: u32, level: u8, vector: &[i8], scale: f32, inv_mag: f32, neighbors: &[u32], upper_idx: u32) -> Result<(), Wedged> {
386
+ debug_assert!(neighbors.len() <= self.file.layer0_cap);
387
+ debug_assert_eq!(vector.len(), self.file.dims);
388
+ let seq = self.file.seq_atomic(id);
389
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
390
+ let p = self.file.slot_ptr_mut(id);
391
+ let dims = self.file.dims;
392
+ unsafe {
393
+ *p.add(S_LEVEL) = level;
394
+ (p.add(S_DEGREE) as *mut u16).write_unaligned((neighbors.len() as u16).to_le());
395
+ (p.add(S_SCALE) as *mut f32).write_unaligned(scale);
396
+ (p.add(S_INV_MAG) as *mut f32).write_unaligned(inv_mag);
397
+ (p.add(S_UPPER_IDX) as *mut u32).write_unaligned(upper_idx);
398
+ std::ptr::copy_nonoverlapping(vector.as_ptr() as *const u8, p.add(S_VECTOR), dims);
399
+ for (i, n) in neighbors.iter().enumerate() {
400
+ (p.add(S_VECTOR + dims + i * 4) as *mut u32).write_unaligned(n.to_le());
401
+ }
402
+ // valid last within the locked section; the seqlock release publishes it
403
+ *p.add(S_FLAGS) = FLAG_VALID;
404
+ }
405
+ Ok(())
406
+ }
407
+
408
+ /// Atomic read-modify-write of a node's layer-0 neighbor list under its seqlock.
409
+ /// `f` may read OTHER slots (e.g. distance_between for pruning) — those are plain
410
+ /// unlocked reads, so no lock ordering issue — but must not lock this graph's slots.
411
+ /// Returns false for absent/deleted nodes.
412
+ pub fn update_neighbors<F: FnOnce(&mut Vec<u32>)>(&self, id: u32, f: F) -> Result<bool, Wedged> {
413
+ if !self.in_range(id) {
414
+ return Ok(false);
415
+ }
416
+ let seq = self.file.seq_atomic(id);
417
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
418
+ let p = self.file.slot_ptr_mut(id);
419
+ let dims = self.file.dims;
420
+ let cap = self.file.layer0_cap;
421
+ unsafe {
422
+ let flags = *p.add(S_FLAGS);
423
+ if flags & FLAG_VALID == 0 || flags & FLAG_DELETED != 0 {
424
+ return Ok(false);
425
+ }
426
+ let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
427
+ let base = p.add(S_VECTOR + dims) as *mut u32;
428
+ let mut list: Vec<u32> = (0..degree.min(cap)).map(|i| u32::from_le(base.add(i).read_unaligned())).collect();
429
+ f(&mut list);
430
+ list.truncate(cap);
431
+ (p.add(S_DEGREE) as *mut u16).write_unaligned((list.len() as u16).to_le());
432
+ for (i, n) in list.iter().enumerate() {
433
+ base.add(i).write_unaligned(n.to_le());
434
+ }
435
+ }
436
+ Ok(true)
437
+ }
438
+
439
+ /// Apply a precomputed neighbor list only if the current list still equals `expected` —
440
+ /// the compare and the write share one lock acquisition, so heavy work (distance-based
441
+ /// pruning, which can major-fault) happens OUTSIDE the lock and the critical section
442
+ /// stays microseconds. Returns false when the list changed or the node is gone.
443
+ pub fn set_neighbors_if(&self, id: u32, expected: &[u32], next: &[u32]) -> Result<bool, Wedged> {
444
+ debug_assert!(next.len() <= self.file.layer0_cap);
445
+ if !self.in_range(id) {
446
+ return Ok(false);
447
+ }
448
+ let seq = self.file.seq_atomic(id);
449
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
450
+ let p = self.file.slot_ptr_mut(id);
451
+ let dims = self.file.dims;
452
+ unsafe {
453
+ if *p.add(S_FLAGS) != FLAG_VALID {
454
+ return Ok(false);
455
+ }
456
+ let degree = u16::from_le((p.add(S_DEGREE) as *const u16).read_unaligned()) as usize;
457
+ if degree != expected.len() {
458
+ return Ok(false);
459
+ }
460
+ let base = p.add(S_VECTOR + dims) as *mut u32;
461
+ for (i, want) in expected.iter().enumerate() {
462
+ if u32::from_le(base.add(i).read_unaligned()) != *want {
463
+ return Ok(false);
464
+ }
465
+ }
466
+ (p.add(S_DEGREE) as *mut u16).write_unaligned((next.len() as u16).to_le());
467
+ for (i, n) in next.iter().enumerate() {
468
+ base.add(i).write_unaligned(n.to_le());
469
+ }
470
+ }
471
+ Ok(true)
472
+ }
473
+
474
+ /// Replace only the neighbor list (single-writer construction path).
475
+ pub fn write_neighbors(&self, id: u32, neighbors: &[u32]) -> Result<(), Wedged> {
476
+ debug_assert!(neighbors.len() <= self.file.layer0_cap);
477
+ let seq = self.file.seq_atomic(id);
478
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
479
+ let p = self.file.slot_ptr_mut(id);
480
+ let dims = self.file.dims;
481
+ unsafe {
482
+ (p.add(S_DEGREE) as *mut u16).write_unaligned((neighbors.len() as u16).to_le());
483
+ for (i, n) in neighbors.iter().enumerate() {
484
+ (p.add(S_VECTOR + dims + i * 4) as *mut u32).write_unaligned(n.to_le());
485
+ }
486
+ }
487
+ Ok(())
488
+ }
489
+
490
+ /// Mark deleted (traversals skip it), free its upper entry, and return the id to the
491
+ /// plane freelist. Deleting the current entry point re-elects a replacement — without
492
+ /// that, every search returns empty and every insert orphans itself against the dead
493
+ /// entry.
494
+ pub fn delete_node(&self, id: u32) -> Result<(), Wedged> {
495
+ if !self.in_range(id) {
496
+ return Ok(()); // never-allocated or out-of-range ids have nothing to delete
497
+ }
498
+ // capture neighbors before invalidating: they are the best re-election candidates
499
+ let (entry_id, _) = self.file.entry_point();
500
+ let mut candidates: Vec<u32> = Vec::new();
501
+ if entry_id == id {
502
+ self.neighbors_into(id, &mut candidates);
503
+ }
504
+ let upper_idx;
505
+ {
506
+ let seq = self.file.seq_atomic(id);
507
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
508
+ let p = self.file.slot_ptr_mut(id);
509
+ unsafe {
510
+ if *p.add(S_FLAGS) != FLAG_VALID {
511
+ // deleting a never-written or already-deleted id must not free again:
512
+ // a double-push makes the freelist a self-cycle that hands the same id
513
+ // to every subsequent allocation
514
+ return Ok(());
515
+ }
516
+ upper_idx = (p.add(S_UPPER_IDX) as *const u32).read_unaligned();
517
+ (p.add(S_UPPER_IDX) as *mut u32).write_unaligned(NO_UPPER);
518
+ *p.add(S_FLAGS) = FLAG_DELETED;
519
+ }
520
+ }
521
+ if upper_idx != NO_UPPER && (upper_idx as u64) < self.file.upper_capacity {
522
+ // empty the entry under its own lock BEFORE freeing: a traversal that already
523
+ // read this node's upper_idx must find a dead entry, not one reallocated to a
524
+ // different node mid-read
525
+ self.rewrite_upper(upper_idx, &[])?;
526
+ }
527
+ self.file.free_upper(upper_idx);
528
+ if entry_id == id {
529
+ self.reelect_entry_point_replacing(&candidates, id);
530
+ }
531
+ self.file.free_id(id);
532
+ Ok(())
533
+ }
534
+
535
+ /// Pick a new entry point: the highest-level live node among `preferred`, else the
536
+ /// first live node found scanning the id range (rare path: only when the entry's whole
537
+ /// neighborhood is gone). An empty graph clears the entry.
538
+ /// A node's level without copying its vector or edges (cheap re-election scans).
539
+ fn node_level(&self, id: u32) -> Option<u8> {
540
+ if !self.in_range(id) {
541
+ return None;
542
+ }
543
+ let seq = self.file.seq_atomic(id);
544
+ seqlock::read_consistent(seq, self.file.self_tag, || {
545
+ let p = self.file.slot_ptr(id);
546
+ unsafe {
547
+ if *p.add(S_FLAGS) != FLAG_VALID {
548
+ return None;
549
+ }
550
+ Some(*p.add(S_LEVEL))
551
+ }
552
+ }, self.slot_sanitizer(id), || None, self.owner_dead())
553
+ }
554
+
555
+ /// Pick a new entry point: the highest-level live node among `preferred`, else the
556
+ /// highest-level live node found scanning the id range (level reads only — no per-node
557
+ /// vector copies; still O(high-water), which only runs when an entry point vanished
558
+ /// with no live neighborhood). Preferring level keeps the hierarchy navigable — a
559
+ /// level-0 entry degrades every search to a layer-0-only beam. An empty graph clears
560
+ /// the entry.
561
+ pub(crate) fn reelect_entry_point(&self, preferred: &[u32]) {
562
+ self.reelect_entry_point_replacing(preferred, crate::format::NO_ID)
563
+ }
564
+
565
+ fn reelect_entry_point_replacing(&self, preferred: &[u32], replacing: u32) {
566
+ let mut best: Option<(u32, u8)> = None;
567
+ // the most recently replaced entry point is the best cheap candidate: usually alive,
568
+ // usually high-level — and it makes the full fallback scan a last resort
569
+ let prev = self.file.previous_entry_point();
570
+ if prev != crate::format::NO_ID && prev != replacing {
571
+ if let Some(level) = self.node_level(prev) {
572
+ best = Some((prev, level));
573
+ }
574
+ }
575
+ for &cand in preferred {
576
+ if let Some(level) = self.node_level(cand) {
577
+ if best.map(|(_, l)| level > l).unwrap_or(true) {
578
+ best = Some((cand, level));
579
+ }
580
+ }
581
+ }
582
+ if best.is_none() {
583
+ let hw = self.file.id_high_water().min(self.file.max_nodes) as u32;
584
+ for cand in 0..hw {
585
+ if let Some(level) = self.node_level(cand) {
586
+ if best.map(|(_, l)| level > l).unwrap_or(true) {
587
+ best = Some((cand, level));
588
+ if level as usize >= MAX_UPPER_LEVELS {
589
+ break; // cannot do better
590
+ }
591
+ }
592
+ }
593
+ }
594
+ }
595
+ match best {
596
+ Some((cand, level)) => self.file.set_entry_point_if_not_better(cand, level as u32, replacing),
597
+ None => self.file.set_entry_point_if_not_better(crate::format::NO_ID, 0, replacing),
598
+ }
599
+ }
600
+
601
+ /// write_node, but only when the slot has never been touched — the check and the write
602
+ /// share ONE seqlock acquisition, so a concurrent live mirror's newer write can never be
603
+ /// overwritten by a backfill scan's older snapshot (a two-step check-then-write left
604
+ /// exactly that window). Returns true when this state was written.
605
+ #[allow(clippy::too_many_arguments)]
606
+ pub fn write_node_if_untouched(
607
+ &self,
608
+ id: u32,
609
+ level: u8,
610
+ vector: &[i8],
611
+ scale: f32,
612
+ inv_mag: f32,
613
+ neighbors: &[u32],
614
+ upper_levels: &[Vec<u32>],
615
+ ) -> Result<bool, Wedged> {
616
+ debug_assert!(neighbors.len() <= self.file.layer0_cap);
617
+ debug_assert_eq!(vector.len(), self.file.dims);
618
+ self.file.ensure_high_water(id);
619
+ // the upper entry is allocated before taking the slot lock (allocation is cheap and
620
+ // an unused entry is freed below on the untouched-check failing)
621
+ let upper_idx = if upper_levels.is_empty() { NO_UPPER } else { self.write_upper(upper_levels)? };
622
+ let seq = self.file.seq_atomic(id);
623
+ let written = {
624
+ let _guard = seqlock::write_lock(seq, self.file.self_tag, self.slot_sanitizer(id), self.owner_dead())?;
625
+ let p = self.file.slot_ptr_mut(id);
626
+ let dims = self.file.dims;
627
+ unsafe {
628
+ if *p.add(S_FLAGS) != 0 {
629
+ false
630
+ } else {
631
+ *p.add(S_LEVEL) = level;
632
+ (p.add(S_DEGREE) as *mut u16).write_unaligned((neighbors.len() as u16).to_le());
633
+ (p.add(S_SCALE) as *mut f32).write_unaligned(scale);
634
+ (p.add(S_INV_MAG) as *mut f32).write_unaligned(inv_mag);
635
+ (p.add(S_UPPER_IDX) as *mut u32).write_unaligned(upper_idx);
636
+ std::ptr::copy_nonoverlapping(vector.as_ptr() as *const u8, p.add(S_VECTOR), dims);
637
+ for (i, n) in neighbors.iter().enumerate() {
638
+ (p.add(S_VECTOR + dims + i * 4) as *mut u32).write_unaligned(n.to_le());
639
+ }
640
+ *p.add(S_FLAGS) = FLAG_VALID;
641
+ true
642
+ }
643
+ }
644
+ };
645
+ if !written {
646
+ self.file.free_upper(upper_idx);
647
+ }
648
+ Ok(written)
649
+ }
650
+ }