1use crate::computation::rational::RationalInteger;
10use crate::computation::{OperationResult, VetoType};
11use crate::planning::explanation::{ExplainedRule, ExplainedRuleId};
12use crate::planning::semantics::{LemmaType, RulePath};
13use crate::planning::unit_family::FamilyUnitCatalog;
14use crate::result_value::{
15 rule_result_value_failure_message, rule_result_value_from_literal, RuleResultValue,
16 RuleResultValueFailure,
17};
18use serde::Serialize;
19use std::collections::{HashMap, HashSet};
20use std::sync::Arc;
21
22pub use crate::planning::explanation::{
24 Cause, ConversionTraceRole, ExplanationNode, SerializedConversionTraceStep,
25};
26
27#[derive(Debug, Clone)]
28pub struct Explanation {
29 pub name: RulePath,
30 pub result: OperationResult,
31 pub result_type: Arc<LemmaType>,
33 pub(crate) family_units: Arc<FamilyUnitCatalog>,
35 pub(crate) runtime_scales: HashMap<String, RationalInteger>,
37 pub body: String,
38 pub causes: Vec<Cause>,
39 pub children: Vec<ExplanationNode>,
40 pub rules: Vec<ExplainedRule>,
42}
43
44#[derive(Serialize)]
45struct ExplanationSer<'a> {
46 #[serde(rename = "type")]
47 kind: &'static str,
48 #[serde(serialize_with = "crate::planning::explanation::serialize_rule_path_as_name")]
49 name: &'a RulePath,
50 #[serde(flatten)]
51 result: RuleResultValue,
52 body: &'a str,
53 #[serde(skip_serializing_if = "<[_]>::is_empty")]
54 causes: &'a [Cause],
55 #[serde(skip_serializing_if = "<[_]>::is_empty")]
56 children: &'a [ExplanationNode],
57 #[serde(skip_serializing_if = "<[_]>::is_empty")]
58 rules: &'a [ExplainedRule],
59}
60
61impl Serialize for Explanation {
62 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
63 where
64 S: serde::Serializer,
65 {
66 ExplanationSer {
67 kind: "rule",
68 name: &self.name,
69 result: explanation_result_value(
70 &self.result,
71 self.result_type.as_ref(),
72 &self.family_units,
73 Some(&self.runtime_scales),
74 ),
75 body: &self.body,
76 causes: self.causes.as_slice(),
77 children: self.children.as_slice(),
78 rules: self.rules.as_slice(),
79 }
80 .serialize(serializer)
81 }
82}
83
84pub(crate) fn format_operation_result(result: &OperationResult, result_type: &LemmaType) -> String {
85 match result {
86 OperationResult::Value(bound) => bound.to_literal().display_value_with_type(result_type),
87 OperationResult::Veto(VetoType::UserDefined { message: None }) => String::new(),
88 OperationResult::Veto(veto) => veto.to_string(),
89 }
90}
91
92pub(crate) fn explanation_result_value(
94 result: &OperationResult,
95 lemma_type: &LemmaType,
96 family_units: &FamilyUnitCatalog,
97 runtime_scales: Option<&HashMap<String, RationalInteger>>,
98) -> RuleResultValue {
99 match result {
100 OperationResult::Value(bound) => {
101 match rule_result_value_from_literal(
102 &bound.to_literal(),
103 lemma_type,
104 family_units,
105 runtime_scales,
106 ) {
107 Ok(value) => value,
108 Err(RuleResultValueFailure::DecimalLimit) => RuleResultValue {
110 result: Some(
111 rule_result_value_failure_message(RuleResultValueFailure::DecimalLimit)
112 .to_string(),
113 ),
114 ..RuleResultValue::default()
115 },
116 Err(failure) => panic!(
117 "BUG: settled explanation value failed rule_result_value_from_literal: {}",
118 rule_result_value_failure_message(failure)
119 ),
120 }
121 }
122 OperationResult::Veto(VetoType::UserDefined { message: None }) => RuleResultValue {
123 result: Some(String::new()),
124 ..RuleResultValue::default()
125 },
126 OperationResult::Veto(veto) => RuleResultValue {
127 result: Some(veto.to_string()),
128 ..RuleResultValue::default()
129 },
130 }
131}
132
133pub fn format_explanation(explanation: &Explanation) -> String {
134 let mut lines = Vec::new();
135 let result_line = explanation_result_value(
136 &explanation.result,
137 explanation.result_type.as_ref(),
138 &explanation.family_units,
139 Some(&explanation.runtime_scales),
140 )
141 .result
142 .expect("BUG: root explanation must carry result");
143 lines.push(format!("{}: {}", explanation.name, result_line));
144 let mut state = FormatState {
145 lines: &mut lines,
146 rules: &explanation.rules,
147 expanded: HashSet::new(),
148 stack: Vec::new(),
149 };
150 state.push_rule_contents(
151 &result_line,
152 &explanation.body,
153 &explanation.causes,
154 &explanation.children,
155 String::new(),
156 );
157 state.run();
158 lines.join("\n")
159}
160
161#[derive(Copy, Clone)]
162enum Connector {
163 Branch,
164 Last,
165}
166
167enum ConversionSteps {
168 Owned(Vec<SerializedConversionTraceStep>),
169}
170
171impl ConversionSteps {
172 fn as_slice(&self) -> &[SerializedConversionTraceStep] {
173 match self {
174 ConversionSteps::Owned(steps) => steps.as_slice(),
175 }
176 }
177}
178
179struct Siblings<'a> {
182 nodes: Vec<&'a ExplanationNode>,
183 row_headlines: std::collections::HashMap<usize, String>,
185}
186
187impl<'a> Siblings<'a> {
188 fn new(nodes: impl Iterator<Item = &'a ExplanationNode>) -> Self {
189 let mut flat = Vec::new();
190 for node in nodes {
191 flatten_visible(node, &mut flat);
192 }
193 let row_headlines = aligned_row_headlines(&flat);
194 Self {
195 nodes: flat,
196 row_headlines,
197 }
198 }
199
200 fn len(&self) -> usize {
201 self.nodes.len()
202 }
203}
204
205fn flatten_visible<'a>(node: &'a ExplanationNode, out: &mut Vec<&'a ExplanationNode>) {
206 let mut pending = vec![node];
207 while let Some(node) = pending.pop() {
208 if is_bare_literal_compose(node) || is_visually_empty_compose(node) {
209 continue;
210 }
211 match node {
212 ExplanationNode::Filter { children, .. } => {
213 for child in children.iter().rev() {
214 pending.push(child);
215 }
216 }
217 _ => out.push(node),
218 }
219 }
220}
221
222enum Work<'a> {
223 Line {
224 indent: String,
225 connector: Connector,
226 text: String,
227 },
228 RuleBody {
229 id: ExplainedRuleId,
230 result_line: String,
231 indent: String,
232 },
233 Nodes {
234 siblings: Siblings<'a>,
235 parent_body: Option<&'a str>,
236 indent: String,
237 next: usize,
238 elide_list_commands: bool,
239 },
240 CauseChildren {
241 children: Vec<&'a ExplanationNode>,
242 indent: String,
243 next: usize,
244 },
245 Conversion {
246 steps: ConversionSteps,
247 siblings: Siblings<'a>,
248 indent: String,
249 next: usize,
250 },
251}
252
253struct FormatState<'a> {
254 lines: &'a mut Vec<String>,
255 rules: &'a [ExplainedRule],
256 expanded: HashSet<ExplainedRuleId>,
257 stack: Vec<Work<'a>>,
258}
259
260impl<'a> FormatState<'a> {
261 fn push_line(&mut self, indent: &str, connector: Connector, text: &str) {
262 self.lines
263 .push(format!("{indent}{} {text}", connector_str(connector)));
264 }
265
266 fn child_indent(indent: &str, connector: Connector) -> String {
267 match connector {
268 Connector::Branch => format!("{indent}│ "),
269 Connector::Last => format!("{indent} "),
270 }
271 }
272
273 fn rule_by_id(&self, id: ExplainedRuleId) -> &'a ExplainedRule {
274 self.rules
275 .iter()
276 .find(|rule| rule.id == id)
277 .unwrap_or_else(|| panic!("BUG: explanation missing rules entry for id {}", id.0))
278 }
279
280 fn push_rule_contents(
281 &mut self,
282 result_line: &str,
283 body: &'a str,
284 causes: &'a [Cause],
285 children: &'a [ExplanationNode],
286 indent: String,
287 ) {
288 let body_restated_by_sole_data = matches!(
291 children,
292 [ExplanationNode::Data { name, .. }] if name.input_key() == body
293 );
294 let body_shown = !body.is_empty() && body != result_line && !body_restated_by_sole_data;
295 let total = causes.len() + usize::from(body_shown);
296 if body_shown {
298 let child_indent = Self::child_indent(&indent, Connector::Last);
299 self.stack.push(Work::Nodes {
300 siblings: Siblings::new(children.iter()),
301 parent_body: Some(body),
302 indent: child_indent,
303 next: 0,
304 elide_list_commands: false,
305 });
306 self.stack.push(Work::Line {
307 indent: indent.clone(),
308 connector: Connector::Last,
309 text: body.to_string(),
310 });
311 } else if !children.is_empty() {
312 self.stack.push(Work::Nodes {
313 siblings: Siblings::new(children.iter()),
314 parent_body: None,
315 indent: indent.clone(),
316 next: 0,
317 elide_list_commands: false,
318 });
319 }
320 for (i, cause) in causes.iter().enumerate().rev() {
321 let connector = if i + 1 == total {
322 Connector::Last
323 } else {
324 Connector::Branch
325 };
326 let value = cause.value.as_str();
327 let line = if value == "true" {
328 cause.condition.clone()
329 } else {
330 format!("{} is {}", cause.condition, value)
331 };
332 let visible: Vec<&'a ExplanationNode> = cause
333 .children
334 .iter()
335 .filter(|node| !is_bare_literal_compose(node))
336 .filter(|node| !data_restates_true_cause_line(node, value, &line))
337 .collect();
338 let child_indent = Self::child_indent(&indent, connector);
339 self.stack.push(Work::CauseChildren {
340 children: visible,
341 indent: child_indent,
342 next: 0,
343 });
344 self.stack.push(Work::Line {
345 indent: indent.clone(),
346 connector,
347 text: line,
348 });
349 }
350 }
351
352 fn run(&mut self) {
353 while let Some(work) = self.stack.pop() {
354 match work {
355 Work::Line {
356 indent,
357 connector,
358 text,
359 } => self.push_line(&indent, connector, &text),
360 Work::RuleBody {
361 id,
362 result_line,
363 indent,
364 } => {
365 let rule = self.rule_by_id(id);
366 self.push_rule_contents(
367 &result_line,
368 rule.body.as_str(),
369 rule.causes.as_slice(),
370 rule.children.as_slice(),
371 indent,
372 );
373 }
374 Work::Nodes {
375 siblings,
376 parent_body,
377 indent,
378 next,
379 elide_list_commands,
380 } => self.step_nodes(siblings, parent_body, indent, next, elide_list_commands),
381 Work::CauseChildren {
382 children,
383 indent,
384 next,
385 } => self.step_cause_children(children, indent, next),
386 Work::Conversion {
387 steps,
388 siblings,
389 indent,
390 next,
391 } => self.step_conversion(steps, siblings, indent, next),
392 }
393 }
394 }
395
396 fn step_nodes(
397 &mut self,
398 siblings: Siblings<'a>,
399 parent_body: Option<&'a str>,
400 indent: String,
401 next: usize,
402 elide_list_commands: bool,
403 ) {
404 if next >= siblings.len() {
405 return;
406 }
407 let connector = if next + 1 == siblings.len() {
408 Connector::Last
409 } else {
410 Connector::Branch
411 };
412 let node = siblings.nodes[next];
413 let row_headline = siblings.row_headlines.get(&next).cloned();
414 self.stack.push(Work::Nodes {
415 siblings,
416 parent_body,
417 indent: indent.clone(),
418 next: next + 1,
419 elide_list_commands,
420 });
421 self.emit_node(
422 node,
423 connector,
424 parent_body,
425 indent,
426 elide_list_commands,
427 row_headline,
428 );
429 }
430
431 fn step_cause_children(
432 &mut self,
433 children: Vec<&'a ExplanationNode>,
434 indent: String,
435 next: usize,
436 ) {
437 if next >= children.len() {
438 return;
439 }
440 let connector = if next + 1 == children.len() {
441 Connector::Last
442 } else {
443 Connector::Branch
444 };
445 let node = children[next];
446 self.stack.push(Work::CauseChildren {
447 children,
448 indent: indent.clone(),
449 next: next + 1,
450 });
451 self.emit_node(node, connector, None, indent, false, None);
452 }
453
454 fn step_conversion(
455 &mut self,
456 steps: ConversionSteps,
457 siblings: Siblings<'a>,
458 indent: String,
459 next: usize,
460 ) {
461 let sibling_len = siblings.len();
462 let step_len = steps.as_slice().len();
463 let total = step_len + sibling_len;
464 if next >= total {
465 return;
466 }
467 let connector = if next + 1 == total {
468 Connector::Last
469 } else {
470 Connector::Branch
471 };
472 if next < sibling_len {
473 let node = siblings.nodes[next];
474 let row_headline = siblings.row_headlines.get(&next).cloned();
475 self.stack.push(Work::Conversion {
476 steps,
477 siblings,
478 indent: indent.clone(),
479 next: next + 1,
480 });
481 self.emit_node(node, connector, None, indent, false, row_headline);
482 return;
483 }
484 let step_index = next - sibling_len;
485 let text = steps.as_slice()[step_index].text.clone();
486 self.stack.push(Work::Conversion {
487 steps,
488 siblings,
489 indent: indent.clone(),
490 next: next + 1,
491 });
492 self.push_line(&indent, connector, &text);
493 }
494
495 fn emit_node(
496 &mut self,
497 node: &'a ExplanationNode,
498 connector: Connector,
499 parent_body: Option<&'a str>,
500 indent: String,
501 elide_list_commands: bool,
502 row_headline: Option<String>,
503 ) {
504 match node {
505 ExplanationNode::RuleRef { id, name, result } => {
506 let result_str = result
507 .result
508 .as_deref()
509 .expect("BUG: ExplanationNode::RuleRef.result.result not filled by eval");
510 if self.expanded.insert(*id) {
511 self.push_line(&indent, connector, &format!("{name}: {result_str}"));
512 self.stack.push(Work::RuleBody {
513 id: *id,
514 result_line: result_str.to_string(),
515 indent: Self::child_indent(&indent, connector),
516 });
517 } else {
518 self.push_line(
519 &indent,
520 connector,
521 &format!("{name}: {result_str} (see above)"),
522 );
523 }
524 }
525 ExplanationNode::Compose {
526 expression,
527 operands,
528 result: _,
529 ..
530 } => {
531 let siblings = Siblings::new(operands.iter());
532 if parent_body.is_some_and(|body| body == expression.as_str()) {
533 self.stack.push(Work::Nodes {
534 siblings,
535 parent_body: None,
536 indent,
537 next: 0,
538 elide_list_commands,
539 });
540 } else {
541 self.push_line(&indent, connector, expression);
542 let child_indent = Self::child_indent(&indent, connector);
543 self.stack.push(Work::Nodes {
544 siblings,
545 parent_body: None,
546 indent: child_indent,
547 next: 0,
548 elide_list_commands,
549 });
550 }
551 }
552 ExplanationNode::ListCommand {
553 expression,
554 operands,
555 result: _,
556 ..
557 } => {
558 let elide = elide_list_commands
559 || parent_body.is_some_and(|body| body == expression.as_str());
560 if elide {
561 self.stack.push(Work::Nodes {
562 siblings: Siblings::new(operands.iter()),
563 parent_body: None,
564 indent,
565 next: 0,
566 elide_list_commands: true,
567 });
568 } else {
569 self.push_line(&indent, connector, expression);
570 let child_indent = Self::child_indent(&indent, connector);
571 self.stack.push(Work::Nodes {
572 siblings: Siblings::new(operands.iter()),
573 parent_body: None,
574 indent: child_indent,
575 next: 0,
576 elide_list_commands: true,
577 });
578 }
579 }
580 ExplanationNode::Data { name, result } => {
581 let result_str = result
582 .result
583 .as_deref()
584 .expect("BUG: ExplanationNode::Data.result.result not filled by eval");
585 if name.data.is_empty() {
586 self.push_line(&indent, connector, result_str);
587 } else {
588 self.push_line(&indent, connector, &format!("{name}: {result_str}"));
589 }
590 }
591 ExplanationNode::DataUnused { name } => {
592 self.push_line(&indent, connector, &name.to_string());
593 }
594 ExplanationNode::Conversion {
595 expression,
596 steps,
597 operands,
598 result: _,
599 } => {
600 let expression_is_parent_body =
601 parent_body.is_some_and(|body| body == expression.as_str());
602 let rule_steps: Vec<SerializedConversionTraceStep> = steps
603 .iter()
604 .filter(|step| !matches!(step.role, ConversionTraceRole::Outcome))
605 .cloned()
606 .collect();
607 let siblings = if let [ExplanationNode::Compose {
611 operands: compose_operands,
612 ..
613 }] = operands.as_slice()
614 {
615 Siblings::new(compose_operands.iter())
616 } else {
617 Siblings::new(operands.iter())
618 };
619 if expression_is_parent_body {
620 self.stack.push(Work::Conversion {
621 steps: ConversionSteps::Owned(rule_steps),
622 siblings,
623 indent,
624 next: 0,
625 });
626 } else {
627 self.push_line(&indent, connector, expression);
628 let child_indent = Self::child_indent(&indent, connector);
629 self.stack.push(Work::Conversion {
630 steps: ConversionSteps::Owned(rule_steps),
631 siblings,
632 indent: child_indent,
633 next: 0,
634 });
635 }
636 }
637 ExplanationNode::Veto { message } => {
638 let text = match message.as_deref() {
639 Some(msg) if !msg.is_empty() => format!("veto \"{msg}\""),
640 _ => "veto".to_string(),
641 };
642 self.push_line(&indent, connector, &text);
643 }
644 ExplanationNode::Row {
645 number,
646 appended,
647 key,
648 fields,
649 } => {
650 let tree_fields = row_tree_fields(key, fields);
651 if key.is_empty()
653 && row_comma_columns(key, fields).is_empty()
654 && tree_fields.len() == 1
655 {
656 if let ExplanationNode::RuleRef { id, name, result } = tree_fields[0] {
657 let result_str = result
658 .result
659 .as_deref()
660 .expect("BUG: RuleRef.result.result not filled by eval");
661 let prefix = if *appended {
662 format!("appended {number}.")
663 } else {
664 format!("{number}.")
665 };
666 let child_indent = Self::child_indent(&indent, connector);
667 if self.expanded.insert(*id) {
668 self.push_line(
669 &indent,
670 connector,
671 &format!("{prefix} {name}: {result_str}"),
672 );
673 self.stack.push(Work::RuleBody {
674 id: *id,
675 result_line: result_str.to_string(),
676 indent: child_indent,
677 });
678 } else {
679 self.push_line(
680 &indent,
681 connector,
682 &format!("{prefix} {name}: {result_str} (see above)"),
683 );
684 }
685 return;
686 }
687 }
688 let headline = row_headline
689 .unwrap_or_else(|| row_headline_for(*number, *appended, key, fields));
690 self.push_line(&indent, connector, &headline);
691 if !tree_fields.is_empty() {
692 let child_indent = Self::child_indent(&indent, connector);
693 self.stack.push(Work::Nodes {
694 siblings: Siblings::new(tree_fields.into_iter()),
695 parent_body: None,
696 indent: child_indent,
697 next: 0,
698 elide_list_commands,
699 });
700 }
701 }
702 ExplanationNode::Filter { .. } => {
703 unreachable!("BUG: Filter nodes are flattened in Siblings::new");
704 }
705 }
706 }
707}
708
709fn connector_str(connector: Connector) -> &'static str {
710 match connector {
711 Connector::Branch => "├─",
712 Connector::Last => "└─",
713 }
714}
715
716fn aligned_row_headlines(nodes: &[&ExplanationNode]) -> std::collections::HashMap<usize, String> {
717 let mut out = std::collections::HashMap::new();
718 let mut index = 0;
719 while index < nodes.len() {
720 if !matches!(nodes[index], ExplanationNode::Row { .. }) {
721 index += 1;
722 continue;
723 }
724 let start = index;
725 while index < nodes.len() && matches!(nodes[index], ExplanationNode::Row { .. }) {
726 index += 1;
727 }
728 let rows = &nodes[start..index];
729 let branched = rows.iter().any(|node| {
730 let ExplanationNode::Row { key, fields, .. } = node else {
731 unreachable!("BUG: row run contains non-Row");
732 };
733 !row_tree_fields(key, fields).is_empty()
734 });
735 let columns: Vec<Vec<RowColumn>> = rows
736 .iter()
737 .map(|node| {
738 let ExplanationNode::Row { key, fields, .. } = node else {
739 unreachable!("BUG: row run contains non-Row");
740 };
741 row_comma_columns(key, fields)
742 })
743 .collect();
744 let col_widths = if branched {
745 Vec::new()
746 } else {
747 let width = columns.iter().map(|cols| cols.len()).max().unwrap_or(0);
748 let mut widths = vec![0; width];
749 for cols in &columns {
750 for (i, column) in cols.iter().enumerate() {
751 widths[i] = widths[i].max(column.text.len());
752 }
753 }
754 widths
755 };
756 for (offset, (node, cols)) in rows.iter().zip(columns.iter()).enumerate() {
757 let ExplanationNode::Row {
758 number, appended, ..
759 } = node
760 else {
761 unreachable!("BUG: row run contains non-Row");
762 };
763 let line = if branched {
764 cols.iter()
765 .map(|column| column.text.as_str())
766 .collect::<Vec<_>>()
767 .join(", ")
768 } else {
769 let mut line = String::new();
770 for (i, column) in cols.iter().enumerate() {
771 let text = &column.text;
772 if i + 1 == cols.len() {
773 line.push_str(text);
774 } else {
775 line.push_str(text);
776 line.push(',');
777 let pad = col_widths[i].saturating_sub(text.len()) + 1;
778 line.push_str(&" ".repeat(pad));
779 }
780 }
781 line
782 };
783 let all_appended = rows
784 .iter()
785 .all(|row| matches!(row, ExplanationNode::Row { appended: true, .. }));
786 let headline = if key_is_empty(node) && !all_appended {
787 let prefix = if *appended {
788 format!("appended {number}.")
789 } else {
790 format!("{number}.")
791 };
792 if line.is_empty() {
793 prefix
794 } else {
795 format!("{prefix} {line}")
796 }
797 } else {
798 line
799 };
800 out.insert(start + offset, headline);
801 }
802 }
803 out
804}
805
806fn key_is_empty(node: &ExplanationNode) -> bool {
807 matches!(node, ExplanationNode::Row { key, .. } if key.is_empty())
808}
809
810fn row_headline_for(
811 number: usize,
812 appended: bool,
813 key: &[ExplanationNode],
814 fields: &[ExplanationNode],
815) -> String {
816 let cols = row_comma_columns(key, fields);
817 let comma = cols
818 .iter()
819 .map(|column| column.text.as_str())
820 .collect::<Vec<_>>()
821 .join(", ");
822 if key.is_empty() {
823 let prefix = if appended {
824 format!("appended {number}.")
825 } else {
826 format!("{number}.")
827 };
828 if comma.is_empty() {
829 prefix
830 } else {
831 format!("{prefix} {comma}")
832 }
833 } else {
834 comma
835 }
836}
837
838struct RowColumn {
840 name: String,
841 text: String,
842}
843
844fn row_comma_columns(key: &[ExplanationNode], fields: &[ExplanationNode]) -> Vec<RowColumn> {
845 let mut cols = Vec::new();
846 let mut seen = std::collections::HashSet::new();
847 for node in key {
848 let column = row_column(node);
849 seen.insert(column.name.clone());
850 cols.push(column);
851 }
852 for node in fields {
853 if !is_comma_field(node) {
854 continue;
855 }
856 let column = row_column(node);
857 if seen.insert(column.name.clone()) {
858 cols.push(column);
859 }
860 }
861 cols
862}
863
864fn row_tree_fields<'a>(
865 key: &'a [ExplanationNode],
866 fields: &'a [ExplanationNode],
867) -> Vec<&'a ExplanationNode> {
868 let _ = key;
869 fields.iter().filter(|node| !is_comma_field(node)).collect()
870}
871
872fn is_comma_field(node: &ExplanationNode) -> bool {
873 matches!(
874 node,
875 ExplanationNode::Data { .. }
876 | ExplanationNode::DataUnused { .. }
877 | ExplanationNode::Veto { .. }
878 )
879}
880
881fn row_column(node: &ExplanationNode) -> RowColumn {
882 match node {
883 ExplanationNode::Data { name, result } => {
884 let value = result
885 .result
886 .as_deref()
887 .expect("BUG: row Data must carry result");
888 RowColumn {
889 name: name.to_string(),
890 text: format!("{name}: {value}"),
891 }
892 }
893 ExplanationNode::DataUnused { name } => RowColumn {
894 name: name.to_string(),
895 text: name.to_string(),
896 },
897 ExplanationNode::Veto { message } => {
898 let value = match message.as_deref() {
899 Some(msg) if !msg.is_empty() => format!("veto \"{msg}\""),
900 _ => "veto".to_string(),
901 };
902 RowColumn {
903 name: String::new(),
904 text: format!(": {value}"),
905 }
906 }
907 other => panic!("BUG: comma column node is {other:?}"),
908 }
909}
910
911fn is_bare_literal_compose(node: &ExplanationNode) -> bool {
913 matches!(node, ExplanationNode::Compose { operands, .. } if operands.is_empty())
914}
915
916fn is_visually_empty_compose(node: &ExplanationNode) -> bool {
921 let ExplanationNode::Compose { operands, .. } = node else {
922 return false;
923 };
924 operands.iter().all(operand_omitted_from_tree)
925}
926
927fn operand_omitted_from_tree(node: &ExplanationNode) -> bool {
928 if is_bare_literal_compose(node) {
929 return true;
930 }
931 is_visually_empty_compose(node)
932}
933
934fn data_restates_true_cause_line(node: &ExplanationNode, value: &str, line: &str) -> bool {
937 if value != "true" {
938 return false;
939 }
940 match node {
941 ExplanationNode::Data {
942 name,
943 result: node_result,
944 } => {
945 let result_str = node_result
946 .result
947 .as_deref()
948 .expect("BUG: Data node must carry result");
949 line == format!("{} is {result_str}", name.input_key())
950 }
951 _ => false,
952 }
953}