Skip to main content

subetha_cxc/
shared_linked_list.rs

1//! `SharedLinkedList<T>` - cross-process doubly-linked list with
2//! O(1) handle-based removal.
3//!
4//! Backed by [`SharedRegion`] of `Node<T>`.
5//! Each node stores T plus raw `next` and `prev` slot indices.
6//! Slot 0 is the sentinel head; real nodes live in slots 1..N.
7//!
8//! # Architectural value: stable handles
9//!
10//! Push operations return a [`NodeHandle`] - a stable u32 slot
11//! index. Callers retain handles to call `remove(handle)` for O(1)
12//! middle removal. This is the unique value over
13//! [`SharedRing`](crate::SharedRing) (FIFO only, no random access)
14//! and [`SharedVec`](crate::SharedVec) (no middle removal).
15//!
16//! # Handle validity contract
17//!
18//! A NodeHandle is valid from the moment push returns it until the
19//! caller invokes `remove(handle)` or one of the pop operations on
20//! that node. Using a stale handle after the node has been removed
21//! is a logic error (the slot may have been reused by a subsequent
22//! push). This is the same contract as `std::list::iterator` in
23//! C++.
24//!
25//! # Concurrency model
26//!
27//! SINGLE-WRITER, MULTI-READER. push / pop / remove / set require
28//! external serialisation (wrap in a [`SharedSemaphore`](
29//! crate::SharedSemaphore) with 1 permit, or use the application's
30//! own coordination). Iteration is lock-free.
31
32use std::marker::PhantomData;
33use std::path::Path;
34
35use crate::shared_region::{OffsetPtr, RegionError, SharedRegion};
36
37/// Sentinel NIL value for next/prev pointers.
38pub const NIL_INDEX: u32 = u32::MAX;
39
40/// Slot index of the sentinel head node within the underlying
41/// SharedRegion. Always 0; allocated at create time.
42pub const HEAD_INDEX: u32 = 0;
43
44#[derive(Debug)]
45#[repr(C)]
46pub struct Node<T: Copy + Default + 'static> {
47    pub value: T,
48    pub next: u32,
49    pub prev: u32,
50}
51
52// Manual impls so we don't need `T: Clone` (Copy implies it but
53// derive(Clone) requires it written explicitly).
54impl<T: Copy + Default + 'static> Clone for Node<T> {
55    fn clone(&self) -> Self { *self }
56}
57impl<T: Copy + Default + 'static> Copy for Node<T> {}
58
59/// Stable handle to a list node. Valid until removed.
60#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
61#[repr(C)]
62pub struct NodeHandle<T> {
63    pub index: u32,
64    _phantom: PhantomData<T>,
65}
66
67impl<T> NodeHandle<T> {
68    pub const NIL: Self = Self { index: NIL_INDEX, _phantom: PhantomData };
69
70    #[inline]
71    pub fn new(index: u32) -> Self {
72        Self { index, _phantom: PhantomData }
73    }
74
75    #[inline]
76    pub fn is_nil(self) -> bool { self.index == NIL_INDEX }
77}
78
79#[derive(Debug, Clone, Copy, PartialEq, Eq)]
80pub enum LinkedListError {
81    Region(RegionError),
82    InvalidHandle,
83    LayoutMismatch,
84    IoError(std::io::ErrorKind),
85}
86
87impl From<RegionError> for LinkedListError {
88    fn from(e: RegionError) -> Self { Self::Region(e) }
89}
90impl From<std::io::Error> for LinkedListError {
91    fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
92}
93
94pub struct SharedLinkedList<T: Copy + Default + 'static> {
95    region: SharedRegion<Node<T>>,
96    /// Cached absolute base of the node-slot array (mmap_ptr + header +
97    /// per-slot guard array), computed once at create/open. Lets the
98    /// link-update paths read/write only the `next`/`prev`/`value` field
99    /// they touch instead of copying the whole Node through
100    /// region.get/region.set. The MMF mapping is fixed for the handle's
101    /// lifetime, so the address is stable across moves.
102    slots_base: usize,
103    _phantom: PhantomData<T>,
104    header_sidecar: subetha_core::HandshakeHeader,
105    ring_sidecar: Box<subetha_core::ObservationRing>,
106}
107
108impl<T: Copy + Default + Send + Sync + 'static>
109    subetha_sidecar::AdaptiveInstance for SharedLinkedList<T>
110{
111    fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
112    fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
113    fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
114        Box::new(subetha_sidecar::NoMigrationPolicy)
115    }
116}
117
118impl<T: Copy + Default + 'static> SharedLinkedList<T> {
119    pub fn create(
120        path: impl AsRef<Path>, capacity: usize,
121    ) -> Result<Self, LinkedListError> {
122        assert!(capacity >= 2, "capacity must include sentinel head + at least one node");
123        let region = SharedRegion::<Node<T>>::create(path, capacity)?;
124        // Allocate the sentinel head at slot 0. next and prev both
125        // point at HEAD (self) so an empty list is a 1-element ring.
126        let head = Node {
127            value: T::default(),
128            next: HEAD_INDEX,
129            prev: HEAD_INDEX,
130        };
131        let head_ptr = region.allocate(head)?;
132        assert_eq!(head_ptr.index, HEAD_INDEX,
133            "first allocation must be slot 0");
134        let slots_base = Self::slots_base_of(&region);
135        Ok(Self {
136            region, slots_base, _phantom: PhantomData,
137            header_sidecar: subetha_core::HandshakeHeader::new(),
138            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
139        })
140    }
141
142    pub fn open(
143        path: impl AsRef<Path>, capacity: usize,
144    ) -> Result<Self, LinkedListError> {
145        let region = SharedRegion::<Node<T>>::open(path, capacity)?;
146        let slots_base = Self::slots_base_of(&region);
147        Ok(Self {
148            region, slots_base, _phantom: PhantomData,
149            header_sidecar: subetha_core::HandshakeHeader::new(),
150            ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
151        })
152    }
153
154    /// Absolute base address of the node-slot array within the region's
155    /// MMF (after the RegionHeader and the per-slot atomic-guard array).
156    #[inline]
157    fn slots_base_of(region: &SharedRegion<Node<T>>) -> usize {
158        region.mmap_ptr() as usize
159            + std::mem::size_of::<crate::shared_region::RegionHeader>()
160            + region.capacity() * std::mem::size_of::<u32>()
161    }
162
163    #[inline]
164    fn node_ptr(&self, idx: u32) -> usize {
165        self.slots_base + idx as usize * std::mem::size_of::<Node<T>>()
166    }
167
168    /// Field-direct reads/writes of a node's `value`/`next`/`prev`. The
169    /// link-update paths only touch one field, so this avoids copying the
170    /// whole Node through region.get/region.set. Non-atomic, matching the
171    /// existing model (writers serialise externally; a concurrent reader
172    /// sees old-or-new for a word-sized field, never a torn whole node).
173    #[inline]
174    fn read_value(&self, idx: u32) -> T {
175        let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, value);
176        unsafe { (addr as *const T).read() }
177    }
178    #[inline]
179    fn read_next(&self, idx: u32) -> u32 {
180        let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, next);
181        unsafe { (addr as *const u32).read() }
182    }
183    #[inline]
184    fn read_prev(&self, idx: u32) -> u32 {
185        let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, prev);
186        unsafe { (addr as *const u32).read() }
187    }
188    #[inline]
189    fn set_next(&self, idx: u32, value: u32) {
190        let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, next);
191        unsafe { (addr as *mut u32).write(value); }
192    }
193    #[inline]
194    fn set_prev(&self, idx: u32, value: u32) {
195        let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, prev);
196        unsafe { (addr as *mut u32).write(value); }
197    }
198    #[inline]
199    fn set_value(&self, idx: u32, value: T) {
200        let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, value);
201        unsafe { (addr as *mut T).write(value); }
202    }
203
204    #[inline]
205    pub fn capacity(&self) -> usize { self.region.capacity() }
206
207    /// Approximate node count (excludes the sentinel head).
208    pub fn len(&self) -> usize {
209        self.region.len().saturating_sub(1)
210    }
211
212    pub fn is_empty(&self) -> bool { self.len() == 0 }
213
214    /// Access the underlying region (advanced use; e.g., to wrap
215    /// writes in a SharedSemaphore for cross-process serialisation).
216    pub fn region(&self) -> &SharedRegion<Node<T>> { &self.region }
217
218    fn read_node(&self, idx: u32) -> Node<T> {
219        self.region.get(OffsetPtr::new(idx))
220            .expect("valid node index")
221    }
222
223    /// Push to the front of the list. Returns a stable NodeHandle.
224    pub fn push_front(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
225        let r = self.push_front_inner(value);
226        self.ring_sidecar.push_op(
227            crate::sidecar_ops::linked_list::OP_PUSH_FRONT,
228            if r.is_err() { 1 } else { 0 },
229        );
230        r
231    }
232
233    fn push_front_inner(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
234        let old_first = self.read_next(HEAD_INDEX);
235        let new = Node {
236            value,
237            next: old_first,
238            prev: HEAD_INDEX,
239        };
240        let new_ptr = self.region.allocate(new)?;
241        let new_idx = new_ptr.index;
242        // Link the successor's `prev` to the new node: the old first node,
243        // or the head itself when the list was empty.
244        if old_first != HEAD_INDEX {
245            self.set_prev(old_first, new_idx);
246        } else {
247            self.set_prev(HEAD_INDEX, new_idx);
248        }
249        self.set_next(HEAD_INDEX, new_idx);
250        Ok(NodeHandle::new(new_idx))
251    }
252
253    /// Push to the back of the list. Returns a stable NodeHandle.
254    pub fn push_back(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
255        let r = self.push_back_inner(value);
256        self.ring_sidecar.push_op(
257            crate::sidecar_ops::linked_list::OP_PUSH_BACK,
258            if r.is_err() { 1 } else { 0 },
259        );
260        r
261    }
262
263    fn push_back_inner(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
264        let old_last = self.read_prev(HEAD_INDEX);
265        let new = Node {
266            value,
267            next: HEAD_INDEX,
268            prev: old_last,
269        };
270        let new_ptr = self.region.allocate(new)?;
271        let new_idx = new_ptr.index;
272        // Link the predecessor's `next` to the new node: the old last
273        // node, or the head itself when the list was empty.
274        if old_last != HEAD_INDEX {
275            self.set_next(old_last, new_idx);
276        } else {
277            self.set_next(HEAD_INDEX, new_idx);
278        }
279        self.set_prev(HEAD_INDEX, new_idx);
280        Ok(NodeHandle::new(new_idx))
281    }
282
283    /// Remove and return the first element.
284    pub fn pop_front(&self) -> Option<T> {
285        let first_idx = self.read_next(HEAD_INDEX);
286        if first_idx == HEAD_INDEX {
287            self.ring_sidecar
288                .push_op(crate::sidecar_ops::linked_list::OP_POP_FRONT, 2); // empty
289            return None;
290        }
291        let r = self.remove_by_index(first_idx);
292        self.ring_sidecar.push_op(
293            crate::sidecar_ops::linked_list::OP_POP_FRONT,
294            if r.is_none() { 2 } else { 0 },
295        );
296        r
297    }
298
299    /// Remove and return the last element.
300    pub fn pop_back(&self) -> Option<T> {
301        let head = self.read_node(HEAD_INDEX);
302        if head.prev == HEAD_INDEX {
303            self.ring_sidecar
304                .push_op(crate::sidecar_ops::linked_list::OP_POP_BACK, 2); // empty
305            return None;
306        }
307        let last_idx = head.prev;
308        let r = self.remove_by_index(last_idx);
309        self.ring_sidecar.push_op(
310            crate::sidecar_ops::linked_list::OP_POP_BACK,
311            if r.is_none() { 2 } else { 0 },
312        );
313        r
314    }
315
316    /// Remove the node referenced by `handle` in O(1) (splice +
317    /// free the slot). Returns the removed value or None when the
318    /// handle is the sentinel head OR was already freed.
319    pub fn remove(&self, handle: NodeHandle<T>) -> Option<T> {
320        if handle.is_nil() || handle.index == HEAD_INDEX {
321            self.ring_sidecar
322                .push_op(crate::sidecar_ops::linked_list::OP_REMOVE, 2); // absent (nil/head)
323            return None;
324        }
325        let r = self.remove_by_index(handle.index);
326        self.ring_sidecar.push_op(
327            crate::sidecar_ops::linked_list::OP_REMOVE,
328            if r.is_none() { 2 } else { 0 },
329        );
330        r
331    }
332
333    fn remove_by_index(&self, idx: u32) -> Option<T> {
334        // Read only the three fields of the node being spliced out, not
335        // the whole node, and repoint neighbours field-direct.
336        let node_prev = self.read_prev(idx);
337        let node_next = self.read_next(idx);
338        let node_value = self.read_value(idx);
339        // Predecessor side: link prev.next -> node.next.
340        if node_prev == HEAD_INDEX {
341            self.set_next(HEAD_INDEX, node_next);
342            if node_next == HEAD_INDEX {
343                self.set_prev(HEAD_INDEX, HEAD_INDEX);
344            }
345        } else {
346            self.set_next(node_prev, node_next);
347        }
348        // Successor side: link next.prev -> node.prev.
349        if node_next == HEAD_INDEX {
350            self.set_prev(HEAD_INDEX, node_prev);
351            if node_prev == HEAD_INDEX {
352                self.set_next(HEAD_INDEX, HEAD_INDEX);
353            }
354        } else {
355            self.set_prev(node_next, node_prev);
356        }
357        self.region.free(OffsetPtr::new(idx)).ok();
358        Some(node_value)
359    }
360
361    /// Read the value at `handle`. Returns None for nil or stale
362    /// handle (after remove).
363    pub fn get(&self, handle: NodeHandle<T>) -> Option<T> {
364        let r = if handle.is_nil() || handle.index == HEAD_INDEX {
365            None
366        } else {
367            Some(self.read_value(handle.index))
368        };
369        self.ring_sidecar.push_op(
370            crate::sidecar_ops::linked_list::OP_ITER,
371            if r.is_none() { 2 } else { 0 },
372        );
373        r
374    }
375
376    /// Overwrite the value at `handle` (keeping the node's position
377    /// in the list).
378    pub fn set(&self, handle: NodeHandle<T>, value: T) -> Result<(), LinkedListError> {
379        if handle.is_nil() || handle.index == HEAD_INDEX {
380            self.ring_sidecar
381                .push_op(crate::sidecar_ops::linked_list::OP_PUSH_BACK, 1); // invalid handle (positional write rejected)
382            return Err(LinkedListError::InvalidHandle);
383        }
384        self.set_value(handle.index, value);
385        self.ring_sidecar
386            .push_op(crate::sidecar_ops::linked_list::OP_PUSH_BACK, 0);
387        Ok(())
388    }
389
390    /// First node's value (None if empty).
391    pub fn first(&self) -> Option<T> {
392        let head = self.read_node(HEAD_INDEX);
393        let r = if head.next == HEAD_INDEX {
394            None
395        } else {
396            Some(self.read_node(head.next).value)
397        };
398        self.ring_sidecar.push_op(
399            crate::sidecar_ops::linked_list::OP_ITER,
400            if r.is_none() { 2 } else { 0 },
401        );
402        r
403    }
404
405    /// Last node's value (None if empty).
406    pub fn last(&self) -> Option<T> {
407        let head = self.read_node(HEAD_INDEX);
408        let r = if head.prev == HEAD_INDEX {
409            None
410        } else {
411            Some(self.read_node(head.prev).value)
412        };
413        self.ring_sidecar.push_op(
414            crate::sidecar_ops::linked_list::OP_ITER,
415            if r.is_none() { 2 } else { 0 },
416        );
417        r
418    }
419
420    /// Snapshot of all values in forward order.
421    pub fn iter_forward(&self) -> Vec<T> {
422        let mut out = Vec::with_capacity(self.len());
423        let mut cur = self.read_node(HEAD_INDEX).next;
424        while cur != HEAD_INDEX {
425            let node = self.read_node(cur);
426            out.push(node.value);
427            cur = node.next;
428        }
429        self.ring_sidecar
430            .push_op(crate::sidecar_ops::linked_list::OP_ITER, 0);
431        out
432    }
433
434    /// Snapshot of all values in reverse order (back to front).
435    pub fn iter_backward(&self) -> Vec<T> {
436        let mut out = Vec::with_capacity(self.len());
437        let mut cur = self.read_node(HEAD_INDEX).prev;
438        while cur != HEAD_INDEX {
439            let node = self.read_node(cur);
440            out.push(node.value);
441            cur = node.prev;
442        }
443        self.ring_sidecar
444            .push_op(crate::sidecar_ops::linked_list::OP_ITER, 0);
445        out
446    }
447
448    /// Snapshot of all (handle, value) pairs in forward order.
449    /// Useful for finding a handle by predicate, then removing it.
450    pub fn iter_forward_with_handles(&self) -> Vec<(NodeHandle<T>, T)> {
451        let mut out = Vec::with_capacity(self.len());
452        let mut cur = self.read_node(HEAD_INDEX).next;
453        while cur != HEAD_INDEX {
454            let node = self.read_node(cur);
455            out.push((NodeHandle::new(cur), node.value));
456            cur = node.next;
457        }
458        self.ring_sidecar
459            .push_op(crate::sidecar_ops::linked_list::OP_ITER, 0);
460        out
461    }
462
463    pub fn flush(&self) -> Result<(), LinkedListError> {
464        Ok(self.region.flush()?)
465    }
466
467    pub fn flush_async(&self) -> Result<(), LinkedListError> {
468        Ok(self.region.flush_async()?)
469    }
470}
471
472#[cfg(test)]
473mod tests {
474    use super::*;
475
476    fn tmp(name: &str) -> std::path::PathBuf {
477        let mut p = std::env::temp_dir();
478        let pid = std::process::id();
479        p.push(format!("subetha-linkedlist-{name}-{pid}.bin"));
480        p
481    }
482
483    #[test]
484    fn create_initial_state_is_empty() {
485        let p = tmp("init");
486        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
487        assert!(l.is_empty());
488        assert_eq!(l.len(), 0);
489        assert_eq!(l.first(), None);
490        assert_eq!(l.last(), None);
491        assert_eq!(l.iter_forward(), Vec::<u32>::new());
492        std::fs::remove_file(&p).ok();
493    }
494
495    #[test]
496    fn push_back_and_iterate_forward() {
497        let p = tmp("push-back");
498        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
499        for i in [10u32, 20, 30, 40, 50] { l.push_back(i).unwrap(); }
500        assert_eq!(l.len(), 5);
501        assert_eq!(l.iter_forward(), vec![10, 20, 30, 40, 50]);
502        assert_eq!(l.iter_backward(), vec![50, 40, 30, 20, 10]);
503        assert_eq!(l.first(), Some(10));
504        assert_eq!(l.last(), Some(50));
505        std::fs::remove_file(&p).ok();
506    }
507
508    #[test]
509    fn push_front_inserts_at_head() {
510        let p = tmp("push-front");
511        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
512        for i in [10u32, 20, 30] { l.push_front(i).unwrap(); }
513        assert_eq!(l.iter_forward(), vec![30, 20, 10]);
514        std::fs::remove_file(&p).ok();
515    }
516
517    #[test]
518    fn pop_front_and_back_round_trip() {
519        let p = tmp("pop");
520        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
521        for i in [10u32, 20, 30, 40] { l.push_back(i).unwrap(); }
522        assert_eq!(l.pop_front(), Some(10));
523        assert_eq!(l.pop_back(), Some(40));
524        assert_eq!(l.iter_forward(), vec![20, 30]);
525        assert_eq!(l.pop_front(), Some(20));
526        assert_eq!(l.pop_back(), Some(30));
527        assert!(l.is_empty());
528        assert_eq!(l.pop_front(), None);
529        assert_eq!(l.pop_back(), None);
530        std::fs::remove_file(&p).ok();
531    }
532
533    #[test]
534    fn remove_by_handle_in_middle_preserves_integrity() {
535        let p = tmp("remove-middle");
536        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
537        let h1 = l.push_back(10).unwrap();
538        let h2 = l.push_back(20).unwrap();
539        let h3 = l.push_back(30).unwrap();
540        let h4 = l.push_back(40).unwrap();
541        let h5 = l.push_back(50).unwrap();
542        // Remove the middle one.
543        assert_eq!(l.remove(h3), Some(30));
544        assert_eq!(l.len(), 4);
545        assert_eq!(l.iter_forward(), vec![10, 20, 40, 50]);
546        assert_eq!(l.iter_backward(), vec![50, 40, 20, 10]);
547        // Remove the head.
548        assert_eq!(l.remove(h1), Some(10));
549        assert_eq!(l.iter_forward(), vec![20, 40, 50]);
550        // Remove the tail.
551        assert_eq!(l.remove(h5), Some(50));
552        assert_eq!(l.iter_forward(), vec![20, 40]);
553        // Remove remaining via handles.
554        assert_eq!(l.remove(h2), Some(20));
555        assert_eq!(l.remove(h4), Some(40));
556        assert!(l.is_empty());
557        std::fs::remove_file(&p).ok();
558    }
559
560    #[test]
561    fn remove_nil_or_head_returns_none() {
562        let p = tmp("remove-nil");
563        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 8).unwrap();
564        l.push_back(1).unwrap();
565        assert_eq!(l.remove(NodeHandle::NIL), None);
566        assert_eq!(l.remove(NodeHandle::new(HEAD_INDEX)), None);
567        std::fs::remove_file(&p).ok();
568    }
569
570    #[test]
571    fn get_and_set_via_handle() {
572        let p = tmp("get-set");
573        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 8).unwrap();
574        let h = l.push_back(42).unwrap();
575        assert_eq!(l.get(h), Some(42));
576        l.set(h, 100).unwrap();
577        assert_eq!(l.get(h), Some(100));
578        assert_eq!(l.iter_forward(), vec![100]);
579        // get on NIL.
580        assert_eq!(l.get(NodeHandle::NIL), None);
581        // set on NIL.
582        assert!(l.set(NodeHandle::NIL, 0).is_err());
583        std::fs::remove_file(&p).ok();
584    }
585
586    #[test]
587    fn full_capacity_returns_error() {
588        let p = tmp("full");
589        // capacity 4 = head + 3 nodes.
590        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 4).unwrap();
591        l.push_back(1).unwrap();
592        l.push_back(2).unwrap();
593        l.push_back(3).unwrap();
594        assert!(l.push_back(4).is_err());
595        // After popping, can push again.
596        l.pop_front().unwrap();
597        l.push_back(4).unwrap();
598        assert_eq!(l.iter_forward(), vec![2, 3, 4]);
599        std::fs::remove_file(&p).ok();
600    }
601
602    #[test]
603    fn iter_forward_with_handles_returns_pairs() {
604        let p = tmp("iter-handles");
605        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
606        let h1 = l.push_back(10).unwrap();
607        let h2 = l.push_back(20).unwrap();
608        let h3 = l.push_back(30).unwrap();
609        let pairs = l.iter_forward_with_handles();
610        assert_eq!(pairs.len(), 3);
611        assert_eq!(pairs[0], (h1, 10));
612        assert_eq!(pairs[1], (h2, 20));
613        assert_eq!(pairs[2], (h3, 30));
614        std::fs::remove_file(&p).ok();
615    }
616
617    #[test]
618    fn cross_handle_visibility() {
619        let p = tmp("cross-handle");
620        let writer: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
621        let reader: SharedLinkedList<u32> = SharedLinkedList::open(&p, 16).unwrap();
622        writer.push_back(100).unwrap();
623        writer.push_back(200).unwrap();
624        assert_eq!(reader.iter_forward(), vec![100, 200]);
625        let h = writer.push_back(300).unwrap();
626        assert_eq!(reader.iter_forward(), vec![100, 200, 300]);
627        writer.remove(h);
628        assert_eq!(reader.iter_forward(), vec![100, 200]);
629        std::fs::remove_file(&p).ok();
630    }
631
632    #[test]
633    fn struct_payload_round_trip() {
634        #[derive(Clone, Copy, Debug, PartialEq, Default)]
635        #[repr(C)]
636        struct Event { ts_us: u64, code: u32 }
637        let p = tmp("struct");
638        let l: SharedLinkedList<Event> = SharedLinkedList::create(&p, 16).unwrap();
639        let h1 = l.push_back(Event { ts_us: 100, code: 1 }).unwrap();
640        let _h2 = l.push_back(Event { ts_us: 200, code: 2 }).unwrap();
641        assert_eq!(l.get(h1), Some(Event { ts_us: 100, code: 1 }));
642        let items = l.iter_forward();
643        assert_eq!(items.len(), 2);
644        std::fs::remove_file(&p).ok();
645    }
646
647    #[test]
648    fn disk_persistence_survives_reopen() {
649        let p = tmp("disk");
650        {
651            let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
652            for i in [10u32, 20, 30, 40] { l.push_back(i).unwrap(); }
653            l.flush().unwrap();
654        }
655        let l2: SharedLinkedList<u32> = SharedLinkedList::open(&p, 16).unwrap();
656        assert_eq!(l2.iter_forward(), vec![10, 20, 30, 40]);
657        std::fs::remove_file(&p).ok();
658    }
659
660    #[test]
661    fn lru_pattern_move_to_front() {
662        // Realistic LRU pattern: when a key is accessed, remove its
663        // node and re-push at the front. Demonstrates O(1) handle-
664        // based remove + push_front composition.
665        let p = tmp("lru");
666        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
667        let h_a = l.push_back(1).unwrap();  // LRU=1
668        let _h_b = l.push_back(2).unwrap();
669        let _h_c = l.push_back(3).unwrap();  // MRU=3
670        // "Access" key A: move to front.
671        let val_a = l.remove(h_a).unwrap();
672        l.push_front(val_a).unwrap();
673        // Now order is A, C... wait, push_front so A is first; then
674        // existing order continues.
675        assert_eq!(l.iter_forward(), vec![1, 2, 3]);
676        // After move-to-front, A is now MRU. Pop_back evicts
677        // the LRU.
678        assert_eq!(l.pop_back(), Some(3));  // wait, last was 3
679        std::fs::remove_file(&p).ok();
680    }
681
682    #[test]
683    fn free_list_pattern_uses_handles() {
684        // Realistic free-list pattern: callers hold handles to their
685        // own work items in a shared list; release returns the value.
686        let p = tmp("free-list");
687        let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
688        let mut handles = vec![];
689        for i in 0..10u32 { handles.push(l.push_back(i).unwrap()); }
690        // Release every other one.
691        for (idx, h) in handles.iter().enumerate() {
692            if idx % 2 == 0 {
693                let v = l.remove(*h).unwrap();
694                assert_eq!(v, idx as u32);
695            }
696        }
697        assert_eq!(l.len(), 5);
698        assert_eq!(l.iter_forward(), vec![1, 3, 5, 7, 9]);
699        std::fs::remove_file(&p).ok();
700    }
701}