use std::collections::HashMap;
use std::hash::Hash;
use std::sync::Arc;
#[derive(Debug, Clone)]
pub(crate) struct ColumnDictionary<T: Clone + Eq + Hash> {
values: Vec<Option<Arc<T>>>,
ids: HashMap<Arc<T>, u32>,
refcounts: Vec<u32>,
free_ids: Vec<u32>,
}
impl<T: Clone + Eq + Hash> Default for ColumnDictionary<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Clone + Eq + Hash> ColumnDictionary<T> {
pub(crate) fn new() -> Self {
Self {
values: Vec::new(),
ids: HashMap::new(),
refcounts: Vec::new(),
free_ids: Vec::new(),
}
}
pub(crate) fn intern(&mut self, value: &T) -> u32 {
if let Some(&id) = self.ids.get(value) {
return id;
}
let shared = Arc::new(value.clone());
let id = if let Some(reused) = self.free_ids.pop() {
let idx = reused as usize;
self.values[idx] = Some(Arc::clone(&shared));
self.refcounts[idx] = 0;
reused
} else {
let id = self.values.len() as u32;
self.values.push(Some(Arc::clone(&shared)));
self.refcounts.push(0);
id
};
self.ids.insert(shared, id);
id
}
pub(crate) fn get_id(&self, value: &T) -> Option<u32> {
self.ids.get(value).copied()
}
pub(crate) fn live_len(&self) -> usize {
self.ids.len()
}
pub(crate) fn iter_live_slots(&self) -> impl Iterator<Item = (u32, &T)> + '_ {
self.values
.iter()
.enumerate()
.filter_map(|(idx, slot)| slot.as_deref().map(|value| (idx as u32, value)))
}
pub(crate) fn from_live_terms(terms: Vec<T>, refcounts: Vec<u32>) -> Self {
debug_assert_eq!(
terms.len(),
refcounts.len(),
"one refcount per term is required"
);
let n = terms.len();
let mut values = Vec::with_capacity(n);
let mut ids = HashMap::with_capacity(n);
for (idx, term) in terms.into_iter().enumerate() {
let shared = Arc::new(term);
ids.insert(Arc::clone(&shared), idx as u32);
values.push(Some(shared));
}
Self {
values,
ids,
refcounts,
free_ids: Vec::new(),
}
}
pub(crate) fn resolve(&self, id: u32) -> Option<&T> {
self.values
.get(id as usize)
.and_then(|slot| slot.as_deref())
}
pub(crate) fn retain(&mut self, id: u32) {
debug_assert!(
self.resolve(id).is_some(),
"retain of an id with no live value"
);
if let Some(rc) = self.refcounts.get_mut(id as usize) {
*rc += 1;
}
}
pub(crate) fn shrink_to_fit(&mut self) {
self.values.shrink_to_fit();
self.ids.shrink_to_fit();
self.refcounts.shrink_to_fit();
self.free_ids.shrink_to_fit();
}
pub(crate) fn reserve(&mut self, additional: usize) {
self.values.reserve(additional);
self.ids.reserve(additional);
self.refcounts.reserve(additional);
}
pub(crate) fn capacity_estimate(&self) -> usize {
self.values.capacity()
}
pub(crate) fn slot_len(&self) -> usize {
self.values.len()
}
pub(crate) fn release(&mut self, id: u32) {
let idx = id as usize;
let rc = match self.refcounts.get_mut(idx) {
Some(rc) => rc,
None => return,
};
debug_assert!(*rc > 0, "release of an id with no outstanding reference");
if *rc == 0 {
return;
}
*rc -= 1;
if *rc == 0 {
if let Some(shared) = self.values[idx].take() {
self.ids.remove(shared.as_ref());
}
self.free_ids.push(id);
}
}
}
impl<T: Clone + Eq + Hash + std::fmt::Display> ColumnDictionary<T> {
pub(crate) fn size_estimate(&self) -> usize {
use std::mem::size_of;
let structural = self.values.capacity() * size_of::<Option<Arc<T>>>()
+ self.ids.capacity() * (size_of::<Arc<T>>() + size_of::<u32>())
+ self.refcounts.capacity() * size_of::<u32>()
+ self.free_ids.capacity() * size_of::<u32>();
let per_term_header = 2 * size_of::<usize>() + size_of::<T>();
let mut live_terms = 0usize;
let string_bytes: usize = self
.values
.iter()
.filter_map(Option::as_ref)
.map(|v| {
live_terms += 1;
v.to_string().len()
})
.sum();
structural + live_terms * per_term_header + string_bytes
}
}
#[cfg(test)]
impl<T: Clone + Eq + Hash> ColumnDictionary<T> {
pub(crate) fn slot_count(&self) -> usize {
self.values.len()
}
pub(crate) fn live_count(&self) -> usize {
self.ids.len()
}
pub(crate) fn free_count(&self) -> usize {
self.free_ids.len()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[derive(Debug, Clone, PartialEq, Eq)]
struct Collider(String);
impl Hash for Collider {
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
0u8.hash(state);
}
}
impl std::fmt::Display for Collider {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
fn c(s: &str) -> Collider {
Collider(s.to_string())
}
#[test]
fn colliding_values_get_distinct_ids_and_resolve_correctly() {
let mut dict: ColumnDictionary<Collider> = ColumnDictionary::new();
let a = dict.intern(&c("a"));
let b = dict.intern(&c("b"));
let d = dict.intern(&c("c"));
assert_ne!(a, b);
assert_ne!(b, d);
assert_ne!(a, d);
assert_eq!(dict.intern(&c("a")), a);
assert_eq!(dict.intern(&c("b")), b);
assert_eq!(dict.slot_count(), 3);
assert_eq!(dict.live_count(), 3);
assert_eq!(dict.resolve(a), Some(&c("a")));
assert_eq!(dict.resolve(b), Some(&c("b")));
assert_eq!(dict.resolve(d), Some(&c("c")));
assert_eq!(dict.get_id(&c("b")), Some(b));
assert_eq!(dict.get_id(&c("absent")), None);
}
#[test]
fn release_under_full_collision_reclaims_only_the_right_id() {
let mut dict: ColumnDictionary<Collider> = ColumnDictionary::new();
let a = dict.intern(&c("a"));
let b = dict.intern(&c("b"));
let d = dict.intern(&c("c"));
for id in [a, b, d] {
dict.retain(id);
}
assert_eq!(dict.live_count(), 3);
dict.release(b);
assert_eq!(dict.get_id(&c("b")), None);
assert_eq!(dict.resolve(b), None);
assert_eq!(dict.live_count(), 2);
assert_eq!(dict.free_count(), 1);
assert_eq!(dict.resolve(a), Some(&c("a")));
assert_eq!(dict.resolve(d), Some(&c("c")));
assert_eq!(dict.get_id(&c("a")), Some(a));
assert_eq!(dict.get_id(&c("c")), Some(d));
let e = dict.intern(&c("e"));
assert_eq!(e, b, "freed id must be reused");
assert_eq!(dict.slot_count(), 3, "no new slot allocated");
assert_eq!(dict.resolve(e), Some(&c("e")));
assert_eq!(dict.resolve(a), Some(&c("a")));
}
#[test]
fn refcount_keeps_shared_term_until_last_release() {
let mut dict: ColumnDictionary<Collider> = ColumnDictionary::new();
let a = dict.intern(&c("a"));
dict.retain(a);
dict.retain(a); assert_eq!(dict.live_count(), 1);
dict.release(a);
assert_eq!(dict.get_id(&c("a")), Some(a), "one reference remains");
assert_eq!(dict.resolve(a), Some(&c("a")));
dict.release(a);
assert_eq!(dict.get_id(&c("a")), None, "last reference gone");
assert_eq!(dict.resolve(a), None);
assert_eq!(dict.free_count(), 1);
}
#[test]
fn shrink_and_reserve_preserve_all_live_mappings() {
let mut dict: ColumnDictionary<Collider> = ColumnDictionary::new();
dict.reserve(64);
let mut ids = Vec::new();
for i in 0..50 {
let id = dict.intern(&c(&format!("v{i}")));
dict.retain(id);
ids.push((i, id));
}
for &(_, id) in ids.iter().filter(|(i, _)| i % 3 == 0) {
dict.release(id);
}
dict.shrink_to_fit();
for &(i, id) in &ids {
if i % 3 == 0 {
assert_eq!(dict.resolve(id), None, "released v{i} must be gone");
} else {
assert_eq!(
dict.resolve(id),
Some(&c(&format!("v{i}"))),
"live v{i} must survive shrink"
);
}
}
}
}