Skip to main content

lemma/evaluation/
explanations.rs

1//! Root explanation type and formatting.
2//!
3//! The root `Explanation` (with `result: OperationResult`) is assembled at eval time.
4//! The tree types (`ExplanationNode`, `Cause`, `SerializedConversionTraceStep`) are
5//! factored into `planning::explanation` as the API/evaluation model; evaluation
6//! builds them while walking THE DAG.
7
8use crate::computation::{OperationResult, VetoType};
9use crate::planning::semantics::RulePath;
10use serde::Serialize;
11
12// Re-export tree types for use within the evaluation module
13pub use crate::planning::explanation::{
14    Cause, ConversionTraceRole, ExplanationNode, SerializedConversionTraceStep,
15};
16
17#[derive(Debug, Clone)]
18pub struct Explanation {
19    pub name: RulePath,
20    pub result: OperationResult,
21    pub body: String,
22    pub causes: Vec<Cause>,
23    pub children: Vec<ExplanationNode>,
24}
25
26impl Serialize for Explanation {
27    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
28    where
29        S: serde::Serializer,
30    {
31        ExplanationNode::Rule {
32            name: self.name.clone(),
33            result: Some(format_operation_result(&self.result)),
34            body: self.body.clone(),
35            causes: self.causes.clone(),
36            children: self.children.clone(),
37        }
38        .serialize(serializer)
39    }
40}
41
42pub(crate) fn format_operation_result(result: &OperationResult) -> String {
43    match result {
44        OperationResult::Value(value) => value.display_value(),
45        OperationResult::Veto(VetoType::UserDefined { message: None }) => String::new(),
46        OperationResult::Veto(veto) => veto.to_string(),
47    }
48}
49
50pub fn format_explanation(explanation: &Explanation) -> String {
51    let mut lines = Vec::new();
52    let result_display = format_operation_result(&explanation.result);
53    lines.push(format!("{}: {}", explanation.name.rule, result_display));
54    let mut ctx = FormatContext {
55        lines: &mut lines,
56        indent: String::new(),
57    };
58    ctx.render_rule_contents(
59        &result_display,
60        &explanation.body,
61        &explanation.causes,
62        &explanation.children,
63    );
64    lines.join("\n")
65}
66
67#[derive(Copy, Clone)]
68enum Connector {
69    Branch,
70    Last,
71}
72
73struct FormatContext<'a> {
74    lines: &'a mut Vec<String>,
75    indent: String,
76}
77
78impl<'a> FormatContext<'a> {
79    fn push_line(&mut self, connector: Connector, text: &str) {
80        self.lines.push(format!(
81            "{}{} {text}",
82            self.indent,
83            connector_str(connector)
84        ));
85    }
86
87    fn child_indent(&self, connector: Connector) -> String {
88        match connector {
89            Connector::Branch => format!("{}│  ", self.indent),
90            Connector::Last => format!("{}   ", self.indent),
91        }
92    }
93
94    fn render_rule_contents(
95        &mut self,
96        result_display: &str,
97        body: &str,
98        causes: &[Cause],
99        children: &[ExplanationNode],
100    ) {
101        let body_shown = !body.is_empty() && body != result_display;
102        let total = causes.len() + usize::from(body_shown);
103        let mut index = 0;
104
105        for cause in causes {
106            index += 1;
107            let connector = if index == total {
108                Connector::Last
109            } else {
110                Connector::Branch
111            };
112            let value = cause.value.as_str();
113            let line = if value == "true" {
114                cause.condition.clone()
115            } else {
116                format!("{} is {}", cause.condition, value)
117            };
118            self.push_line(connector, &line);
119            let child_indent = self.child_indent(connector);
120            let mut child_ctx = FormatContext {
121                lines: self.lines,
122                indent: child_indent,
123            };
124            child_ctx.render_nodes(&cause.children, None);
125        }
126
127        if body_shown {
128            self.push_line(Connector::Last, body);
129            let child_indent = self.child_indent(Connector::Last);
130            let mut child_ctx = FormatContext {
131                lines: self.lines,
132                indent: child_indent,
133            };
134            child_ctx.render_nodes(children, Some(body));
135        } else if !children.is_empty() {
136            self.render_nodes(children, None);
137        }
138    }
139
140    fn render_nodes(&mut self, nodes: &[ExplanationNode], parent_body: Option<&str>) {
141        let len = nodes.len();
142        for (i, node) in nodes.iter().enumerate() {
143            let connector = if i + 1 == len {
144                Connector::Last
145            } else {
146                Connector::Branch
147            };
148            self.render_node(node, connector, parent_body);
149        }
150    }
151
152    fn render_conversion_contents(
153        &mut self,
154        steps: &[SerializedConversionTraceStep],
155        operands: &[ExplanationNode],
156    ) {
157        let total = steps.len() + operands.len();
158        let mut index = 0;
159        for step in steps {
160            index += 1;
161            let connector = if index == total {
162                Connector::Last
163            } else {
164                Connector::Branch
165            };
166            self.push_line(connector, &step.text);
167        }
168        for operand in operands {
169            index += 1;
170            let connector = if index == total {
171                Connector::Last
172            } else {
173                Connector::Branch
174            };
175            self.render_node(operand, connector, None);
176        }
177    }
178
179    fn render_node(
180        &mut self,
181        node: &ExplanationNode,
182        connector: Connector,
183        parent_body: Option<&str>,
184    ) {
185        match node {
186            ExplanationNode::Rule {
187                name,
188                result,
189                body,
190                causes,
191                children,
192            } => {
193                let result_str = result
194                    .as_deref()
195                    .expect("BUG: ExplanationNode::Rule.result not filled by eval");
196                self.push_line(connector, &format!("{}: {result_str}", name.rule));
197                let child_indent = self.child_indent(connector);
198                let mut child_ctx = FormatContext {
199                    lines: self.lines,
200                    indent: child_indent,
201                };
202                child_ctx.render_rule_contents(result_str, body, causes, children);
203            }
204            ExplanationNode::Compose {
205                expression,
206                operands,
207            } => {
208                if parent_body.is_some_and(|body| body == expression) {
209                    self.render_nodes(operands, None);
210                } else {
211                    self.push_line(connector, expression);
212                    let child_indent = self.child_indent(connector);
213                    let mut child_ctx = FormatContext {
214                        lines: self.lines,
215                        indent: child_indent,
216                    };
217                    child_ctx.render_nodes(operands, None);
218                }
219            }
220            ExplanationNode::Data { name, display } => {
221                if name.data.is_empty() {
222                    self.push_line(connector, display);
223                } else {
224                    self.push_line(connector, &format!("{name}: {display}"));
225                }
226            }
227            ExplanationNode::DataUnused { name } => {
228                self.push_line(connector, &name.to_string());
229            }
230            ExplanationNode::Conversion {
231                expression,
232                steps,
233                operands,
234            } => {
235                let expression_is_parent_body = parent_body.is_some_and(|body| body == expression);
236                if expression_is_parent_body {
237                    let steps_without_outcome: Vec<SerializedConversionTraceStep> = steps
238                        .iter()
239                        .filter(|step| !matches!(step.role, ConversionTraceRole::Outcome))
240                        .cloned()
241                        .collect();
242                    self.render_conversion_contents(&steps_without_outcome, operands);
243                } else {
244                    self.push_line(connector, expression);
245                    let child_indent = self.child_indent(connector);
246                    let mut child_ctx = FormatContext {
247                        lines: self.lines,
248                        indent: child_indent,
249                    };
250                    child_ctx.render_conversion_contents(steps, operands);
251                }
252            }
253            ExplanationNode::Veto { message } => {
254                let text = match message.as_deref() {
255                    Some(msg) if !msg.is_empty() => format!("veto \"{msg}\""),
256                    _ => "veto".to_string(),
257                };
258                self.push_line(connector, &text);
259            }
260            ExplanationNode::Piecewise { .. } => {
261                unreachable!("BUG: Piecewise must be lowered before format")
262            }
263        }
264    }
265}
266
267fn connector_str(connector: Connector) -> &'static str {
268    match connector {
269        Connector::Branch => "├─",
270        Connector::Last => "└─",
271    }
272}