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