1use aether_ast::{DerivationTrace, PhaseGraph, PlanExplanation, TupleId};
2use aether_runtime::DerivedSet;
3use indexmap::{IndexMap, IndexSet};
4use thiserror::Error;
5
6pub trait Explainer {
7 fn explain_tuple(&self, id: &TupleId) -> Result<DerivationTrace, ExplainError>;
8 fn explain_plan(&self, plan: &PhaseGraph) -> Result<PlanExplanation, ExplainError>;
9}
10
11#[derive(Clone, Debug, Default)]
12pub struct InMemoryExplainer {
13 tuples: IndexMap<TupleId, aether_ast::DerivedTuple>,
14}
15
16impl InMemoryExplainer {
17 pub fn from_derived_set(derived: &DerivedSet) -> Self {
18 let tuples = derived
19 .tuples
20 .iter()
21 .map(|tuple| (tuple.tuple.id, tuple.clone()))
22 .collect();
23 Self { tuples }
24 }
25
26 fn collect_trace(
27 &self,
28 tuple_id: TupleId,
29 visited: &mut IndexSet<TupleId>,
30 tuples: &mut Vec<aether_ast::DerivedTuple>,
31 ) -> Result<(), ExplainError> {
32 if !visited.insert(tuple_id) {
33 return Ok(());
34 }
35
36 let tuple = self
37 .tuples
38 .get(&tuple_id)
39 .cloned()
40 .ok_or(ExplainError::UnknownTuple(tuple_id))?;
41
42 tuples.push(tuple.clone());
43 for parent in &tuple.metadata.parent_tuple_ids {
44 if !self.tuples.contains_key(parent) {
45 return Err(ExplainError::DanglingParentTuple {
46 tuple: tuple_id,
47 parent: *parent,
48 });
49 }
50 self.collect_trace(*parent, visited, tuples)?;
51 }
52
53 Ok(())
54 }
55}
56
57impl Explainer for InMemoryExplainer {
58 fn explain_tuple(&self, id: &TupleId) -> Result<DerivationTrace, ExplainError> {
59 let mut visited = IndexSet::new();
60 let mut tuples = Vec::new();
61 self.collect_trace(*id, &mut visited, &mut tuples)?;
62
63 Ok(DerivationTrace { root: *id, tuples })
64 }
65
66 fn explain_plan(&self, plan: &PhaseGraph) -> Result<PlanExplanation, ExplainError> {
67 Ok(PlanExplanation {
68 summary: format!(
69 "Phase graph with {} node(s) and {} edge(s)",
70 plan.nodes.len(),
71 plan.edges.len()
72 ),
73 phase_graph: plan.clone(),
74 })
75 }
76}
77
78#[derive(Debug, Error)]
79pub enum ExplainError {
80 #[error("unknown tuple {0}")]
81 UnknownTuple(TupleId),
82 #[error("tuple {tuple} references missing parent tuple {parent}")]
83 DanglingParentTuple { tuple: TupleId, parent: TupleId },
84}
85
86#[cfg(test)]
87mod tests {
88 use super::{ExplainError, Explainer, InMemoryExplainer};
89 use aether_ast::{
90 DerivedTuple, DerivedTupleMetadata, PredicateId, RuleId, Tuple, TupleId, Value,
91 };
92 use aether_runtime::{DerivedSet, RuntimeIteration};
93
94 fn tuple(
95 id: u64,
96 values: &[u64],
97 parent_tuple_ids: &[u64],
98 source_datom_ids: &[u64],
99 iteration: usize,
100 ) -> DerivedTuple {
101 DerivedTuple {
102 tuple: Tuple {
103 id: TupleId::new(id),
104 predicate: PredicateId::new(1),
105 values: values.iter().copied().map(Value::U64).collect(),
106 },
107 metadata: DerivedTupleMetadata {
108 rule_id: RuleId::new(1),
109 predicate_id: PredicateId::new(1),
110 stratum: 0,
111 scc_id: 0,
112 iteration,
113 parent_tuple_ids: parent_tuple_ids.iter().copied().map(TupleId::new).collect(),
114 source_datom_ids: source_datom_ids
115 .iter()
116 .copied()
117 .map(aether_ast::ElementId::new)
118 .collect(),
119 imported_cuts: Vec::new(),
120 },
121 policy: None,
122 }
123 }
124
125 #[test]
126 fn explain_tuple_returns_recursive_trace() {
127 let derived = DerivedSet {
128 tuples: vec![
129 tuple(1, &[1, 2], &[], &[11], 1),
130 tuple(2, &[2, 3], &[], &[12], 1),
131 tuple(3, &[1, 3], &[1, 2], &[11, 12], 2),
132 ],
133 iterations: vec![
134 RuntimeIteration {
135 iteration: 1,
136 delta_size: 2,
137 },
138 RuntimeIteration {
139 iteration: 2,
140 delta_size: 1,
141 },
142 RuntimeIteration {
143 iteration: 3,
144 delta_size: 0,
145 },
146 ],
147 predicate_index: Default::default(),
148 };
149 let explainer = InMemoryExplainer::from_derived_set(&derived);
150
151 let trace = explainer
152 .explain_tuple(&TupleId::new(3))
153 .expect("explain recursive tuple");
154
155 assert_eq!(trace.root, TupleId::new(3));
156 assert_eq!(
157 trace
158 .tuples
159 .iter()
160 .map(|tuple| tuple.tuple.id)
161 .collect::<Vec<_>>(),
162 vec![TupleId::new(3), TupleId::new(1), TupleId::new(2)]
163 );
164 assert_eq!(trace.tuples[0].metadata.source_datom_ids.len(), 2);
165 }
166
167 #[test]
168 fn explain_tuple_reports_unknown_roots() {
169 let explainer = InMemoryExplainer::default();
170 let error = explainer
171 .explain_tuple(&TupleId::new(99))
172 .expect_err("unknown tuple should fail");
173
174 assert!(matches!(error, ExplainError::UnknownTuple(id) if id == TupleId::new(99)));
175 }
176
177 #[test]
178 fn explain_tuple_reports_dangling_parent_references() {
179 let derived = DerivedSet {
180 tuples: vec![tuple(7, &[1, 4], &[8], &[21], 2)],
181 iterations: Vec::new(),
182 predicate_index: Default::default(),
183 };
184 let explainer = InMemoryExplainer::from_derived_set(&derived);
185
186 let error = explainer
187 .explain_tuple(&TupleId::new(7))
188 .expect_err("dangling parent should fail");
189
190 assert!(matches!(
191 error,
192 ExplainError::DanglingParentTuple { tuple, parent }
193 if tuple == TupleId::new(7) && parent == TupleId::new(8)
194 ));
195 }
196}