use std::alloc::{self, Layout};
use std::ptr::NonNull;
use smallvec::SmallVec;
use crate::dict::NameDict;
use crate::{NodeId, RelationshipId};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) struct AdjEntry {
pub type_id: u32,
pub neighbour: NodeId,
pub rel: RelationshipId,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(super) enum Side {
Out,
In,
}
impl Side {
#[cfg(test)]
fn opposite(self) -> Side {
match self {
Side::Out => Side::In,
Side::In => Side::Out,
}
}
}
#[inline]
fn width(value: u64) -> usize {
((64 - value.leading_zeros() as usize).div_ceil(8)).max(1)
}
#[inline]
fn type_width(code: u8) -> usize {
match code {
0 => 1,
1 => 2,
_ => 4,
}
}
#[inline]
fn entry_len(h: u8) -> usize {
1 + type_width(h >> 6) + (h & 7) as usize + 1 + ((h >> 3) & 7) as usize + 1
}
#[inline]
fn read(bytes: &[u8], pos: usize, n: usize) -> u64 {
if let Some(word) = bytes.get(pos..pos + 8) {
let word = u64::from_le_bytes(word.try_into().unwrap());
word & (u64::MAX >> (64 - 8 * n))
} else {
let mut buf = [0u8; 8];
buf[..n].copy_from_slice(&bytes[pos..pos + n]);
u64::from_le_bytes(buf)
}
}
#[inline]
fn encode(entry: AdjEntry, mirrored: bool, buf: &mut [u8; 21]) -> usize {
let nb = width(entry.neighbour);
let rb = width(entry.rel);
let (tcode, tb) = match entry.type_id {
0..=0xFF => (0u8, 1),
0x100..=0xFFFF => (1, 2),
_ => (2, 4),
};
let header = (nb as u8 - 1) | ((rb as u8 - 1) << 3) | (tcode << 6);
let len = 1 + tb + nb + rb;
let ty = &entry.type_id.to_le_bytes()[..tb];
let neighbour = &entry.neighbour.to_le_bytes()[..nb];
let rel = &entry.rel.to_le_bytes()[..rb];
if mirrored {
buf[..rb].copy_from_slice(rel);
buf[rb..rb + nb].copy_from_slice(neighbour);
buf[rb + nb..rb + nb + tb].copy_from_slice(ty);
buf[len - 1] = header;
} else {
buf[0] = header;
buf[1..1 + tb].copy_from_slice(ty);
buf[1 + tb..1 + tb + nb].copy_from_slice(neighbour);
buf[1 + tb + nb..len].copy_from_slice(rel);
}
len
}
#[repr(C)]
struct Header {
out_len: u32,
in_len: u32,
cap: u32,
}
const HEADER: usize = std::mem::size_of::<Header>();
const ALIGN: usize = std::mem::align_of::<Header>();
const _: () = assert!(HEADER == 12);
const MAX_CAP: usize = (u32::MAX as usize & !15) - HEADER;
#[inline]
const fn capacity_for(min: usize) -> usize {
((HEADER + min + 15) & !15) - HEADER
}
const _: () = assert!(capacity_for(MAX_CAP) == MAX_CAP && MAX_CAP <= u32::MAX as usize);
#[derive(Default)]
pub(super) struct Adjacency {
ptr: Option<NonNull<Header>>,
}
unsafe impl Send for Adjacency {}
unsafe impl Sync for Adjacency {}
impl Adjacency {
#[inline]
fn header(&self) -> Option<&Header> {
self.ptr.map(|ptr| unsafe { &*ptr.as_ptr() })
}
#[inline]
fn data(ptr: NonNull<Header>) -> *mut u8 {
unsafe { ptr.as_ptr().cast::<u8>().add(HEADER) }
}
#[inline]
pub(super) fn outgoing(&self) -> AdjSide<'_> {
let bytes = match self.ptr {
Some(ptr) => unsafe {
let len = (*ptr.as_ptr()).out_len as usize;
std::slice::from_raw_parts(Self::data(ptr), len)
},
None => &[],
};
AdjSide {
bytes,
mirrored: false,
}
}
#[inline]
pub(super) fn incoming(&self) -> AdjSide<'_> {
let bytes = match self.ptr {
Some(ptr) => unsafe {
let h = &*ptr.as_ptr();
let len = h.in_len as usize;
std::slice::from_raw_parts(Self::data(ptr).add(h.cap as usize - len), len)
},
None => &[],
};
AdjSide {
bytes,
mirrored: true,
}
}
#[inline]
fn layout(cap: usize) -> Layout {
Layout::from_size_align(HEADER + cap, ALIGN).expect("adjacency list fits the address space")
}
fn allocate(cap: usize) -> NonNull<Header> {
let cap32 = u32::try_from(cap).expect("adjacency list under 4 GiB");
let layout = Self::layout(cap);
let raw = unsafe { alloc::alloc(layout) };
let Some(ptr) = NonNull::new(raw.cast::<Header>()) else {
alloc::handle_alloc_error(layout)
};
unsafe {
ptr.as_ptr().write(Header {
out_len: 0,
in_len: 0,
cap: cap32,
})
};
ptr
}
fn grow(&mut self, cap: usize) {
let Some(ptr) = self.ptr else {
self.ptr = Some(Self::allocate(cap));
return;
};
let cap32 = u32::try_from(cap).expect("adjacency list under 4 GiB");
let new_layout = Self::layout(cap);
unsafe {
let old_cap = (*ptr.as_ptr()).cap as usize;
let in_len = (*ptr.as_ptr()).in_len as usize;
let raw = alloc::realloc(
ptr.as_ptr().cast(),
Self::layout(old_cap),
new_layout.size(),
);
let Some(new) = NonNull::new(raw.cast::<Header>()) else {
alloc::handle_alloc_error(new_layout)
};
let data = Self::data(new);
std::ptr::copy(data.add(old_cap - in_len), data.add(cap - in_len), in_len);
(*new.as_ptr()).cap = cap32;
self.ptr = Some(new);
}
}
fn free(&mut self) {
if let Some(ptr) = self.ptr.take() {
unsafe {
let cap = (*ptr.as_ptr()).cap as usize;
alloc::dealloc(ptr.as_ptr().cast(), Self::layout(cap));
}
}
}
pub(super) fn clear(&mut self) {
self.free();
}
#[inline]
fn used(&self) -> usize {
self.header()
.map_or(0, |h| h.out_len as usize + h.in_len as usize)
}
#[inline]
pub(super) fn side(&self, side: Side) -> AdjSide<'_> {
match side {
Side::Out => self.outgoing(),
Side::In => self.incoming(),
}
}
pub(super) fn push(&mut self, side: Side, entry: AdjEntry) {
let mut buf = [0u8; 21];
let len = encode(entry, side == Side::In, &mut buf);
let (used, cap) = self.header().map_or((0, 0), |h| {
(h.out_len as usize + h.in_len as usize, h.cap as usize)
});
if used + len > cap {
assert!(used + len <= MAX_CAP, "adjacency list over 4 GiB");
let want = (used + len).max(cap + cap / 2).min(MAX_CAP);
self.grow(capacity_for(want));
}
let ptr = self.ptr.expect("allocated above");
unsafe {
let header = &mut *ptr.as_ptr();
let at = if side == Side::Out {
let at = header.out_len as usize;
header.out_len += len as u32;
at
} else {
header.in_len += len as u32;
header.cap as usize - header.in_len as usize
};
std::ptr::copy_nonoverlapping(buf.as_ptr(), Self::data(ptr).add(at), len);
}
}
pub(super) fn remove(&mut self, side: Side, rel: RelationshipId) -> bool {
let mut iter = self.side(side).iter();
let (before, after) = loop {
let before = iter.pos();
let Some(entry) = iter.next() else {
return false;
};
if entry.rel == rel {
break (before, iter.pos());
}
};
let Some(ptr) = self.ptr else {
return false;
};
unsafe {
let header = &mut *ptr.as_ptr();
let data = Self::data(ptr);
if side == Side::Out {
let len = header.out_len as usize;
std::ptr::copy(data.add(after), data.add(before), len - after);
header.out_len -= (after - before) as u32;
} else {
let base = data.add(header.cap as usize - header.in_len as usize);
std::ptr::copy(base, base.add(before - after), after);
header.in_len -= (before - after) as u32;
}
}
if self.used() == 0 {
self.free();
}
true
}
pub(super) fn heap_bytes(&self) -> usize {
self.header().map_or(0, |h| HEADER + h.cap as usize)
}
pub(super) fn heap_bytes_by_side(&self) -> (usize, usize) {
let Some(h) = self.header() else {
return (0, 0);
};
let shared = HEADER + h.cap as usize - h.out_len as usize - h.in_len as usize;
let out = h.out_len as usize + shared / 2;
(out, self.heap_bytes() - out)
}
}
impl Clone for Adjacency {
fn clone(&self) -> Self {
let (out, inc) = (self.outgoing().bytes, self.incoming().bytes);
if out.is_empty() && inc.is_empty() {
return Adjacency { ptr: None };
}
let cap = capacity_for(out.len() + inc.len());
let ptr = Self::allocate(cap);
unsafe {
let data = Self::data(ptr);
std::ptr::copy_nonoverlapping(out.as_ptr(), data, out.len());
std::ptr::copy_nonoverlapping(inc.as_ptr(), data.add(cap - inc.len()), inc.len());
let header = &mut *ptr.as_ptr();
header.out_len = out.len() as u32;
header.in_len = inc.len() as u32;
}
Adjacency { ptr: Some(ptr) }
}
}
impl Drop for Adjacency {
fn drop(&mut self) {
self.free();
}
}
impl std::fmt::Debug for Adjacency {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Adjacency")
.field("outgoing", &self.outgoing())
.field("incoming", &self.incoming())
.finish()
}
}
#[derive(Clone, Copy)]
pub(super) struct AdjSide<'a> {
bytes: &'a [u8],
mirrored: bool,
}
impl<'a> AdjSide<'a> {
#[inline]
pub(super) fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
#[inline]
pub(super) fn iter(&self) -> AdjIter<'a> {
AdjIter {
bytes: self.bytes,
pos: if self.mirrored { self.bytes.len() } else { 0 },
mirrored: self.mirrored,
}
}
pub(super) fn count(&self) -> usize {
let bytes = self.bytes;
let mut n = 0;
if self.mirrored {
let mut pos = bytes.len();
while pos > 0 {
pos -= entry_len(bytes[pos - 1]);
n += 1;
}
} else {
let mut pos = 0;
while pos < bytes.len() {
pos += entry_len(bytes[pos]);
n += 1;
}
}
n
}
}
impl std::fmt::Debug for AdjSide<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_list().entries(self.iter()).finish()
}
}
pub(super) struct AdjIter<'a> {
bytes: &'a [u8],
pos: usize,
mirrored: bool,
}
impl AdjIter<'_> {
#[inline]
fn pos(&self) -> usize {
self.pos
}
}
impl Iterator for AdjIter<'_> {
type Item = AdjEntry;
#[inline]
fn next(&mut self) -> Option<AdjEntry> {
if self.mirrored {
if self.pos == 0 {
return None;
}
let h = self.bytes[self.pos - 1];
let tb = type_width(h >> 6);
let nb = (h & 7) as usize + 1;
let rb = ((h >> 3) & 7) as usize + 1;
let ty = self.pos - 1 - tb;
let neighbour = ty - nb;
let rel = neighbour - rb;
self.pos = rel;
return Some(AdjEntry {
type_id: read(self.bytes, ty, tb) as u32,
neighbour: read(self.bytes, neighbour, nb),
rel: read(self.bytes, rel, rb),
});
}
let h = *self.bytes.get(self.pos)?;
let tb = type_width(h >> 6);
let nb = (h & 7) as usize + 1;
let rb = ((h >> 3) & 7) as usize + 1;
let p = self.pos + 1;
let entry = AdjEntry {
type_id: read(self.bytes, p, tb) as u32,
neighbour: read(self.bytes, p + tb, nb),
rel: read(self.bytes, p + tb + nb, rb),
};
self.pos = p + tb + nb + rb;
Some(entry)
}
}
pub(super) enum TypeFilter {
Any,
One(u32),
Many(SmallVec<u32, 4>),
Nothing,
}
impl TypeFilter {
#[inline]
pub(super) fn resolve(dict: &NameDict, types: &[String]) -> Self {
match types {
[] => TypeFilter::Any,
[single] => match dict.id_of(single) {
Some(id) => TypeFilter::One(id),
None => TypeFilter::Nothing,
},
many => {
let ids: SmallVec<u32, 4> = many.iter().filter_map(|t| dict.id_of(t)).collect();
match ids.as_slice() {
[] => TypeFilter::Nothing,
[one] => TypeFilter::One(*one),
_ => TypeFilter::Many(ids),
}
}
}
}
#[inline]
pub(super) fn matches(&self, type_id: u32) -> bool {
match self {
TypeFilter::Any => true,
TypeFilter::One(id) => *id == type_id,
TypeFilter::Many(ids) => ids.contains(&type_id),
TypeFilter::Nothing => false,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn entries() -> Vec<AdjEntry> {
let ids = [
0u64,
1,
255,
256,
65_535,
65_536,
16_777_215,
16_777_216,
u32::MAX as u64,
u32::MAX as u64 + 1,
(1 << 48) - 1,
1 << 56,
u64::MAX,
];
let types = [0u32, 1, 255, 256, 65_535, 65_536, u32::MAX];
let mut out = Vec::new();
for (i, &neighbour) in ids.iter().enumerate() {
for (j, &type_id) in types.iter().enumerate() {
let rel = ids[(i * 7 + j * 3) % ids.len()] ^ (out.len() as u64);
out.push(AdjEntry {
type_id,
neighbour,
rel,
});
}
}
out
}
#[test]
fn every_width_round_trips_in_both_directions() {
let entries = entries();
let mut adj = Adjacency::default();
for &e in &entries {
adj.push(Side::Out, e);
adj.push(Side::In, AdjEntry { rel: !e.rel, ..e });
}
let incoming: Vec<_> = entries
.iter()
.map(|&e| AdjEntry { rel: !e.rel, ..e })
.collect();
assert_eq!(adj.outgoing().iter().collect::<Vec<_>>(), entries);
assert_eq!(adj.incoming().iter().collect::<Vec<_>>(), incoming);
assert_eq!(adj.outgoing().count(), entries.len());
assert_eq!(adj.incoming().count(), entries.len());
let copy = adj.clone();
assert_eq!(copy.outgoing().iter().collect::<Vec<_>>(), entries);
assert_eq!(copy.incoming().iter().collect::<Vec<_>>(), incoming);
assert!(copy.heap_bytes() <= adj.heap_bytes());
}
#[test]
fn a_single_short_entry_reads_without_eight_bytes_behind_it() {
for dir in [Side::Out, Side::In] {
let mut adj = Adjacency::default();
let e = AdjEntry {
type_id: 3,
neighbour: 7,
rel: 9,
};
adj.push(dir, e);
assert_eq!(adj.side(dir).iter().collect::<Vec<_>>(), vec![e]);
assert!(adj.side(dir.opposite()).is_empty());
assert_eq!(adj.heap_bytes(), 16);
}
}
#[test]
fn remove_takes_exactly_one_entry_and_keeps_order() {
for dir in [Side::Out, Side::In] {
let entries = entries();
let mut adj = Adjacency::default();
for &e in &entries {
adj.push(dir, e);
adj.push(
dir.opposite(),
AdjEntry {
rel: e.rel ^ 1,
..e
},
);
}
let other: Vec<_> = adj.side(dir.opposite()).iter().collect();
let mut expected = entries.clone();
for index in [expected.len() / 2, 0, expected.len() - 3] {
let gone = expected.remove(index);
assert!(adj.remove(dir, gone.rel));
assert!(!adj.remove(dir, gone.rel));
assert_eq!(adj.side(dir).iter().collect::<Vec<_>>(), expected);
assert_eq!(adj.side(dir).count(), expected.len());
assert_eq!(adj.side(dir.opposite()).iter().collect::<Vec<_>>(), other);
}
for e in expected {
assert!(adj.remove(dir, e.rel));
}
assert!(adj.side(dir).is_empty());
for e in other {
assert!(adj.remove(dir.opposite(), e.rel));
}
assert_eq!(adj.heap_bytes(), 0);
}
}
#[test]
fn eight_entries_of_a_16m_graph_take_one_80_byte_allocation() {
let mut adj = Adjacency::default();
for rel in 0..8u64 {
adj.push(
if rel % 2 == 0 { Side::Out } else { Side::In },
AdjEntry {
type_id: 200,
neighbour: 15_999_999,
rel: 16_000_000 + rel,
},
);
}
assert_eq!(adj.clone().heap_bytes(), 80);
assert_eq!(std::mem::size_of::<Adjacency>(), 8);
}
#[test]
fn a_type_filter_resolves_names_once() {
let mut dict = NameDict::default();
for i in 0..20 {
dict.id_or_insert_str(&format!("T{i}"));
}
let filter = |types: &[&str]| {
let types: Vec<String> = types.iter().map(|t| t.to_string()).collect();
TypeFilter::resolve(&dict, &types)
};
assert!(filter(&[]).matches(19));
let one = filter(&["T4"]);
assert!(one.matches(4) && !one.matches(5));
let many = filter(&["T4", "nope", "T9"]);
assert!(many.matches(4) && many.matches(9) && !many.matches(5));
assert!(!filter(&["nope"]).matches(0));
}
}