#![forbid(unsafe_code)]
#![deny(clippy::all, clippy::pedantic)]
#![allow(
clippy::cast_precision_loss,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::similar_names,
clippy::doc_markdown,
clippy::must_use_candidate,
clippy::needless_pass_by_value,
clippy::missing_panics_doc,
clippy::missing_errors_doc,
clippy::return_self_not_must_use,
clippy::unreadable_literal
)]
pub mod distance_tax;
pub mod merge;
use std::collections::HashSet;
use std::sync::Arc;
use dashmap::mapref::entry::Entry;
use dashmap::DashMap;
use smallvec::SmallVec;
use thiserror::Error;
use pacr_types::{CausalId, PacrRecord};
#[derive(Debug)]
pub struct CausalDag {
nodes: DashMap<CausalId, Arc<PacrRecord>>,
children: DashMap<CausalId, SmallVec<[CausalId; 4]>>,
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub enum DagError {
#[error("duplicate causal ID: {0}")]
DuplicateId(CausalId),
#[error("missing predecessor: {child} references unknown predecessor {parent}")]
MissingPredecessor {
child: CausalId,
parent: CausalId,
},
#[error("self-reference: {0} cannot be its own causal predecessor")]
SelfReference(CausalId),
}
impl CausalDag {
#[must_use]
pub fn new() -> Self {
Self {
nodes: DashMap::new(),
children: DashMap::new(),
}
}
#[must_use]
pub fn with_capacity(capacity: usize) -> Self {
Self {
nodes: DashMap::with_capacity(capacity),
children: DashMap::with_capacity(capacity),
}
}
pub fn append(&self, record: PacrRecord) -> Result<Arc<PacrRecord>, DagError> {
let id = record.id;
if record.predecessors.contains(&id) {
return Err(DagError::SelfReference(id));
}
for pred_id in &record.predecessors {
if !pred_id.is_genesis() && !self.nodes.contains_key(pred_id) {
return Err(DagError::MissingPredecessor {
child: id,
parent: *pred_id,
});
}
}
let record_arc = match self.nodes.entry(id) {
Entry::Occupied(_) => return Err(DagError::DuplicateId(id)),
Entry::Vacant(slot) => {
let arc = Arc::new(record);
slot.insert(Arc::clone(&arc));
arc
}
};
for pred_id in &record_arc.predecessors {
self.children.entry(*pred_id).or_default().push(id);
}
Ok(record_arc)
}
#[must_use]
pub fn get(&self, id: &CausalId) -> Option<Arc<PacrRecord>> {
self.nodes.get(id).map(|r| Arc::clone(r.value()))
}
#[must_use]
pub fn contains(&self, id: &CausalId) -> bool {
self.nodes.contains_key(id)
}
#[must_use]
pub fn predecessors(&self, id: &CausalId) -> Option<SmallVec<[CausalId; 4]>> {
self.nodes.get(id).map(|r| r.predecessors.clone())
}
#[must_use]
pub fn successors(&self, id: &CausalId) -> SmallVec<[CausalId; 4]> {
self.children
.get(id)
.map(|entry| entry.value().clone())
.unwrap_or_default()
}
#[must_use]
pub fn ancestry(&self, id: &CausalId, max_depth: usize) -> Vec<CausalId> {
let mut visited: HashSet<CausalId> = HashSet::new();
let mut result: Vec<CausalId> = Vec::new();
let mut queue: std::collections::VecDeque<(CausalId, usize)> =
std::collections::VecDeque::new();
if let Some(record) = self.get(id) {
for pred in &record.predecessors {
if !pred.is_genesis() {
queue.push_back((*pred, 1));
}
}
}
while let Some((current, depth)) = queue.pop_front() {
if depth > max_depth || visited.contains(¤t) {
continue;
}
visited.insert(current);
result.push(current);
if let Some(record) = self.get(¤t) {
for pred in &record.predecessors {
if !pred.is_genesis() && !visited.contains(pred) {
queue.push_back((*pred, depth + 1));
}
}
}
}
result
}
#[must_use]
pub fn len(&self) -> usize {
self.nodes.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.nodes.is_empty()
}
}
impl Default for CausalDag {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
use bytes::Bytes;
use pacr_types::{CognitiveSplit, Estimate, PacrBuilder, ResourceTriple};
pub(super) fn make_record(id: u128, preds: &[u128]) -> PacrRecord {
let predecessors = preds.iter().copied().map(CausalId).collect();
PacrBuilder::new()
.id(CausalId(id))
.predecessors(predecessors)
.landauer_cost(Estimate::exact(1e-20))
.resources(ResourceTriple {
energy: Estimate::exact(1e-19),
time: Estimate::exact(1e-9),
space: Estimate::exact(128.0),
})
.cognitive_split(CognitiveSplit {
statistical_complexity: Estimate::exact(1.0),
entropy_rate: Estimate::exact(0.5),
})
.payload(Bytes::new())
.build()
.expect("test record is always valid")
}
#[test]
fn append_genesis_record_succeeds() {
let dag = CausalDag::new();
let r = dag.append(make_record(1, &[0])); assert!(r.is_ok());
assert_eq!(dag.len(), 1);
}
#[test]
fn append_duplicate_id_returns_error() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
let err = dag.append(make_record(1, &[0])).unwrap_err();
assert!(matches!(err, DagError::DuplicateId(_)));
}
#[test]
fn append_missing_predecessor_returns_error() {
let dag = CausalDag::new();
let err = dag.append(make_record(2, &[99])).unwrap_err();
assert!(matches!(err, DagError::MissingPredecessor { .. }));
}
#[test]
fn append_self_reference_returns_error() {
let dag = CausalDag::new();
let err = dag.append(make_record(5, &[5])).unwrap_err();
assert!(matches!(err, DagError::SelfReference(_)));
}
#[test]
fn append_chain_of_three_succeeds() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
dag.append(make_record(2, &[1])).unwrap();
dag.append(make_record(3, &[2])).unwrap();
assert_eq!(dag.len(), 3);
}
#[test]
fn get_returns_correct_record() {
let dag = CausalDag::new();
dag.append(make_record(7, &[0])).unwrap();
let got = dag.get(&CausalId(7)).expect("should be present");
assert_eq!(got.id, CausalId(7));
}
#[test]
fn get_absent_id_returns_none() {
let dag = CausalDag::new();
assert!(dag.get(&CausalId(42)).is_none());
}
#[test]
fn contains_returns_true_after_append() {
let dag = CausalDag::new();
dag.append(make_record(10, &[0])).unwrap();
assert!(dag.contains(&CausalId(10)));
assert!(!dag.contains(&CausalId(11)));
}
#[test]
fn predecessors_returns_pi_set() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
dag.append(make_record(2, &[0])).unwrap();
dag.append(make_record(3, &[1, 2])).unwrap();
let preds = dag.predecessors(&CausalId(3)).unwrap();
assert!(preds.contains(&CausalId(1)));
assert!(preds.contains(&CausalId(2)));
assert_eq!(preds.len(), 2);
}
#[test]
fn successors_empty_for_leaf_node() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
assert!(dag.successors(&CausalId(1)).is_empty());
}
#[test]
fn successors_populated_after_child_appended() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
dag.append(make_record(2, &[1])).unwrap();
let succ = dag.successors(&CausalId(1));
assert_eq!(succ.len(), 1);
assert_eq!(succ[0], CausalId(2));
}
#[test]
fn successors_diamond_shape() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
dag.append(make_record(2, &[0])).unwrap();
dag.append(make_record(3, &[1, 2])).unwrap();
let succ = dag.successors(&CausalId(1));
assert_eq!(succ.len(), 1);
assert_eq!(succ[0], CausalId(3));
}
#[test]
fn ancestry_linear_chain() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
dag.append(make_record(2, &[1])).unwrap();
dag.append(make_record(3, &[2])).unwrap();
let ancestors = dag.ancestry(&CausalId(3), 10);
assert!(ancestors.contains(&CausalId(1)));
assert!(ancestors.contains(&CausalId(2)));
assert!(!ancestors.contains(&CausalId(0)));
assert!(!ancestors.contains(&CausalId(3)));
}
#[test]
fn ancestry_respects_max_depth() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
dag.append(make_record(2, &[1])).unwrap();
dag.append(make_record(3, &[2])).unwrap();
dag.append(make_record(4, &[3])).unwrap();
let ancestors = dag.ancestry(&CausalId(4), 1);
assert_eq!(ancestors.len(), 1);
assert!(ancestors.contains(&CausalId(3)));
}
#[test]
fn ancestry_empty_for_genesis_child() {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
let ancestors = dag.ancestry(&CausalId(1), 10);
assert!(ancestors.is_empty());
}
#[test]
fn append_only_get_never_returns_different_record() {
let dag = CausalDag::new();
dag.append(make_record(42, &[0])).unwrap();
let first = dag.get(&CausalId(42)).unwrap();
let second = dag.get(&CausalId(42)).unwrap();
assert!(Arc::ptr_eq(&first, &second));
}
#[test]
fn is_empty_and_len_track_correctly() {
let dag = CausalDag::new();
assert!(dag.is_empty());
assert_eq!(dag.len(), 0);
dag.append(make_record(1, &[0])).unwrap();
assert!(!dag.is_empty());
assert_eq!(dag.len(), 1);
dag.append(make_record(2, &[1])).unwrap();
assert_eq!(dag.len(), 2);
}
}
#[cfg(test)]
mod prop_tests {
use super::tests::make_record;
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn appended_record_is_retrievable(id in 1_u128..10_000_u128) {
let dag = CausalDag::new();
dag.append(make_record(id, &[0])).unwrap();
let got = dag.get(&CausalId(id));
prop_assert!(got.is_some());
prop_assert_eq!(got.unwrap().id, CausalId(id));
}
#[test]
fn duplicate_id_always_rejected(id in 1_u128..10_000_u128) {
let dag = CausalDag::new();
dag.append(make_record(id, &[0])).unwrap();
let err = dag.append(make_record(id, &[0]));
prop_assert!(err.is_err());
prop_assert!(matches!(err.unwrap_err(), DagError::DuplicateId(_)));
}
#[test]
fn linear_chain_len_correct(n in 1_usize..50_usize) {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
for i in 2..=(n as u128) {
dag.append(make_record(i, &[i - 1])).unwrap();
}
prop_assert_eq!(dag.len(), n);
}
#[test]
fn self_reference_always_rejected(id in 1_u128..10_000_u128) {
let dag = CausalDag::new();
let err = dag.append(make_record(id, &[id]));
prop_assert!(matches!(err, Err(DagError::SelfReference(_))));
}
#[test]
fn missing_predecessor_always_rejected(
id in 1_u128..5_000_u128,
parent in 5_001_u128..10_000_u128, ) {
let dag = CausalDag::new();
let result = dag.append(make_record(id, &[parent]));
prop_assert!(result.is_err());
let is_missing = matches!(result.unwrap_err(),
DagError::MissingPredecessor { .. });
prop_assert!(is_missing);
}
#[test]
fn all_appended_ids_are_retrievable(n in 1_usize..100_usize) {
let dag = CausalDag::new();
dag.append(make_record(1, &[0])).unwrap();
for i in 2..=(n as u128) {
dag.append(make_record(i, &[i - 1])).unwrap();
}
for i in 1..=(n as u128) {
prop_assert!(dag.get(&CausalId(i)).is_some(),
"id={i} should be in DAG");
}
}
#[test]
fn successors_consistent_with_predecessors(
a in 1_u128..1_000_u128,
b in 1_001_u128..2_000_u128,
) {
let dag = CausalDag::new();
dag.append(make_record(a, &[0])).unwrap();
dag.append(make_record(b, &[a])).unwrap();
let succ = dag.successors(&CausalId(a));
prop_assert!(succ.contains(&CausalId(b)),
"successors({a}) should contain {b}");
}
}
}