use std::marker::PhantomData;
use std::path::Path;
use crate::shared_region::{OffsetPtr, RegionError, SharedRegion};
pub const NIL_INDEX: u32 = u32::MAX;
pub const HEAD_INDEX: u32 = 0;
#[derive(Debug)]
#[repr(C)]
pub struct Node<T: Copy + Default + 'static> {
pub value: T,
pub next: u32,
pub prev: u32,
}
impl<T: Copy + Default + 'static> Clone for Node<T> {
fn clone(&self) -> Self { *self }
}
impl<T: Copy + Default + 'static> Copy for Node<T> {}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(C)]
pub struct NodeHandle<T> {
pub index: u32,
_phantom: PhantomData<T>,
}
impl<T> NodeHandle<T> {
pub const NIL: Self = Self { index: NIL_INDEX, _phantom: PhantomData };
#[inline]
pub fn new(index: u32) -> Self {
Self { index, _phantom: PhantomData }
}
#[inline]
pub fn is_nil(self) -> bool { self.index == NIL_INDEX }
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum LinkedListError {
Region(RegionError),
InvalidHandle,
LayoutMismatch,
IoError(std::io::ErrorKind),
}
impl From<RegionError> for LinkedListError {
fn from(e: RegionError) -> Self { Self::Region(e) }
}
impl From<std::io::Error> for LinkedListError {
fn from(e: std::io::Error) -> Self { Self::IoError(e.kind()) }
}
pub struct SharedLinkedList<T: Copy + Default + 'static> {
region: SharedRegion<Node<T>>,
slots_base: usize,
_phantom: PhantomData<T>,
header_sidecar: subetha_core::HandshakeHeader,
ring_sidecar: Box<subetha_core::ObservationRing>,
}
impl<T: Copy + Default + Send + Sync + 'static>
subetha_sidecar::AdaptiveInstance for SharedLinkedList<T>
{
fn header(&self) -> &subetha_core::HandshakeHeader { &self.header_sidecar }
fn ring(&self) -> &subetha_core::ObservationRing { &self.ring_sidecar }
fn make_policy(&self) -> Box<dyn subetha_sidecar::Policy> {
Box::new(subetha_sidecar::NoMigrationPolicy)
}
}
impl<T: Copy + Default + 'static> SharedLinkedList<T> {
pub fn create(
path: impl AsRef<Path>, capacity: usize,
) -> Result<Self, LinkedListError> {
assert!(capacity >= 2, "capacity must include sentinel head + at least one node");
let region = SharedRegion::<Node<T>>::create(path, capacity)?;
let head = Node {
value: T::default(),
next: HEAD_INDEX,
prev: HEAD_INDEX,
};
let head_ptr = region.allocate(head)?;
assert_eq!(head_ptr.index, HEAD_INDEX,
"first allocation must be slot 0");
let slots_base = Self::slots_base_of(®ion);
Ok(Self {
region, slots_base, _phantom: PhantomData,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
pub fn open(
path: impl AsRef<Path>, capacity: usize,
) -> Result<Self, LinkedListError> {
let region = SharedRegion::<Node<T>>::open(path, capacity)?;
let slots_base = Self::slots_base_of(®ion);
Ok(Self {
region, slots_base, _phantom: PhantomData,
header_sidecar: subetha_core::HandshakeHeader::new(),
ring_sidecar: Box::new(subetha_core::ObservationRing::new()),
})
}
#[inline]
fn slots_base_of(region: &SharedRegion<Node<T>>) -> usize {
region.mmap_ptr() as usize
+ std::mem::size_of::<crate::shared_region::RegionHeader>()
+ region.capacity() * std::mem::size_of::<u32>()
}
#[inline]
fn node_ptr(&self, idx: u32) -> usize {
self.slots_base + idx as usize * std::mem::size_of::<Node<T>>()
}
#[inline]
fn read_value(&self, idx: u32) -> T {
let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, value);
unsafe { (addr as *const T).read() }
}
#[inline]
fn read_next(&self, idx: u32) -> u32 {
let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, next);
unsafe { (addr as *const u32).read() }
}
#[inline]
fn read_prev(&self, idx: u32) -> u32 {
let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, prev);
unsafe { (addr as *const u32).read() }
}
#[inline]
fn set_next(&self, idx: u32, value: u32) {
let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, next);
unsafe { (addr as *mut u32).write(value); }
}
#[inline]
fn set_prev(&self, idx: u32, value: u32) {
let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, prev);
unsafe { (addr as *mut u32).write(value); }
}
#[inline]
fn set_value(&self, idx: u32, value: T) {
let addr = self.node_ptr(idx) + std::mem::offset_of!(Node<T>, value);
unsafe { (addr as *mut T).write(value); }
}
#[inline]
pub fn capacity(&self) -> usize { self.region.capacity() }
pub fn len(&self) -> usize {
self.region.len().saturating_sub(1)
}
pub fn is_empty(&self) -> bool { self.len() == 0 }
pub fn region(&self) -> &SharedRegion<Node<T>> { &self.region }
fn read_node(&self, idx: u32) -> Node<T> {
self.region.get(OffsetPtr::new(idx))
.expect("valid node index")
}
pub fn push_front(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
let r = self.push_front_inner(value);
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_PUSH_FRONT,
if r.is_err() { 1 } else { 0 },
);
r
}
fn push_front_inner(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
let old_first = self.read_next(HEAD_INDEX);
let new = Node {
value,
next: old_first,
prev: HEAD_INDEX,
};
let new_ptr = self.region.allocate(new)?;
let new_idx = new_ptr.index;
if old_first != HEAD_INDEX {
self.set_prev(old_first, new_idx);
} else {
self.set_prev(HEAD_INDEX, new_idx);
}
self.set_next(HEAD_INDEX, new_idx);
Ok(NodeHandle::new(new_idx))
}
pub fn push_back(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
let r = self.push_back_inner(value);
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_PUSH_BACK,
if r.is_err() { 1 } else { 0 },
);
r
}
fn push_back_inner(&self, value: T) -> Result<NodeHandle<T>, LinkedListError> {
let old_last = self.read_prev(HEAD_INDEX);
let new = Node {
value,
next: HEAD_INDEX,
prev: old_last,
};
let new_ptr = self.region.allocate(new)?;
let new_idx = new_ptr.index;
if old_last != HEAD_INDEX {
self.set_next(old_last, new_idx);
} else {
self.set_next(HEAD_INDEX, new_idx);
}
self.set_prev(HEAD_INDEX, new_idx);
Ok(NodeHandle::new(new_idx))
}
pub fn pop_front(&self) -> Option<T> {
let first_idx = self.read_next(HEAD_INDEX);
if first_idx == HEAD_INDEX {
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_POP_FRONT, 2); return None;
}
let r = self.remove_by_index(first_idx);
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_POP_FRONT,
if r.is_none() { 2 } else { 0 },
);
r
}
pub fn pop_back(&self) -> Option<T> {
let head = self.read_node(HEAD_INDEX);
if head.prev == HEAD_INDEX {
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_POP_BACK, 2); return None;
}
let last_idx = head.prev;
let r = self.remove_by_index(last_idx);
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_POP_BACK,
if r.is_none() { 2 } else { 0 },
);
r
}
pub fn remove(&self, handle: NodeHandle<T>) -> Option<T> {
if handle.is_nil() || handle.index == HEAD_INDEX {
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_REMOVE, 2); return None;
}
let r = self.remove_by_index(handle.index);
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_REMOVE,
if r.is_none() { 2 } else { 0 },
);
r
}
fn remove_by_index(&self, idx: u32) -> Option<T> {
let node_prev = self.read_prev(idx);
let node_next = self.read_next(idx);
let node_value = self.read_value(idx);
if node_prev == HEAD_INDEX {
self.set_next(HEAD_INDEX, node_next);
if node_next == HEAD_INDEX {
self.set_prev(HEAD_INDEX, HEAD_INDEX);
}
} else {
self.set_next(node_prev, node_next);
}
if node_next == HEAD_INDEX {
self.set_prev(HEAD_INDEX, node_prev);
if node_prev == HEAD_INDEX {
self.set_next(HEAD_INDEX, HEAD_INDEX);
}
} else {
self.set_prev(node_next, node_prev);
}
self.region.free(OffsetPtr::new(idx)).ok();
Some(node_value)
}
pub fn get(&self, handle: NodeHandle<T>) -> Option<T> {
let r = if handle.is_nil() || handle.index == HEAD_INDEX {
None
} else {
Some(self.read_value(handle.index))
};
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_ITER,
if r.is_none() { 2 } else { 0 },
);
r
}
pub fn set(&self, handle: NodeHandle<T>, value: T) -> Result<(), LinkedListError> {
if handle.is_nil() || handle.index == HEAD_INDEX {
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_PUSH_BACK, 1); return Err(LinkedListError::InvalidHandle);
}
self.set_value(handle.index, value);
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_PUSH_BACK, 0);
Ok(())
}
pub fn first(&self) -> Option<T> {
let head = self.read_node(HEAD_INDEX);
let r = if head.next == HEAD_INDEX {
None
} else {
Some(self.read_node(head.next).value)
};
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_ITER,
if r.is_none() { 2 } else { 0 },
);
r
}
pub fn last(&self) -> Option<T> {
let head = self.read_node(HEAD_INDEX);
let r = if head.prev == HEAD_INDEX {
None
} else {
Some(self.read_node(head.prev).value)
};
self.ring_sidecar.push_op(
crate::sidecar_ops::linked_list::OP_ITER,
if r.is_none() { 2 } else { 0 },
);
r
}
pub fn iter_forward(&self) -> Vec<T> {
let mut out = Vec::with_capacity(self.len());
let mut cur = self.read_node(HEAD_INDEX).next;
while cur != HEAD_INDEX {
let node = self.read_node(cur);
out.push(node.value);
cur = node.next;
}
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_ITER, 0);
out
}
pub fn iter_backward(&self) -> Vec<T> {
let mut out = Vec::with_capacity(self.len());
let mut cur = self.read_node(HEAD_INDEX).prev;
while cur != HEAD_INDEX {
let node = self.read_node(cur);
out.push(node.value);
cur = node.prev;
}
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_ITER, 0);
out
}
pub fn iter_forward_with_handles(&self) -> Vec<(NodeHandle<T>, T)> {
let mut out = Vec::with_capacity(self.len());
let mut cur = self.read_node(HEAD_INDEX).next;
while cur != HEAD_INDEX {
let node = self.read_node(cur);
out.push((NodeHandle::new(cur), node.value));
cur = node.next;
}
self.ring_sidecar
.push_op(crate::sidecar_ops::linked_list::OP_ITER, 0);
out
}
pub fn flush(&self) -> Result<(), LinkedListError> {
Ok(self.region.flush()?)
}
pub fn flush_async(&self) -> Result<(), LinkedListError> {
Ok(self.region.flush_async()?)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn tmp(name: &str) -> std::path::PathBuf {
let mut p = std::env::temp_dir();
let pid = std::process::id();
p.push(format!("subetha-linkedlist-{name}-{pid}.bin"));
p
}
#[test]
fn create_initial_state_is_empty() {
let p = tmp("init");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
assert!(l.is_empty());
assert_eq!(l.len(), 0);
assert_eq!(l.first(), None);
assert_eq!(l.last(), None);
assert_eq!(l.iter_forward(), Vec::<u32>::new());
std::fs::remove_file(&p).ok();
}
#[test]
fn push_back_and_iterate_forward() {
let p = tmp("push-back");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
for i in [10u32, 20, 30, 40, 50] { l.push_back(i).unwrap(); }
assert_eq!(l.len(), 5);
assert_eq!(l.iter_forward(), vec![10, 20, 30, 40, 50]);
assert_eq!(l.iter_backward(), vec![50, 40, 30, 20, 10]);
assert_eq!(l.first(), Some(10));
assert_eq!(l.last(), Some(50));
std::fs::remove_file(&p).ok();
}
#[test]
fn push_front_inserts_at_head() {
let p = tmp("push-front");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
for i in [10u32, 20, 30] { l.push_front(i).unwrap(); }
assert_eq!(l.iter_forward(), vec![30, 20, 10]);
std::fs::remove_file(&p).ok();
}
#[test]
fn pop_front_and_back_round_trip() {
let p = tmp("pop");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
for i in [10u32, 20, 30, 40] { l.push_back(i).unwrap(); }
assert_eq!(l.pop_front(), Some(10));
assert_eq!(l.pop_back(), Some(40));
assert_eq!(l.iter_forward(), vec![20, 30]);
assert_eq!(l.pop_front(), Some(20));
assert_eq!(l.pop_back(), Some(30));
assert!(l.is_empty());
assert_eq!(l.pop_front(), None);
assert_eq!(l.pop_back(), None);
std::fs::remove_file(&p).ok();
}
#[test]
fn remove_by_handle_in_middle_preserves_integrity() {
let p = tmp("remove-middle");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
let h1 = l.push_back(10).unwrap();
let h2 = l.push_back(20).unwrap();
let h3 = l.push_back(30).unwrap();
let h4 = l.push_back(40).unwrap();
let h5 = l.push_back(50).unwrap();
assert_eq!(l.remove(h3), Some(30));
assert_eq!(l.len(), 4);
assert_eq!(l.iter_forward(), vec![10, 20, 40, 50]);
assert_eq!(l.iter_backward(), vec![50, 40, 20, 10]);
assert_eq!(l.remove(h1), Some(10));
assert_eq!(l.iter_forward(), vec![20, 40, 50]);
assert_eq!(l.remove(h5), Some(50));
assert_eq!(l.iter_forward(), vec![20, 40]);
assert_eq!(l.remove(h2), Some(20));
assert_eq!(l.remove(h4), Some(40));
assert!(l.is_empty());
std::fs::remove_file(&p).ok();
}
#[test]
fn remove_nil_or_head_returns_none() {
let p = tmp("remove-nil");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 8).unwrap();
l.push_back(1).unwrap();
assert_eq!(l.remove(NodeHandle::NIL), None);
assert_eq!(l.remove(NodeHandle::new(HEAD_INDEX)), None);
std::fs::remove_file(&p).ok();
}
#[test]
fn get_and_set_via_handle() {
let p = tmp("get-set");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 8).unwrap();
let h = l.push_back(42).unwrap();
assert_eq!(l.get(h), Some(42));
l.set(h, 100).unwrap();
assert_eq!(l.get(h), Some(100));
assert_eq!(l.iter_forward(), vec![100]);
assert_eq!(l.get(NodeHandle::NIL), None);
assert!(l.set(NodeHandle::NIL, 0).is_err());
std::fs::remove_file(&p).ok();
}
#[test]
fn full_capacity_returns_error() {
let p = tmp("full");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 4).unwrap();
l.push_back(1).unwrap();
l.push_back(2).unwrap();
l.push_back(3).unwrap();
assert!(l.push_back(4).is_err());
l.pop_front().unwrap();
l.push_back(4).unwrap();
assert_eq!(l.iter_forward(), vec![2, 3, 4]);
std::fs::remove_file(&p).ok();
}
#[test]
fn iter_forward_with_handles_returns_pairs() {
let p = tmp("iter-handles");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
let h1 = l.push_back(10).unwrap();
let h2 = l.push_back(20).unwrap();
let h3 = l.push_back(30).unwrap();
let pairs = l.iter_forward_with_handles();
assert_eq!(pairs.len(), 3);
assert_eq!(pairs[0], (h1, 10));
assert_eq!(pairs[1], (h2, 20));
assert_eq!(pairs[2], (h3, 30));
std::fs::remove_file(&p).ok();
}
#[test]
fn cross_handle_visibility() {
let p = tmp("cross-handle");
let writer: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
let reader: SharedLinkedList<u32> = SharedLinkedList::open(&p, 16).unwrap();
writer.push_back(100).unwrap();
writer.push_back(200).unwrap();
assert_eq!(reader.iter_forward(), vec![100, 200]);
let h = writer.push_back(300).unwrap();
assert_eq!(reader.iter_forward(), vec![100, 200, 300]);
writer.remove(h);
assert_eq!(reader.iter_forward(), vec![100, 200]);
std::fs::remove_file(&p).ok();
}
#[test]
fn struct_payload_round_trip() {
#[derive(Clone, Copy, Debug, PartialEq, Default)]
#[repr(C)]
struct Event { ts_us: u64, code: u32 }
let p = tmp("struct");
let l: SharedLinkedList<Event> = SharedLinkedList::create(&p, 16).unwrap();
let h1 = l.push_back(Event { ts_us: 100, code: 1 }).unwrap();
let _h2 = l.push_back(Event { ts_us: 200, code: 2 }).unwrap();
assert_eq!(l.get(h1), Some(Event { ts_us: 100, code: 1 }));
let items = l.iter_forward();
assert_eq!(items.len(), 2);
std::fs::remove_file(&p).ok();
}
#[test]
fn disk_persistence_survives_reopen() {
let p = tmp("disk");
{
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
for i in [10u32, 20, 30, 40] { l.push_back(i).unwrap(); }
l.flush().unwrap();
}
let l2: SharedLinkedList<u32> = SharedLinkedList::open(&p, 16).unwrap();
assert_eq!(l2.iter_forward(), vec![10, 20, 30, 40]);
std::fs::remove_file(&p).ok();
}
#[test]
fn lru_pattern_move_to_front() {
let p = tmp("lru");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 32).unwrap();
let h_a = l.push_back(1).unwrap(); let _h_b = l.push_back(2).unwrap();
let _h_c = l.push_back(3).unwrap(); let val_a = l.remove(h_a).unwrap();
l.push_front(val_a).unwrap();
assert_eq!(l.iter_forward(), vec![1, 2, 3]);
assert_eq!(l.pop_back(), Some(3)); std::fs::remove_file(&p).ok();
}
#[test]
fn free_list_pattern_uses_handles() {
let p = tmp("free-list");
let l: SharedLinkedList<u32> = SharedLinkedList::create(&p, 16).unwrap();
let mut handles = vec![];
for i in 0..10u32 { handles.push(l.push_back(i).unwrap()); }
for (idx, h) in handles.iter().enumerate() {
if idx % 2 == 0 {
let v = l.remove(*h).unwrap();
assert_eq!(v, idx as u32);
}
}
assert_eq!(l.len(), 5);
assert_eq!(l.iter_forward(), vec![1, 3, 5, 7, 9]);
std::fs::remove_file(&p).ok();
}
}