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 wire/evaluation model; evaluation
6//! builds them while walking THE DAG.
7
8use crate::evaluation::operations::{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
113                .value
114                .as_deref()
115                .expect("BUG: Cause.value not filled by eval");
116            let line = if value == "true" {
117                cause.condition.clone()
118            } else {
119                format!("{} is {}", cause.condition, value)
120            };
121            self.push_line(connector, &line);
122            let child_indent = self.child_indent(connector);
123            let mut child_ctx = FormatContext {
124                lines: self.lines,
125                indent: child_indent,
126            };
127            child_ctx.render_nodes(&cause.children, None);
128        }
129
130        if body_shown {
131            self.push_line(Connector::Last, body);
132            let child_indent = self.child_indent(Connector::Last);
133            let mut child_ctx = FormatContext {
134                lines: self.lines,
135                indent: child_indent,
136            };
137            child_ctx.render_nodes(children, Some(body));
138        } else if !children.is_empty() {
139            self.render_nodes(children, None);
140        }
141    }
142
143    fn render_nodes(&mut self, nodes: &[ExplanationNode], parent_body: Option<&str>) {
144        let len = nodes.len();
145        for (i, node) in nodes.iter().enumerate() {
146            let connector = if i + 1 == len {
147                Connector::Last
148            } else {
149                Connector::Branch
150            };
151            self.render_node(node, connector, parent_body);
152        }
153    }
154
155    fn render_conversion_contents(
156        &mut self,
157        steps: &[SerializedConversionTraceStep],
158        operands: &[ExplanationNode],
159    ) {
160        let total = steps.len() + operands.len();
161        let mut index = 0;
162        for step in steps {
163            index += 1;
164            let connector = if index == total {
165                Connector::Last
166            } else {
167                Connector::Branch
168            };
169            self.push_line(connector, &step.text);
170        }
171        for operand in operands {
172            index += 1;
173            let connector = if index == total {
174                Connector::Last
175            } else {
176                Connector::Branch
177            };
178            self.render_node(operand, connector, None);
179        }
180    }
181
182    fn render_node(
183        &mut self,
184        node: &ExplanationNode,
185        connector: Connector,
186        parent_body: Option<&str>,
187    ) {
188        match node {
189            ExplanationNode::Rule {
190                name,
191                result,
192                body,
193                causes,
194                children,
195            } => {
196                let result_str = result
197                    .as_deref()
198                    .expect("BUG: ExplanationNode::Rule.result not filled by eval");
199                self.push_line(connector, &format!("{}: {result_str}", name.rule));
200                let child_indent = self.child_indent(connector);
201                let mut child_ctx = FormatContext {
202                    lines: self.lines,
203                    indent: child_indent,
204                };
205                child_ctx.render_rule_contents(result_str, body, causes, children);
206            }
207            ExplanationNode::Compose {
208                expression,
209                operands,
210            } => {
211                if parent_body.is_some_and(|body| body == expression) {
212                    self.render_nodes(operands, None);
213                } else {
214                    self.push_line(connector, expression);
215                    let child_indent = self.child_indent(connector);
216                    let mut child_ctx = FormatContext {
217                        lines: self.lines,
218                        indent: child_indent,
219                    };
220                    child_ctx.render_nodes(operands, None);
221                }
222            }
223            ExplanationNode::Data { name, display } => {
224                if name.data.is_empty() {
225                    self.push_line(connector, display);
226                } else {
227                    self.push_line(connector, &format!("{name}: {display}"));
228                }
229            }
230            ExplanationNode::DataUnused { name } => {
231                self.push_line(connector, &name.to_string());
232            }
233            ExplanationNode::Conversion {
234                expression,
235                steps,
236                operands,
237            } => {
238                let expression_is_parent_body = parent_body.is_some_and(|body| body == expression);
239                if expression_is_parent_body {
240                    let steps_without_outcome: Vec<SerializedConversionTraceStep> = steps
241                        .iter()
242                        .filter(|step| !matches!(step.role, ConversionTraceRole::Outcome))
243                        .cloned()
244                        .collect();
245                    self.render_conversion_contents(&steps_without_outcome, operands);
246                } else {
247                    self.push_line(connector, expression);
248                    let child_indent = self.child_indent(connector);
249                    let mut child_ctx = FormatContext {
250                        lines: self.lines,
251                        indent: child_indent,
252                    };
253                    child_ctx.render_conversion_contents(steps, operands);
254                }
255            }
256            ExplanationNode::Veto { message } => {
257                let text = match message.as_deref() {
258                    Some(msg) if !msg.is_empty() => format!("veto \"{msg}\""),
259                    _ => "veto".to_string(),
260                };
261                self.push_line(connector, &text);
262            }
263            ExplanationNode::UnitEquivalence { text } => {
264                self.push_line(connector, text);
265            }
266            ExplanationNode::Piecewise { .. } => {
267                unreachable!("BUG: Piecewise must be lowered before format")
268            }
269        }
270    }
271}
272
273fn connector_str(connector: Connector) -> &'static str {
274    match connector {
275        Connector::Branch => "├─",
276        Connector::Last => "└─",
277    }
278}