1#![forbid(unsafe_code)]
36#![deny(clippy::all, clippy::pedantic)]
37#![allow(
38 clippy::cast_precision_loss,
39 clippy::cast_possible_truncation,
40 clippy::cast_sign_loss,
41 clippy::similar_names,
42 clippy::doc_markdown,
43 clippy::must_use_candidate,
44 clippy::needless_pass_by_value,
45 clippy::missing_panics_doc,
46 clippy::missing_errors_doc,
47 clippy::return_self_not_must_use,
48 clippy::unreadable_literal
49)]
50
51pub mod distance_tax;
52pub mod merge;
53
54use std::collections::HashSet;
55use std::sync::Arc;
56
57use dashmap::mapref::entry::Entry;
58use dashmap::DashMap;
59use smallvec::SmallVec;
60use thiserror::Error;
61
62use pacr_types::{CausalId, PacrRecord};
63
64#[derive(Debug)]
80pub struct CausalDag {
81 nodes: DashMap<CausalId, Arc<PacrRecord>>,
84
85 children: DashMap<CausalId, SmallVec<[CausalId; 4]>>,
90}
91
92#[derive(Debug, Clone, Error)]
94#[non_exhaustive]
95pub enum DagError {
96 #[error("duplicate causal ID: {0}")]
99 DuplicateId(CausalId),
100
101 #[error("missing predecessor: {child} references unknown predecessor {parent}")]
104 MissingPredecessor {
105 child: CausalId,
107 parent: CausalId,
109 },
110
111 #[error("self-reference: {0} cannot be its own causal predecessor")]
114 SelfReference(CausalId),
115}
116
117impl CausalDag {
120 #[must_use]
122 pub fn new() -> Self {
123 Self {
124 nodes: DashMap::new(),
125 children: DashMap::new(),
126 }
127 }
128
129 #[must_use]
134 pub fn with_capacity(capacity: usize) -> Self {
135 Self {
136 nodes: DashMap::with_capacity(capacity),
137 children: DashMap::with_capacity(capacity),
138 }
139 }
140
141 pub fn append(&self, record: PacrRecord) -> Result<Arc<PacrRecord>, DagError> {
159 let id = record.id;
160
161 if record.predecessors.contains(&id) {
164 return Err(DagError::SelfReference(id));
165 }
166
167 for pred_id in &record.predecessors {
172 if !pred_id.is_genesis() && !self.nodes.contains_key(pred_id) {
173 return Err(DagError::MissingPredecessor {
174 child: id,
175 parent: *pred_id,
176 });
177 }
178 }
179
180 let record_arc = match self.nodes.entry(id) {
184 Entry::Occupied(_) => return Err(DagError::DuplicateId(id)),
185 Entry::Vacant(slot) => {
186 let arc = Arc::new(record);
187 slot.insert(Arc::clone(&arc));
188 arc
189 }
190 };
191
192 for pred_id in &record_arc.predecessors {
197 self.children.entry(*pred_id).or_default().push(id);
198 }
199
200 Ok(record_arc)
201 }
202
203 #[must_use]
211 pub fn get(&self, id: &CausalId) -> Option<Arc<PacrRecord>> {
212 self.nodes.get(id).map(|r| Arc::clone(r.value()))
213 }
214
215 #[must_use]
221 pub fn contains(&self, id: &CausalId) -> bool {
222 self.nodes.contains_key(id)
223 }
224
225 #[must_use]
233 pub fn predecessors(&self, id: &CausalId) -> Option<SmallVec<[CausalId; 4]>> {
234 self.nodes.get(id).map(|r| r.predecessors.clone())
235 }
236
237 #[must_use]
245 pub fn successors(&self, id: &CausalId) -> SmallVec<[CausalId; 4]> {
246 self.children
247 .get(id)
248 .map(|entry| entry.value().clone())
249 .unwrap_or_default()
250 }
251
252 #[must_use]
262 pub fn ancestry(&self, id: &CausalId, max_depth: usize) -> Vec<CausalId> {
263 let mut visited: HashSet<CausalId> = HashSet::new();
264 let mut result: Vec<CausalId> = Vec::new();
265 let mut queue: std::collections::VecDeque<(CausalId, usize)> =
266 std::collections::VecDeque::new();
267
268 if let Some(record) = self.get(id) {
269 for pred in &record.predecessors {
270 if !pred.is_genesis() {
271 queue.push_back((*pred, 1));
272 }
273 }
274 }
275
276 while let Some((current, depth)) = queue.pop_front() {
277 if depth > max_depth || visited.contains(¤t) {
278 continue;
279 }
280 visited.insert(current);
281 result.push(current);
282
283 if let Some(record) = self.get(¤t) {
284 for pred in &record.predecessors {
285 if !pred.is_genesis() && !visited.contains(pred) {
286 queue.push_back((*pred, depth + 1));
287 }
288 }
289 }
290 }
291
292 result
293 }
294
295 #[must_use]
301 pub fn len(&self) -> usize {
302 self.nodes.len()
303 }
304
305 #[must_use]
307 pub fn is_empty(&self) -> bool {
308 self.nodes.is_empty()
309 }
310}
311
312impl Default for CausalDag {
313 fn default() -> Self {
314 Self::new()
315 }
316}
317
318#[cfg(test)]
321mod tests {
322 use super::*;
323 use bytes::Bytes;
324 use pacr_types::{CognitiveSplit, Estimate, PacrBuilder, ResourceTriple};
325
326 pub(super) fn make_record(id: u128, preds: &[u128]) -> PacrRecord {
328 let predecessors = preds.iter().copied().map(CausalId).collect();
329 PacrBuilder::new()
330 .id(CausalId(id))
331 .predecessors(predecessors)
332 .landauer_cost(Estimate::exact(1e-20))
333 .resources(ResourceTriple {
334 energy: Estimate::exact(1e-19),
335 time: Estimate::exact(1e-9),
336 space: Estimate::exact(128.0),
337 })
338 .cognitive_split(CognitiveSplit {
339 statistical_complexity: Estimate::exact(1.0),
340 entropy_rate: Estimate::exact(0.5),
341 })
342 .payload(Bytes::new())
343 .build()
344 .expect("test record is always valid")
345 }
346
347 #[test]
350 fn append_genesis_record_succeeds() {
351 let dag = CausalDag::new();
352 let r = dag.append(make_record(1, &[0])); assert!(r.is_ok());
354 assert_eq!(dag.len(), 1);
355 }
356
357 #[test]
358 fn append_duplicate_id_returns_error() {
359 let dag = CausalDag::new();
360 dag.append(make_record(1, &[0])).unwrap();
361 let err = dag.append(make_record(1, &[0])).unwrap_err();
362 assert!(matches!(err, DagError::DuplicateId(_)));
363 }
364
365 #[test]
366 fn append_missing_predecessor_returns_error() {
367 let dag = CausalDag::new();
368 let err = dag.append(make_record(2, &[99])).unwrap_err();
370 assert!(matches!(err, DagError::MissingPredecessor { .. }));
371 }
372
373 #[test]
374 fn append_self_reference_returns_error() {
375 let dag = CausalDag::new();
376 let err = dag.append(make_record(5, &[5])).unwrap_err();
377 assert!(matches!(err, DagError::SelfReference(_)));
378 }
379
380 #[test]
381 fn append_chain_of_three_succeeds() {
382 let dag = CausalDag::new();
383 dag.append(make_record(1, &[0])).unwrap();
384 dag.append(make_record(2, &[1])).unwrap();
385 dag.append(make_record(3, &[2])).unwrap();
386 assert_eq!(dag.len(), 3);
387 }
388
389 #[test]
392 fn get_returns_correct_record() {
393 let dag = CausalDag::new();
394 dag.append(make_record(7, &[0])).unwrap();
395 let got = dag.get(&CausalId(7)).expect("should be present");
396 assert_eq!(got.id, CausalId(7));
397 }
398
399 #[test]
400 fn get_absent_id_returns_none() {
401 let dag = CausalDag::new();
402 assert!(dag.get(&CausalId(42)).is_none());
403 }
404
405 #[test]
406 fn contains_returns_true_after_append() {
407 let dag = CausalDag::new();
408 dag.append(make_record(10, &[0])).unwrap();
409 assert!(dag.contains(&CausalId(10)));
410 assert!(!dag.contains(&CausalId(11)));
411 }
412
413 #[test]
416 fn predecessors_returns_pi_set() {
417 let dag = CausalDag::new();
418 dag.append(make_record(1, &[0])).unwrap();
419 dag.append(make_record(2, &[0])).unwrap();
420 dag.append(make_record(3, &[1, 2])).unwrap();
421 let preds = dag.predecessors(&CausalId(3)).unwrap();
422 assert!(preds.contains(&CausalId(1)));
423 assert!(preds.contains(&CausalId(2)));
424 assert_eq!(preds.len(), 2);
425 }
426
427 #[test]
428 fn successors_empty_for_leaf_node() {
429 let dag = CausalDag::new();
430 dag.append(make_record(1, &[0])).unwrap();
431 assert!(dag.successors(&CausalId(1)).is_empty());
433 }
434
435 #[test]
436 fn successors_populated_after_child_appended() {
437 let dag = CausalDag::new();
438 dag.append(make_record(1, &[0])).unwrap();
439 dag.append(make_record(2, &[1])).unwrap();
440 let succ = dag.successors(&CausalId(1));
441 assert_eq!(succ.len(), 1);
442 assert_eq!(succ[0], CausalId(2));
443 }
444
445 #[test]
446 fn successors_diamond_shape() {
447 let dag = CausalDag::new();
450 dag.append(make_record(1, &[0])).unwrap();
451 dag.append(make_record(2, &[0])).unwrap();
452 dag.append(make_record(3, &[1, 2])).unwrap();
453 let succ = dag.successors(&CausalId(1));
454 assert_eq!(succ.len(), 1);
455 assert_eq!(succ[0], CausalId(3));
456 }
457
458 #[test]
461 fn ancestry_linear_chain() {
462 let dag = CausalDag::new();
464 dag.append(make_record(1, &[0])).unwrap();
465 dag.append(make_record(2, &[1])).unwrap();
466 dag.append(make_record(3, &[2])).unwrap();
467 let ancestors = dag.ancestry(&CausalId(3), 10);
468 assert!(ancestors.contains(&CausalId(1)));
470 assert!(ancestors.contains(&CausalId(2)));
471 assert!(!ancestors.contains(&CausalId(0)));
472 assert!(!ancestors.contains(&CausalId(3)));
473 }
474
475 #[test]
476 fn ancestry_respects_max_depth() {
477 let dag = CausalDag::new();
479 dag.append(make_record(1, &[0])).unwrap();
480 dag.append(make_record(2, &[1])).unwrap();
481 dag.append(make_record(3, &[2])).unwrap();
482 dag.append(make_record(4, &[3])).unwrap();
483 let ancestors = dag.ancestry(&CausalId(4), 1);
485 assert_eq!(ancestors.len(), 1);
486 assert!(ancestors.contains(&CausalId(3)));
487 }
488
489 #[test]
490 fn ancestry_empty_for_genesis_child() {
491 let dag = CausalDag::new();
492 dag.append(make_record(1, &[0])).unwrap();
493 let ancestors = dag.ancestry(&CausalId(1), 10);
495 assert!(ancestors.is_empty());
496 }
497
498 #[test]
501 fn append_only_get_never_returns_different_record() {
502 let dag = CausalDag::new();
504 dag.append(make_record(42, &[0])).unwrap();
505 let first = dag.get(&CausalId(42)).unwrap();
506 let second = dag.get(&CausalId(42)).unwrap();
507 assert!(Arc::ptr_eq(&first, &second));
509 }
510
511 #[test]
512 fn is_empty_and_len_track_correctly() {
513 let dag = CausalDag::new();
514 assert!(dag.is_empty());
515 assert_eq!(dag.len(), 0);
516 dag.append(make_record(1, &[0])).unwrap();
517 assert!(!dag.is_empty());
518 assert_eq!(dag.len(), 1);
519 dag.append(make_record(2, &[1])).unwrap();
520 assert_eq!(dag.len(), 2);
521 }
522}
523
524#[cfg(test)]
527mod prop_tests {
528 use super::tests::make_record;
529 use super::*;
530 use proptest::prelude::*;
531
532 proptest! {
533 #[test]
536 fn appended_record_is_retrievable(id in 1_u128..10_000_u128) {
537 let dag = CausalDag::new();
538 dag.append(make_record(id, &[0])).unwrap();
539 let got = dag.get(&CausalId(id));
540 prop_assert!(got.is_some());
541 prop_assert_eq!(got.unwrap().id, CausalId(id));
542 }
543
544 #[test]
546 fn duplicate_id_always_rejected(id in 1_u128..10_000_u128) {
547 let dag = CausalDag::new();
548 dag.append(make_record(id, &[0])).unwrap();
549 let err = dag.append(make_record(id, &[0]));
550 prop_assert!(err.is_err());
551 prop_assert!(matches!(err.unwrap_err(), DagError::DuplicateId(_)));
552 }
553
554 #[test]
557 fn linear_chain_len_correct(n in 1_usize..50_usize) {
558 let dag = CausalDag::new();
559 dag.append(make_record(1, &[0])).unwrap();
561 for i in 2..=(n as u128) {
562 dag.append(make_record(i, &[i - 1])).unwrap();
563 }
564 prop_assert_eq!(dag.len(), n);
565 }
566
567 #[test]
569 fn self_reference_always_rejected(id in 1_u128..10_000_u128) {
570 let dag = CausalDag::new();
571 let err = dag.append(make_record(id, &[id]));
572 prop_assert!(matches!(err, Err(DagError::SelfReference(_))));
573 }
574
575 #[test]
578 fn missing_predecessor_always_rejected(
579 id in 1_u128..5_000_u128,
580 parent in 5_001_u128..10_000_u128, ) {
582 let dag = CausalDag::new();
583 let result = dag.append(make_record(id, &[parent]));
584 prop_assert!(result.is_err());
585 let is_missing = matches!(result.unwrap_err(),
586 DagError::MissingPredecessor { .. });
587 prop_assert!(is_missing);
588 }
589
590 #[test]
594 fn all_appended_ids_are_retrievable(n in 1_usize..100_usize) {
595 let dag = CausalDag::new();
596 dag.append(make_record(1, &[0])).unwrap();
597 for i in 2..=(n as u128) {
598 dag.append(make_record(i, &[i - 1])).unwrap();
599 }
600 for i in 1..=(n as u128) {
601 prop_assert!(dag.get(&CausalId(i)).is_some(),
602 "id={i} should be in DAG");
603 }
604 }
605
606 #[test]
609 fn successors_consistent_with_predecessors(
610 a in 1_u128..1_000_u128,
611 b in 1_001_u128..2_000_u128,
612 ) {
613 let dag = CausalDag::new();
614 dag.append(make_record(a, &[0])).unwrap();
615 dag.append(make_record(b, &[a])).unwrap();
616 let succ = dag.successors(&CausalId(a));
617 prop_assert!(succ.contains(&CausalId(b)),
618 "successors({a}) should contain {b}");
619 }
620 }
621}