1use crate::computation::rational::checked_div;
9use crate::computation::UnitResolutionContext;
10use crate::evaluation::expression::resolve_data_path_value;
11use crate::evaluation::operations::{OperationResult, VetoType};
12use crate::evaluation::{BranchDecision, EvaluationContext, RuleRecording};
13use crate::planning::execution_plan::{
14 ArmRole, ExecutableRule, ExecutionPlan, Instruction, Instructions,
15};
16use crate::planning::semantics::{
17 compare_semantic_dates, negated_comparison, ArithmeticComputation, ComparisonComputation,
18 DataPath, Expression, ExpressionKind, LemmaType, LiteralValue, NegationType, RulePath,
19 SemanticConversionTarget, TypeSpecification, ValueKind,
20};
21use serde::ser::SerializeMap;
22use serde::{Serialize, Serializer};
23use std::cmp::Ordering;
24use std::collections::HashMap;
25
26fn serialize_rule_name<S>(path: &RulePath, serializer: S) -> Result<S::Ok, S::Error>
27where
28 S: Serializer,
29{
30 serializer.serialize_str(&path.rule)
31}
32
33fn serialize_data_input_key<S>(path: &DataPath, serializer: S) -> Result<S::Ok, S::Error>
34where
35 S: Serializer,
36{
37 serializer.serialize_str(&path.input_key())
38}
39
40#[derive(Debug, Clone)]
41pub struct Explanation {
42 pub rule: RulePath,
43 pub result: OperationResult,
44 pub body: String,
45 pub causes: Vec<Cause>,
46 pub children: Vec<ExplanationNode>,
47}
48
49#[derive(Debug, Clone, Serialize)]
50#[serde(tag = "type", rename_all = "snake_case")]
51pub enum ExplanationNode {
52 Rule {
53 #[serde(serialize_with = "serialize_rule_name")]
54 rule: RulePath,
55 result: String,
56 body: String,
57 #[serde(skip_serializing_if = "Vec::is_empty")]
58 causes: Vec<Cause>,
59 #[serde(skip_serializing_if = "Vec::is_empty")]
60 children: Vec<ExplanationNode>,
61 },
62 Compose {
63 expression: String,
64 operands: Vec<ExplanationNode>,
65 },
66 DataInput {
67 #[serde(serialize_with = "serialize_data_input_key")]
68 data: DataPath,
69 display: String,
70 },
71 Conversion {
72 expression: String,
73 steps: Vec<SerializedConversionTraceStep>,
74 operands: Vec<ExplanationNode>,
75 },
76 Veto {
77 #[serde(skip_serializing_if = "Option::is_none")]
78 message: Option<String>,
79 },
80 UnitEquivalence { text: String },
86}
87
88#[derive(Debug, Clone, Serialize)]
97pub struct Cause {
98 pub condition: String,
99 pub value: String,
100 #[serde(skip_serializing_if = "Vec::is_empty")]
101 pub children: Vec<ExplanationNode>,
102}
103
104#[derive(Debug, Clone)]
105pub enum ConversionTraceRole {
106 Outcome,
107 Rule,
108 Source,
109}
110
111#[derive(Debug, Clone)]
112pub struct ConversionTraceStep {
113 pub role: ConversionTraceRole,
114 pub text: String,
115 pub data_ref: Option<DataPath>,
116}
117
118fn build_conversion_steps(
119 value: &LiteralValue,
120 target: &SemanticConversionTarget,
121 result: &LiteralValue,
122 data_ref: Option<&DataPath>,
123 resolution_context: UnitResolutionContext<'_>,
124) -> Vec<ConversionTraceStep> {
125 let mut steps = Vec::new();
126 steps.push(ConversionTraceStep {
127 role: ConversionTraceRole::Outcome,
128 text: result.to_string(),
129 data_ref: None,
130 });
131
132 if let Some(rule_text) = conversion_rule_step_text(value, target, result, resolution_context) {
133 steps.push(ConversionTraceStep {
134 role: ConversionTraceRole::Rule,
135 text: rule_text,
136 data_ref: None,
137 });
138 }
139
140 if value.to_string() != result.to_string() {
143 steps.push(ConversionTraceStep {
144 role: ConversionTraceRole::Source,
145 text: conversion_source_step_text(value, data_ref),
146 data_ref: data_ref.cloned(),
147 });
148 }
149
150 steps
151}
152
153fn conversion_source_step_text(operand: &LiteralValue, data_ref: Option<&DataPath>) -> String {
154 let type_name = type_specification_display_name(&operand.lemma_type);
155 let value_display = operand.to_string();
156 match data_ref {
157 Some(path) => format!("The {type_name} of {path} is {value_display}"),
158 None => format!("The {type_name} is {value_display}"),
159 }
160}
161
162fn type_specification_display_name(lemma_type: &LemmaType) -> &'static str {
163 match &lemma_type.specifications {
164 TypeSpecification::Boolean { .. } => "boolean",
165 TypeSpecification::Measure { .. } => "measure",
166 TypeSpecification::MeasureRange { .. } => "measure range",
167 TypeSpecification::Number { .. } => "number",
168 TypeSpecification::NumberRange { .. } => "number range",
169 TypeSpecification::Text { .. } => "text",
170 TypeSpecification::Date { .. } => "date",
171 TypeSpecification::DateRange { .. } => "date range",
172 TypeSpecification::TimeRange { .. } => "time range",
173 TypeSpecification::Time { .. } => "time",
174 TypeSpecification::Ratio { .. } => "ratio",
175 TypeSpecification::RatioRange { .. } => "ratio range",
176 TypeSpecification::Veto { .. } => "veto",
177 TypeSpecification::Undetermined => "undetermined",
178 }
179}
180
181fn conversion_rule_step_text(
182 value: &LiteralValue,
183 target: &SemanticConversionTarget,
184 result: &LiteralValue,
185 resolution_context: UnitResolutionContext<'_>,
186) -> Option<String> {
187 match &value.value {
188 ValueKind::Range(left, right) => range_span_rule_step_text(left, right, result),
189 ValueKind::Measure(_, from_signature) if !value.lemma_type.is_calendar_like() => {
190 match target {
191 SemanticConversionTarget::Unit {
192 unit_name,
193 owning_type,
194 } => measure_unit_equivalence_step_text(
195 from_signature,
196 unit_name,
197 &value.lemma_type,
198 owning_type.as_ref(),
199 resolution_context,
200 ),
201 _ => None,
202 }
203 }
204 ValueKind::Number(_) => None,
205 ValueKind::Ratio(_, _) => None,
206 ValueKind::Measure(_, _) if value.lemma_type.is_calendar_like() => None,
207 _ => None,
208 }
209}
210
211fn format_explanation_multiplier(
212 rational: &crate::computation::rational::RationalInteger,
213) -> String {
214 use crate::computation::rational::{
215 commit_rational_to_decimal, decimal_to_rational, RationalInteger,
216 };
217 let reduced = RationalInteger::try_reduce_ref(rational).unwrap_or_else(|_| rational.clone());
218 if reduced.denom() == &crate::computation::rational::BigInt::one() {
219 return reduced.numer().to_string();
220 }
221 if let Ok(decimal) = commit_rational_to_decimal(&reduced) {
224 if decimal_to_rational(decimal).is_ok_and(|round_trip| round_trip == reduced) {
225 return decimal.normalize().to_string();
226 }
227 }
228 format!("{}/{}", reduced.numer(), reduced.denom())
229}
230
231fn measure_unit_equivalence_step_text(
232 from_signature: &[(String, i32)],
233 to_unit: &str,
234 lemma_type: &LemmaType,
235 target_owning_type: &LemmaType,
236 resolution_context: UnitResolutionContext<'_>,
237) -> Option<String> {
238 let from_unit = from_signature
239 .first()
240 .map(|(name, _)| name.as_str())
241 .unwrap_or("");
242
243 let both_units_in_lemma_type = match &lemma_type.specifications {
244 TypeSpecification::Measure { units, .. } => {
245 !from_unit.is_empty()
246 && from_signature.len() == 1
247 && units.get(from_unit).is_ok()
248 && units.get(to_unit).is_ok()
249 }
250 _ => false,
251 };
252
253 if both_units_in_lemma_type {
254 let from_factor = lemma_type.measure_unit_factor(from_unit);
255 let to_factor = lemma_type.measure_unit_factor(to_unit);
256 let multiplier = checked_div(from_factor, to_factor).ok()?;
257 let multiplier_display = format_explanation_multiplier(&multiplier);
258 if multiplier_display == "1" {
259 return None;
260 }
261 return Some(format!("1 {from_unit} is {multiplier_display} {to_unit}"));
262 }
263
264 let to_factor = target_owning_type.measure_unit_factor(to_unit).clone();
265 let UnitResolutionContext::WithIndex(unit_index) = resolution_context else {
266 return None;
267 };
268 let from_factor =
269 crate::planning::semantics::signature_factor(from_signature, unit_index, None).ok()?;
270 let multiplier = checked_div(&from_factor, &to_factor).ok()?;
271 let multiplier_display = format_explanation_multiplier(&multiplier);
272 if multiplier_display == "1" {
273 return None;
274 }
275 let source_label = crate::planning::semantics::format_signature_operator_style(from_signature);
276 Some(format!(
277 "1 {source_label} is {multiplier_display} {to_unit}"
278 ))
279}
280
281fn range_span_rule_step_text(
282 left: &LiteralValue,
283 right: &LiteralValue,
284 result: &LiteralValue,
285) -> Option<String> {
286 match (&left.value, &right.value) {
287 (ValueKind::Date(left_date), ValueKind::Date(right_date)) => {
288 let (lower, upper) = ordered_date_pair(left_date, right_date);
289 let lower_literal = LiteralValue::date(lower.clone());
290 let upper_literal = LiteralValue::date(upper.clone());
291 Some(format!("{upper_literal} − {lower_literal} = {result}"))
292 }
293 (ValueKind::Number(_), ValueKind::Number(_)) => {
294 let (lower, upper) = ordered_number_pair(left, right);
295 Some(format!("{upper} − {lower} = {result}"))
296 }
297 (ValueKind::Measure(_, _), ValueKind::Measure(_, _)) => {
298 let (lower, upper) = ordered_measure_pair(left, right);
299 Some(format!("{upper} − {lower} = {result}"))
300 }
301 _ => None,
302 }
303}
304
305fn ordered_date_pair<'a>(
306 left: &'a crate::planning::semantics::SemanticDateTime,
307 right: &'a crate::planning::semantics::SemanticDateTime,
308) -> (
309 &'a crate::planning::semantics::SemanticDateTime,
310 &'a crate::planning::semantics::SemanticDateTime,
311) {
312 match compare_semantic_dates(left, right) {
313 Ordering::Less | Ordering::Equal => (left, right),
314 Ordering::Greater => (right, left),
315 }
316}
317
318fn ordered_number_pair<'a>(
319 left: &'a LiteralValue,
320 right: &'a LiteralValue,
321) -> (&'a LiteralValue, &'a LiteralValue) {
322 let ValueKind::Number(left_number) = &left.value else {
323 unreachable!("BUG: ordered_number_pair called with non-number operand");
324 };
325 let ValueKind::Number(right_number) = &right.value else {
326 unreachable!("BUG: ordered_number_pair called with non-number operand");
327 };
328 if left_number <= right_number {
329 (left, right)
330 } else {
331 (right, left)
332 }
333}
334
335fn ordered_measure_pair<'a>(
336 left: &'a LiteralValue,
337 right: &'a LiteralValue,
338) -> (&'a LiteralValue, &'a LiteralValue) {
339 let ValueKind::Measure(left_magnitude, _) = &left.value else {
340 unreachable!("BUG: ordered_measure_pair called with non-measure operand");
341 };
342 let ValueKind::Measure(right_magnitude, _) = &right.value else {
343 unreachable!("BUG: ordered_measure_pair called with non-measure operand");
344 };
345 if *left_magnitude <= *right_magnitude {
346 (left, right)
347 } else {
348 (right, left)
349 }
350}
351
352#[derive(Debug, Clone, Serialize)]
353pub struct SerializedConversionTraceStep {
354 role: String,
355 text: String,
356}
357
358impl Explanation {
359 fn as_rule_node(&self) -> ExplanationNode {
360 ExplanationNode::Rule {
361 rule: self.rule.clone(),
362 result: format_operation_result(&self.result),
363 body: self.body.clone(),
364 causes: self.causes.clone(),
365 children: self.children.clone(),
366 }
367 }
368}
369
370impl Serialize for Explanation {
371 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
372 where
373 S: serde::Serializer,
374 {
375 let mut map = serializer.serialize_map(None)?;
376 map.serialize_entry("rule", &self.rule.rule)?;
377 map.serialize_entry("result", &format_operation_result(&self.result))?;
378 map.serialize_entry("body", &self.body)?;
379 if !self.causes.is_empty() {
380 map.serialize_entry("causes", &self.causes)?;
381 }
382 map.serialize_entry("children", &self.children)?;
383 map.end()
384 }
385}
386
387fn format_operation_result(result: &OperationResult) -> String {
388 match result {
389 OperationResult::Value(value) => value.display_value(),
390 OperationResult::Veto(VetoType::UserDefined { message: None }) => String::new(),
391 OperationResult::Veto(veto) => veto.to_string(),
392 }
393}
394
395enum WinningSourceBranch<'a> {
397 BranchResult {
399 result_expression: &'a Expression,
400 causes: Vec<Cause>,
401 },
402 ConditionVeto {
406 condition_expression: &'a Expression,
407 causes: Vec<Cause>,
408 },
409}
410
411struct ExplainCtx<'a, 'plan> {
416 context: &'a EvaluationContext<'plan>,
417 plan: &'a ExecutionPlan,
418 built: &'a HashMap<RulePath, Explanation>,
419 instructions: &'a Instructions,
420 recording: &'a RuleRecording,
421}
422
423fn winning_source_branch_and_causes<'a>(
427 exec_rule: &'a ExecutableRule,
428 ctx: &ExplainCtx<'_, '_>,
429) -> WinningSourceBranch<'a> {
430 if exec_rule.branches.len() == 1 {
431 return WinningSourceBranch::BranchResult {
432 result_expression: &exec_rule.branches[0].result,
433 causes: Vec::new(),
434 };
435 }
436
437 let condition_arm: HashMap<u32, u16> = ctx
438 .instructions
439 .arm_tags
440 .iter()
441 .filter(|tag| tag.role == ArmRole::Condition)
442 .map(|tag| (tag.pc, tag.arm))
443 .collect();
444 let result_arm: HashMap<u32, u16> = ctx
445 .instructions
446 .arm_tags
447 .iter()
448 .filter(|tag| tag.role == ArmRole::Result)
449 .map(|tag| (tag.pc, tag.arm))
450 .collect();
451
452 let mut decisions: Vec<(u16, BranchDecision)> = ctx
455 .recording
456 .branch_decisions
457 .iter()
458 .filter_map(|(pc, decision)| condition_arm.get(pc).map(|arm| (*arm, *decision)))
459 .collect();
460
461 if let Some(returned_pc) = ctx.recording.returned_pc {
462 let winning_arm = *result_arm
463 .get(&returned_pc)
464 .expect("BUG: executed Return must carry an arm tag");
465 decisions.sort_by_key(|(arm, _)| *arm);
468 let has_taken = decisions
474 .iter()
475 .any(|(_, d)| matches!(d, BranchDecision::Taken));
476 let causes = decisions
477 .iter()
478 .filter(|(_, decision)| {
479 if has_taken {
480 matches!(decision, BranchDecision::Taken)
481 } else {
482 true
483 }
484 })
485 .map(|(arm, decision)| {
486 let condition = exec_rule.branches[*arm as usize]
487 .condition
488 .as_ref()
489 .expect("BUG: unless branch missing condition");
490 let held = matches!(decision, BranchDecision::Taken);
491 build_cause(condition, held, ctx)
492 })
493 .collect();
494 return WinningSourceBranch::BranchResult {
495 result_expression: &exec_rule.branches[winning_arm as usize].result,
496 causes,
497 };
498 }
499
500 let (veto_pc, _) = *ctx
506 .recording
507 .branch_decisions
508 .iter()
509 .rev()
510 .find(|(_, decision)| matches!(decision, BranchDecision::Veto))
511 .expect("BUG: execution ended without Return but no veto was recorded");
512 let enclosing_tag = ctx
513 .instructions
514 .arm_tags
515 .iter()
516 .filter(|tag| tag.pc >= veto_pc)
517 .min_by_key(|tag| tag.pc)
518 .expect("BUG: veto pc past the final tagged Return");
519
520 let mut causes_decisions: Vec<(u16, BranchDecision)> = ctx
523 .recording
524 .branch_decisions
525 .iter()
526 .filter(|(pc, _)| *pc != veto_pc)
527 .filter_map(|(pc, decision)| condition_arm.get(pc).map(|arm| (*arm, *decision)))
528 .collect();
529 causes_decisions.sort_by_key(|(arm, _)| *arm);
530 let causes = causes_decisions
531 .iter()
532 .map(|(arm, decision)| {
533 let condition = exec_rule.branches[*arm as usize]
534 .condition
535 .as_ref()
536 .expect("BUG: unless branch missing condition");
537 let held = matches!(decision, BranchDecision::Taken);
538 build_cause(condition, held, ctx)
539 })
540 .collect();
541
542 match enclosing_tag.role {
543 ArmRole::Condition => {
544 let condition_expression = exec_rule.branches[enclosing_tag.arm as usize]
545 .condition
546 .as_ref()
547 .expect("BUG: unless branch missing condition");
548 WinningSourceBranch::ConditionVeto {
549 condition_expression,
550 causes,
551 }
552 }
553 ArmRole::Result => WinningSourceBranch::BranchResult {
556 result_expression: &exec_rule.branches[enclosing_tag.arm as usize].result,
557 causes,
558 },
559 }
560}
561
562fn build_cause(condition: &Expression, held: bool, ctx: &ExplainCtx<'_, '_>) -> Cause {
563 let (text, value) = stated_fact(condition, held);
564 let children = match &condition.kind {
569 ExpressionKind::DataPath(_) => Vec::new(),
570 ExpressionKind::Comparison(left, ComparisonComputation::Is, right)
571 if held
572 && matches!(left.kind, ExpressionKind::DataPath(_))
573 && matches!(right.kind, ExpressionKind::Literal(_)) =>
574 {
575 Vec::new()
576 }
577 _ => build_expression_children(condition, ctx),
578 };
579 Cause {
580 condition: text,
581 value,
582 children,
583 }
584}
585
586fn stated_fact(condition: &Expression, held: bool) -> (String, String) {
593 if held {
594 return match &condition.kind {
595 ExpressionKind::DataPath(_) | ExpressionKind::RulePath(_) => (
597 format!("{} is true", format_expression(condition)),
598 "true".to_string(),
599 ),
600 _ => (format_expression(condition), "true".to_string()),
601 };
602 }
603 match &condition.kind {
604 ExpressionKind::Comparison(left, op, right) => {
605 let flipped = Expression::with_source(
606 ExpressionKind::Comparison(
607 std::sync::Arc::clone(left),
608 negated_comparison(op.clone()),
609 std::sync::Arc::clone(right),
610 ),
611 condition.source_location.clone(),
612 );
613 (format_expression(&flipped), "true".to_string())
614 }
615 ExpressionKind::LogicalNegation(inner, _) => stated_fact(inner, true),
617 ExpressionKind::ResultIsVeto(operand) => (
618 format!("{} is not veto", format_expression(operand)),
619 "true".to_string(),
620 ),
621 ExpressionKind::DataPath(_) | ExpressionKind::RulePath(_) => (
622 format!("{} is false", format_expression(condition)),
623 "true".to_string(),
624 ),
625 _ => (format_expression(condition), "false".to_string()),
626 }
627}
628
629pub fn build_explanation(
630 exec_rule: &ExecutableRule,
631 context: &EvaluationContext<'_>,
632 plan: &ExecutionPlan,
633 built: &HashMap<RulePath, Explanation>,
634) -> Explanation {
635 let authoritative_result = context
636 .rule_results
637 .get(&exec_rule.path)
638 .expect("BUG: rule evaluated before explain")
639 .clone();
640 let recording = context
641 .recordings
642 .get(&exec_rule.path)
643 .expect("BUG: recording must exist when explanations are requested");
644 let ctx = ExplainCtx {
645 context,
646 plan,
647 built,
648 instructions: &exec_rule.source_instructions,
649 recording,
650 };
651
652 let (body, causes, children) = match winning_source_branch_and_causes(exec_rule, &ctx) {
653 WinningSourceBranch::BranchResult {
654 result_expression,
655 causes,
656 } => (
657 format_expression(result_expression),
658 causes,
659 build_expression_children(result_expression, &ctx),
660 ),
661 WinningSourceBranch::ConditionVeto {
662 condition_expression,
663 causes,
664 } => (
665 format_expression(condition_expression),
669 causes,
670 build_expression_children(condition_expression, &ctx),
671 ),
672 };
673
674 Explanation {
675 rule: exec_rule.path.clone(),
676 result: authoritative_result,
677 body,
678 causes,
679 children,
680 }
681}
682
683fn embed_rule(rule_path: &RulePath, built: &HashMap<RulePath, Explanation>) -> ExplanationNode {
684 built
685 .get(rule_path)
686 .expect("BUG: rule explanation must be built before dependents")
687 .as_rule_node()
688}
689
690fn is_literal(expr: &Expression) -> bool {
691 matches!(expr.kind, ExpressionKind::Literal(_))
692}
693
694fn flatten_arithmetic_chain<'e>(
696 expr: &'e Expression,
697 op: &ArithmeticComputation,
698 out: &mut Vec<&'e Expression>,
699) {
700 match &expr.kind {
701 ExpressionKind::Arithmetic(left, inner_op, right) if inner_op == op => {
702 flatten_arithmetic_chain(left, op, out);
703 flatten_arithmetic_chain(right, op, out);
704 }
705 _ => out.push(expr),
706 }
707}
708
709fn build_operand_nodes(operands: &[&Expression], ctx: &ExplainCtx<'_, '_>) -> Vec<ExplanationNode> {
710 let mut nodes = unit_equivalence_nodes(operands, ctx);
714 nodes.extend(
715 operands
716 .iter()
717 .filter(|operand| !is_literal(operand))
720 .map(|operand| build_expression_node(operand, ctx)),
721 );
722 nodes
723}
724
725fn operand_leaf_value(expr: &Expression, ctx: &ExplainCtx<'_, '_>) -> Option<LiteralValue> {
728 match &expr.kind {
729 ExpressionKind::Literal(lit) => Some((**lit).clone()),
730 ExpressionKind::DataPath(path) => match resolve_data_path_value(path, ctx.context) {
731 OperationResult::Value(value) => Some(value.as_ref().clone()),
732 OperationResult::Veto(_) => None,
733 },
734 ExpressionKind::RulePath(path) => ctx
735 .context
736 .rule_results
737 .get(path)
738 .and_then(|result| result.value().cloned()),
739 _ => None,
740 }
741}
742
743fn same_measure_family(
745 unit: &str,
746 other: &str,
747 unit_index: &HashMap<String, std::sync::Arc<LemmaType>>,
748) -> bool {
749 unit_index.get(unit).is_some_and(|owning| {
750 matches!(
751 &owning.specifications,
752 TypeSpecification::Measure { units, .. } if units.get(other).is_ok()
753 )
754 })
755}
756
757fn unit_equivalence_nodes(
762 operands: &[&Expression],
763 ctx: &ExplainCtx<'_, '_>,
764) -> Vec<ExplanationNode> {
765 let unit_index = ctx.plan.expression_unit_index();
766 let resolution = UnitResolutionContext::WithIndex(unit_index);
767 let mut seen: Vec<String> = Vec::new();
768 let mut nodes = Vec::new();
769 for operand in operands {
770 let Some(value) = operand_leaf_value(operand, ctx) else {
771 continue;
772 };
773 if !matches!(value.value, ValueKind::Measure(_, _)) {
774 continue;
775 }
776 let expanded =
779 crate::planning::normalize::expand_named_measure_literal(&value, Some(&resolution))
780 .unwrap_or(value);
781 let ValueKind::Measure(_, signature) = &expanded.value else {
782 continue;
783 };
784 for (unit, _) in signature {
785 if seen.iter().any(|known| known == unit) {
786 continue;
787 }
788 if let Some(earlier) = seen
789 .iter()
790 .find(|earlier| same_measure_family(unit, earlier, unit_index))
791 {
792 if let (Some(owning), Some(target_owning)) = (
793 unit_index.get(unit.as_str()),
794 unit_index.get(earlier.as_str()),
795 ) {
796 if let Some(text) = measure_unit_equivalence_step_text(
797 &[(unit.clone(), 1)],
798 earlier,
799 owning,
800 target_owning,
801 UnitResolutionContext::WithIndex(unit_index),
802 ) {
803 nodes.push(ExplanationNode::UnitEquivalence { text });
804 }
805 }
806 }
807 seen.push(unit.clone());
808 }
809 }
810 nodes
811}
812
813fn build_expression_children(expr: &Expression, ctx: &ExplainCtx<'_, '_>) -> Vec<ExplanationNode> {
814 match &expr.kind {
815 ExpressionKind::RulePath(rule_path) => vec![embed_rule(rule_path, ctx.built)],
816 ExpressionKind::DataPath(data_path) => vec![build_data_input_node(data_path, ctx)],
817 ExpressionKind::Literal(_) => Vec::new(),
818 ExpressionKind::Arithmetic(_, op, _)
819 if matches!(
820 op,
821 ArithmeticComputation::Add | ArithmeticComputation::Multiply
822 ) =>
823 {
824 let mut operands = Vec::new();
825 flatten_arithmetic_chain(expr, op, &mut operands);
826 build_operand_nodes(&operands, ctx)
827 }
828 ExpressionKind::Arithmetic(left, _, right)
829 | ExpressionKind::Comparison(left, _, right)
830 | ExpressionKind::LogicalAnd(left, right)
831 | ExpressionKind::LogicalOr(left, right)
832 | ExpressionKind::RangeLiteral(left, right)
833 | ExpressionKind::RangeContainment(left, right) => {
834 build_operand_nodes(&[left.as_ref(), right.as_ref()], ctx)
835 }
836 ExpressionKind::LogicalNegation(operand, _)
837 | ExpressionKind::MathematicalComputation(_, operand)
838 | ExpressionKind::ResultIsVeto(operand)
839 | ExpressionKind::PastFutureRange(_, operand)
840 | ExpressionKind::DateRelative(_, operand)
841 | ExpressionKind::DateCalendar(_, _, operand) => {
842 build_operand_nodes(&[operand.as_ref()], ctx)
843 }
844 ExpressionKind::Veto(veto_expr) => {
845 if veto_expr.message.is_none() {
846 Vec::new()
847 } else {
848 vec![ExplanationNode::Veto {
849 message: veto_expr.message.clone(),
850 }]
851 }
852 }
853 ExpressionKind::UnitConversion(value_expr, target) => {
854 vec![build_conversion_node(value_expr, target, expr, ctx)]
855 }
856 ExpressionKind::Now => Vec::new(),
857 ExpressionKind::Piecewise(_) => {
858 unreachable!("BUG: Piecewise in source expression for explanation")
859 }
860 }
861}
862
863fn build_expression_node(expr: &Expression, ctx: &ExplainCtx<'_, '_>) -> ExplanationNode {
864 match &expr.kind {
865 ExpressionKind::RulePath(rule_path) => embed_rule(rule_path, ctx.built),
866 ExpressionKind::DataPath(data_path) => build_data_input_node(data_path, ctx),
867 ExpressionKind::Literal(_) => {
868 unreachable!("BUG: literal operands are filtered before node construction")
869 }
870 ExpressionKind::UnitConversion(value_expr, target) => {
871 build_conversion_node(value_expr, target, expr, ctx)
872 }
873 ExpressionKind::Veto(veto_expr) => ExplanationNode::Veto {
874 message: veto_expr.message.clone(),
875 },
876 ExpressionKind::Now => ExplanationNode::DataInput {
877 data: DataPath::local(String::new()),
878 display: ctx.context.now().display_value(),
879 },
880 ExpressionKind::Piecewise(_) => {
881 unreachable!("BUG: Piecewise in source expression for explanation")
882 }
883 _ => ExplanationNode::Compose {
884 expression: format_expression(expr),
885 operands: build_expression_children(expr, ctx),
886 },
887 }
888}
889
890fn recorded_conversion_values(
895 expr: &Expression,
896 ctx: &ExplainCtx<'_, '_>,
897) -> Option<(OperationResult, OperationResult)> {
898 let source = expr.source_location.as_ref()?;
899 for tag in &ctx.instructions.conversion_tags {
900 if &tag.source != source {
901 continue;
902 }
903 let Instruction::UnitConversion {
904 destination_register,
905 source_register,
906 ..
907 } = &ctx.instructions.code[tag.pc as usize]
908 else {
909 unreachable!("BUG: conversion tag must reference a UnitConversion instruction");
910 };
911 let operand = ctx
912 .recording
913 .registers
914 .get(*source_register as usize)
915 .cloned()
916 .flatten();
917 let result = ctx
918 .recording
919 .registers
920 .get(*destination_register as usize)
921 .cloned()
922 .flatten();
923 if let (Some(operand), Some(result)) = (operand, result) {
924 return Some((operand, result));
925 }
926 }
927 None
928}
929
930fn build_conversion_node(
931 value_expr: &Expression,
932 target: &SemanticConversionTarget,
933 expr: &Expression,
934 ctx: &ExplainCtx<'_, '_>,
935) -> ExplanationNode {
936 let steps = match recorded_conversion_values(expr, ctx) {
937 Some((OperationResult::Veto(veto), _)) => {
938 return ExplanationNode::Veto {
939 message: Some(veto.to_string()),
940 };
941 }
942 Some((_, OperationResult::Veto(veto))) => {
943 return ExplanationNode::Veto {
944 message: Some(veto.to_string()),
945 };
946 }
947 Some((OperationResult::Value(operand_value), OperationResult::Value(converted_value))) => {
948 let data_ref = data_path_in_expression(value_expr);
949 build_conversion_steps(
950 operand_value.as_ref(),
951 target,
952 converted_value.as_ref(),
953 data_ref.as_ref(),
954 UnitResolutionContext::WithIndex(ctx.plan.expression_unit_index()),
955 )
956 }
957 None => Vec::new(),
960 };
961
962 let operand_named_in_steps = data_path_in_expression(value_expr)
965 .map(|path| {
966 steps
967 .iter()
968 .any(|step| step.data_ref.as_ref() == Some(&path))
969 })
970 .unwrap_or(false);
971 let operands = if is_literal(value_expr) || operand_named_in_steps {
972 Vec::new()
973 } else {
974 vec![build_expression_node(value_expr, ctx)]
975 };
976 ExplanationNode::Conversion {
977 expression: format_expression(expr),
978 steps: steps
979 .iter()
980 .map(SerializedConversionTraceStep::from)
981 .collect(),
982 operands,
983 }
984}
985
986impl From<&ConversionTraceStep> for SerializedConversionTraceStep {
987 fn from(step: &ConversionTraceStep) -> Self {
988 Self {
989 role: match step.role {
990 ConversionTraceRole::Outcome => "outcome".to_string(),
991 ConversionTraceRole::Rule => "rule".to_string(),
992 ConversionTraceRole::Source => "source".to_string(),
993 },
994 text: step.text.clone(),
995 }
996 }
997}
998
999fn build_data_input_node(data_path: &DataPath, ctx: &ExplainCtx<'_, '_>) -> ExplanationNode {
1000 match resolve_data_path_value(data_path, ctx.context) {
1001 OperationResult::Value(value) => ExplanationNode::DataInput {
1002 data: data_path.clone(),
1003 display: value.display_value(),
1004 },
1005 OperationResult::Veto(veto) => ExplanationNode::Veto {
1006 message: Some(veto.to_string()),
1007 },
1008 }
1009}
1010
1011fn data_path_in_expression(value_expr: &Expression) -> Option<DataPath> {
1012 if let ExpressionKind::DataPath(data_path) = &value_expr.kind {
1013 Some(data_path.clone())
1014 } else {
1015 None
1016 }
1017}
1018
1019pub fn format_explanation(explanation: &Explanation) -> String {
1020 let mut lines = Vec::new();
1021 let result_display = format_operation_result(&explanation.result);
1022 lines.push(format!("{}: {}", explanation.rule.rule, result_display));
1023 let mut ctx = FormatContext {
1024 lines: &mut lines,
1025 indent: String::new(),
1026 };
1027 ctx.render_rule_contents(
1028 &result_display,
1029 &explanation.body,
1030 &explanation.causes,
1031 &explanation.children,
1032 );
1033 lines.join("\n")
1034}
1035
1036#[derive(Copy, Clone)]
1037enum Connector {
1038 Branch,
1039 Last,
1040}
1041
1042struct FormatContext<'a> {
1043 lines: &'a mut Vec<String>,
1044 indent: String,
1045}
1046
1047impl<'a> FormatContext<'a> {
1048 fn push_line(&mut self, connector: Connector, text: &str) {
1049 self.lines.push(format!(
1050 "{}{} {text}",
1051 self.indent,
1052 connector_str(connector)
1053 ));
1054 }
1055
1056 fn child_indent(&self, connector: Connector) -> String {
1057 match connector {
1058 Connector::Branch => format!("{}│ ", self.indent),
1059 Connector::Last => format!("{} ", self.indent),
1060 }
1061 }
1062
1063 fn render_rule_contents(
1068 &mut self,
1069 result_display: &str,
1070 body: &str,
1071 causes: &[Cause],
1072 children: &[ExplanationNode],
1073 ) {
1074 let body_shown = !body.is_empty() && body != result_display;
1075 let total = causes.len() + usize::from(body_shown);
1076 let mut index = 0;
1077
1078 for cause in causes {
1079 index += 1;
1080 let connector = if index == total {
1081 Connector::Last
1082 } else {
1083 Connector::Branch
1084 };
1085 let line = if cause.value == "true" {
1086 cause.condition.clone()
1087 } else {
1088 format!("{} is {}", cause.condition, cause.value)
1089 };
1090 self.push_line(connector, &line);
1091 let child_indent = self.child_indent(connector);
1092 let mut child_ctx = FormatContext {
1093 lines: self.lines,
1094 indent: child_indent,
1095 };
1096 child_ctx.render_nodes(&cause.children, None);
1097 }
1098
1099 if body_shown {
1100 self.push_line(Connector::Last, body);
1101 let child_indent = self.child_indent(Connector::Last);
1102 let mut child_ctx = FormatContext {
1103 lines: self.lines,
1104 indent: child_indent,
1105 };
1106 child_ctx.render_nodes(children, Some(body));
1107 } else if !children.is_empty() {
1108 self.render_nodes(children, None);
1109 }
1110 }
1111
1112 fn render_nodes(&mut self, nodes: &[ExplanationNode], parent_body: Option<&str>) {
1113 let len = nodes.len();
1114 for (i, node) in nodes.iter().enumerate() {
1115 let connector = if i + 1 == len {
1116 Connector::Last
1117 } else {
1118 Connector::Branch
1119 };
1120 self.render_node(node, connector, parent_body);
1121 }
1122 }
1123
1124 fn render_conversion_contents(
1127 &mut self,
1128 steps: &[SerializedConversionTraceStep],
1129 operands: &[ExplanationNode],
1130 ) {
1131 let total = steps.len() + operands.len();
1132 let mut index = 0;
1133 for step in steps {
1134 index += 1;
1135 let connector = if index == total {
1136 Connector::Last
1137 } else {
1138 Connector::Branch
1139 };
1140 self.push_line(connector, &step.text);
1141 }
1142 for operand in operands {
1143 index += 1;
1144 let connector = if index == total {
1145 Connector::Last
1146 } else {
1147 Connector::Branch
1148 };
1149 self.render_node(operand, connector, None);
1150 }
1151 }
1152
1153 fn render_node(
1154 &mut self,
1155 node: &ExplanationNode,
1156 connector: Connector,
1157 parent_body: Option<&str>,
1158 ) {
1159 match node {
1160 ExplanationNode::Rule {
1161 rule,
1162 result,
1163 body,
1164 causes,
1165 children,
1166 } => {
1167 self.push_line(connector, &format!("{}: {result}", rule.rule));
1168 let child_indent = self.child_indent(connector);
1169 let mut child_ctx = FormatContext {
1170 lines: self.lines,
1171 indent: child_indent,
1172 };
1173 child_ctx.render_rule_contents(result, body, causes, children);
1174 }
1175 ExplanationNode::Compose {
1176 expression,
1177 operands,
1178 } => {
1179 self.push_line(connector, expression);
1180 let child_indent = self.child_indent(connector);
1181 let mut child_ctx = FormatContext {
1182 lines: self.lines,
1183 indent: child_indent,
1184 };
1185 child_ctx.render_nodes(operands, None);
1186 }
1187 ExplanationNode::DataInput { data, display } => {
1188 if data.data.is_empty() {
1189 self.push_line(connector, display);
1190 } else {
1191 self.push_line(connector, &format!("{data}: {display}"));
1192 }
1193 }
1194 ExplanationNode::Conversion {
1195 expression,
1196 steps,
1197 operands,
1198 } => {
1199 let expression_is_parent_body = parent_body.is_some_and(|body| body == expression);
1204 if expression_is_parent_body {
1205 let steps_without_outcome: Vec<SerializedConversionTraceStep> = steps
1206 .iter()
1207 .filter(|step| step.role != "outcome")
1208 .cloned()
1209 .collect();
1210 self.render_conversion_contents(&steps_without_outcome, operands);
1211 } else {
1212 self.push_line(connector, expression);
1213 let child_indent = self.child_indent(connector);
1214 let mut child_ctx = FormatContext {
1215 lines: self.lines,
1216 indent: child_indent,
1217 };
1218 child_ctx.render_conversion_contents(steps, operands);
1219 }
1220 }
1221 ExplanationNode::Veto { message } => {
1222 self.push_line(
1223 connector,
1224 message
1225 .as_deref()
1226 .expect("BUG: veto explanation must carry message"),
1227 );
1228 }
1229 ExplanationNode::UnitEquivalence { text } => {
1230 self.push_line(connector, text);
1231 }
1232 }
1233 }
1234}
1235
1236fn connector_str(connector: Connector) -> &'static str {
1237 match connector {
1238 Connector::Branch => "├─",
1239 Connector::Last => "└─",
1240 }
1241}
1242
1243fn expression_precedence(kind: &ExpressionKind) -> u8 {
1244 match kind {
1245 ExpressionKind::LogicalAnd(..) | ExpressionKind::LogicalOr(..) => 2,
1246 ExpressionKind::LogicalNegation(..) => 3,
1247 ExpressionKind::Comparison(..) | ExpressionKind::ResultIsVeto(..) => 4,
1248 ExpressionKind::RangeContainment(..) => 4,
1249 ExpressionKind::DateRelative(..) | ExpressionKind::DateCalendar(..) => 4,
1250 ExpressionKind::Arithmetic(_, op, _) => match op {
1251 ArithmeticComputation::Add | ArithmeticComputation::Subtract => 5,
1252 ArithmeticComputation::Multiply
1253 | ArithmeticComputation::Divide
1254 | ArithmeticComputation::Modulo => 6,
1255 ArithmeticComputation::Power => 7,
1256 },
1257 ExpressionKind::UnitConversion(..) => 8,
1258 ExpressionKind::RangeLiteral(..) => 9,
1259 ExpressionKind::MathematicalComputation(..) | ExpressionKind::PastFutureRange(..) => 10,
1260 ExpressionKind::Literal(_)
1261 | ExpressionKind::DataPath(_)
1262 | ExpressionKind::RulePath(_)
1263 | ExpressionKind::Now
1264 | ExpressionKind::Veto(_)
1265 | ExpressionKind::Piecewise(_) => 10,
1266 }
1267}
1268
1269fn write_expression_child(out: &mut String, child: &Expression, parent_prec: u8) {
1270 let child_prec = expression_precedence(&child.kind);
1271 if child_prec < parent_prec {
1272 out.push('(');
1273 out.push_str(&format_expression(child));
1274 out.push(')');
1275 } else {
1276 out.push_str(&format_expression(child));
1277 }
1278}
1279
1280pub fn format_expression(expr: &Expression) -> String {
1281 match &expr.kind {
1282 ExpressionKind::Literal(lit) => lit.display_value(),
1283 ExpressionKind::DataPath(path) => path.to_string(),
1284 ExpressionKind::RulePath(path) => path.to_string(),
1285 ExpressionKind::Arithmetic(left, op, right) => {
1286 let my_prec = expression_precedence(&expr.kind);
1287 let mut out = String::new();
1288 write_expression_child(&mut out, left, my_prec);
1289 out.push(' ');
1290 out.push_str(&op.to_string());
1291 out.push(' ');
1292 write_expression_child(&mut out, right, my_prec);
1293 out
1294 }
1295 ExpressionKind::Comparison(left, op, right) => {
1296 let my_prec = expression_precedence(&expr.kind);
1297 let mut out = String::new();
1298 write_expression_child(&mut out, left, my_prec);
1299 out.push(' ');
1300 out.push_str(&op.to_string());
1301 out.push(' ');
1302 write_expression_child(&mut out, right, my_prec);
1303 out
1304 }
1305 ExpressionKind::UnitConversion(value, target) => {
1306 let my_prec = expression_precedence(&expr.kind);
1307 let mut out = String::new();
1308 write_expression_child(&mut out, value, my_prec);
1309 out.push_str(" as ");
1310 out.push_str(&target.to_string());
1311 out
1312 }
1313 ExpressionKind::LogicalNegation(inner, negation) => {
1314 if let (NegationType::Not, ExpressionKind::ResultIsVeto(operand)) =
1315 (negation, &inner.kind)
1316 {
1317 let my_prec = expression_precedence(&expr.kind);
1318 let mut out = String::new();
1319 write_expression_child(&mut out, operand, my_prec);
1320 out.push_str(" is not veto");
1321 out
1322 } else {
1323 let my_prec = expression_precedence(&expr.kind);
1324 let mut out = String::from("not ");
1325 write_expression_child(&mut out, inner, my_prec);
1326 out
1327 }
1328 }
1329 ExpressionKind::ResultIsVeto(operand) => {
1330 let my_prec = expression_precedence(&expr.kind);
1331 let mut out = String::new();
1332 write_expression_child(&mut out, operand, my_prec);
1333 out.push_str(" is veto");
1334 out
1335 }
1336 ExpressionKind::LogicalAnd(left, right) => {
1337 let my_prec = expression_precedence(&expr.kind);
1338 let mut out = String::new();
1339 write_expression_child(&mut out, left, my_prec);
1340 out.push_str(" and ");
1341 write_expression_child(&mut out, right, my_prec);
1342 out
1343 }
1344 ExpressionKind::LogicalOr(left, right) => {
1345 let my_prec = expression_precedence(&expr.kind);
1346 let mut out = String::new();
1347 write_expression_child(&mut out, left, my_prec);
1348 out.push_str(" or ");
1349 write_expression_child(&mut out, right, my_prec);
1350 out
1351 }
1352 ExpressionKind::MathematicalComputation(op, operand) => {
1353 let my_prec = expression_precedence(&expr.kind);
1354 let mut out = format!("{op} ");
1355 write_expression_child(&mut out, operand, my_prec);
1356 out
1357 }
1358 ExpressionKind::Veto(veto) => match &veto.message {
1359 Some(msg) => format!("veto \"{msg}\""),
1360 None => "veto".to_string(),
1361 },
1362 ExpressionKind::Now => "now".to_string(),
1363 ExpressionKind::DateRelative(kind, date_expr) => {
1364 format!("{} {}", format_expression(date_expr), kind)
1365 }
1366 ExpressionKind::DateCalendar(kind, unit, date_expr) => {
1367 format!("{} {} {}", format_expression(date_expr), kind, unit)
1368 }
1369 ExpressionKind::RangeLiteral(left, right) => {
1370 let my_prec = expression_precedence(&expr.kind);
1371 let mut out = String::new();
1372 write_expression_child(&mut out, left, my_prec);
1373 out.push_str("...");
1374 write_expression_child(&mut out, right, my_prec);
1375 out
1376 }
1377 ExpressionKind::PastFutureRange(kind, offset_expr) => {
1378 let my_prec = expression_precedence(&expr.kind);
1379 let mut out = format!("{} ", kind);
1380 write_expression_child(&mut out, offset_expr, my_prec);
1381 out
1382 }
1383 ExpressionKind::RangeContainment(value, range) => {
1384 let my_prec = expression_precedence(&expr.kind);
1385 let mut out = String::new();
1386 write_expression_child(&mut out, value, my_prec);
1387 out.push_str(" in ");
1388 write_expression_child(&mut out, range, my_prec);
1389 out
1390 }
1391 ExpressionKind::Piecewise(_) => {
1392 unreachable!("BUG: Piecewise in source expression for explanation formatting")
1393 }
1394 }
1395}
1396
1397#[cfg(test)]
1398mod tests {
1399 use super::*;
1400 use crate::computation::rational::rational_new;
1401 use crate::computation::UnitResolutionContext;
1402 use crate::literals::DateGranularity;
1403 use crate::literals::MeasureUnit;
1404 use crate::parsing::ast::DateTimeValue;
1405 use crate::parsing::source::SourceType;
1406 use crate::planning::semantics::{
1407 date_time_to_semantic, duration_decomposition, DataPath, LemmaType, LiteralValue,
1408 MeasureTrait, MeasureUnits, RulePath, SemanticConversionTarget, TypeSpecification,
1409 ValueKind,
1410 };
1411 use crate::Engine;
1412 use rust_decimal::Decimal;
1413 use std::collections::HashMap;
1414 use std::path::PathBuf;
1415 use std::sync::Arc;
1416
1417 const CALC_SPEC: &str = r#"
1418spec calc
1419
1420data money: measure
1421 -> decimals 2
1422 -> unit eur 1
1423
1424data hourly_rate: 85.00 eur
1425data hours_worked: 37.5
1426data is_rush: boolean
1427data is_super_rush: boolean
1428
1429rule labor: hourly_rate * hours_worked
1430rule rush_surcharge: 0 eur
1431 unless is_rush then labor * 25%
1432 unless is_super_rush then labor * 50%
1433rule subtotal: labor + rush_surcharge
1434rule vat: subtotal * 21%
1435rule total: subtotal + vat
1436"#;
1437
1438 const CALC_TOTAL_IS_RUSH_ONLY_GOLDEN_JSON: &str = r#"{
1439 "rule": "total",
1440 "result": "4821.09 eur",
1441 "body": "subtotal + vat",
1442 "children": [
1443 {
1444 "type": "rule",
1445 "rule": "subtotal",
1446 "result": "3984.38 eur",
1447 "body": "labor + rush_surcharge",
1448 "children": [
1449 {
1450 "type": "rule",
1451 "rule": "labor",
1452 "result": "3187.50 eur",
1453 "body": "hourly_rate * hours_worked",
1454 "children": [
1455 {
1456 "type": "data_input",
1457 "data": "hourly_rate",
1458 "display": "85.00 eur"
1459 },
1460 {
1461 "type": "data_input",
1462 "data": "hours_worked",
1463 "display": "37.5"
1464 }
1465 ]
1466 },
1467 {
1468 "type": "rule",
1469 "rule": "rush_surcharge",
1470 "result": "796.88 eur",
1471 "body": "labor * 25%",
1472 "causes": [
1473 {
1474 "condition": "is_rush is true",
1475 "value": "true"
1476 }
1477 ],
1478 "children": [
1479 {
1480 "type": "rule",
1481 "rule": "labor",
1482 "result": "3187.50 eur",
1483 "body": "hourly_rate * hours_worked",
1484 "children": [
1485 {
1486 "type": "data_input",
1487 "data": "hourly_rate",
1488 "display": "85.00 eur"
1489 },
1490 {
1491 "type": "data_input",
1492 "data": "hours_worked",
1493 "display": "37.5"
1494 }
1495 ]
1496 }
1497 ]
1498 }
1499 ]
1500 },
1501 {
1502 "type": "rule",
1503 "rule": "vat",
1504 "result": "836.72 eur",
1505 "body": "subtotal * 21%",
1506 "children": [
1507 {
1508 "type": "rule",
1509 "rule": "subtotal",
1510 "result": "3984.38 eur",
1511 "body": "labor + rush_surcharge",
1512 "children": [
1513 {
1514 "type": "rule",
1515 "rule": "labor",
1516 "result": "3187.50 eur",
1517 "body": "hourly_rate * hours_worked",
1518 "children": [
1519 {
1520 "type": "data_input",
1521 "data": "hourly_rate",
1522 "display": "85.00 eur"
1523 },
1524 {
1525 "type": "data_input",
1526 "data": "hours_worked",
1527 "display": "37.5"
1528 }
1529 ]
1530 },
1531 {
1532 "type": "rule",
1533 "rule": "rush_surcharge",
1534 "result": "796.88 eur",
1535 "body": "labor * 25%",
1536 "causes": [
1537 {
1538 "condition": "is_rush is true",
1539 "value": "true"
1540 }
1541 ],
1542 "children": [
1543 {
1544 "type": "rule",
1545 "rule": "labor",
1546 "result": "3187.50 eur",
1547 "body": "hourly_rate * hours_worked",
1548 "children": [
1549 {
1550 "type": "data_input",
1551 "data": "hourly_rate",
1552 "display": "85.00 eur"
1553 },
1554 {
1555 "type": "data_input",
1556 "data": "hours_worked",
1557 "display": "37.5"
1558 }
1559 ]
1560 }
1561 ]
1562 }
1563 ]
1564 }
1565 ]
1566 }
1567 ]
1568}"#;
1569
1570 fn rush_surcharge_causes(data: HashMap<String, String>) -> serde_json::Value {
1571 let mut engine = Engine::new();
1572 engine
1573 .load(CALC_SPEC, crate::SourceType::Volatile)
1574 .expect("calc spec loads");
1575 let now = DateTimeValue::now();
1576 let response = engine
1577 .run(None, "calc", Some(&now), data, true, None)
1578 .expect("calc eval succeeds");
1579 let explanation = response
1580 .results
1581 .get("rush_surcharge")
1582 .expect("rush_surcharge rule evaluated")
1583 .explanation
1584 .as_ref()
1585 .expect("explanation always built");
1586 serde_json::to_value(&explanation.causes).expect("causes serialize")
1587 }
1588
1589 #[test]
1590 fn unless_causes_neither_matches() {
1591 let mut data = HashMap::new();
1592 data.insert("is_rush".into(), "false".into());
1593 data.insert("is_super_rush".into(), "false".into());
1594 let causes = rush_surcharge_causes(data);
1595 assert_eq!(
1596 causes,
1597 serde_json::json!([
1598 { "condition": "is_rush is false", "value": "true" },
1599 { "condition": "is_super_rush is false", "value": "true" },
1600 ])
1601 );
1602 }
1603
1604 #[test]
1605 fn calc_total_is_rush_only_serializes_to_golden_json() {
1606 let mut data = HashMap::new();
1607 data.insert("is_rush".into(), "true".into());
1608 data.insert("is_super_rush".into(), "false".into());
1609
1610 let mut engine = Engine::new();
1611 engine
1612 .load(CALC_SPEC, crate::SourceType::Volatile)
1613 .expect("calc spec loads");
1614 let now = DateTimeValue::now();
1615 let response = engine
1616 .run(None, "calc", Some(&now), data, true, None)
1617 .expect("calc eval succeeds");
1618 let explanation = response
1619 .results
1620 .get("total")
1621 .expect("total rule evaluated")
1622 .explanation
1623 .as_ref()
1624 .expect("explanation always built");
1625
1626 let actual: serde_json::Value =
1627 serde_json::to_value(explanation).expect("explanation serializes");
1628 let expected: serde_json::Value =
1629 serde_json::from_str(CALC_TOTAL_IS_RUSH_ONLY_GOLDEN_JSON).expect("golden json parses");
1630 assert_eq!(actual, expected);
1631 }
1632
1633 #[test]
1634 fn unless_causes_is_rush_only() {
1635 let mut data = HashMap::new();
1636 data.insert("is_rush".into(), "true".into());
1637 data.insert("is_super_rush".into(), "false".into());
1638 let causes = rush_surcharge_causes(data);
1639 assert_eq!(
1640 causes,
1641 serde_json::json!([
1642 { "condition": "is_rush is true", "value": "true" },
1643 ])
1644 );
1645 }
1646
1647 #[test]
1648 fn unless_causes_is_super_rush() {
1649 let mut data = HashMap::new();
1650 data.insert("is_rush".into(), "true".into());
1651 data.insert("is_super_rush".into(), "true".into());
1652 let causes = rush_surcharge_causes(data);
1653 assert_eq!(
1654 causes,
1655 serde_json::json!([
1656 { "condition": "is_super_rush is true", "value": "true" },
1657 ])
1658 );
1659 }
1660
1661 #[test]
1662 fn conversion_source_step_text_with_data_reference() {
1663 let operand = LiteralValue::measure_with_type(
1664 rational_new(2, 1),
1665 "kilogram".to_string(),
1666 Arc::new(LemmaType::primitive(TypeSpecification::measure())),
1667 );
1668 let path = DataPath::local("mass".to_string());
1669 let text = conversion_source_step_text(&operand, Some(&path));
1670 assert_eq!(text, "The measure of mass is 2 kilogram");
1671 }
1672
1673 #[test]
1674 fn build_conversion_steps_scalar_measure() {
1675 let mut units = MeasureUnits::new();
1676 units.0.push(
1677 MeasureUnit::from_decimal_factor("kilogram".to_string(), Decimal::ONE, vec![]).unwrap(),
1678 );
1679 units.0.push(
1680 MeasureUnit::from_decimal_factor("gram".to_string(), Decimal::new(1, 3), vec![])
1681 .unwrap(),
1682 );
1683 let lemma_type = Arc::new(LemmaType::primitive(TypeSpecification::Measure {
1684 minimum: None,
1685 maximum: None,
1686 decimals: None,
1687 units,
1688 traits: vec![],
1689 decomposition: Default::default(),
1690 help: String::new(),
1691 }));
1692 let operand = LiteralValue::measure_with_type(
1693 rational_new(2, 1),
1694 "kilogram".to_string(),
1695 Arc::clone(&lemma_type),
1696 );
1697 let gram_target = Arc::clone(&lemma_type);
1698 let result = LiteralValue::measure_with_type(
1699 rational_new(2, 1),
1700 "gram".to_string(),
1701 Arc::clone(&lemma_type),
1702 );
1703 let path = DataPath::local("mass".to_string());
1704 let steps = build_conversion_steps(
1705 &operand,
1706 &SemanticConversionTarget::Unit {
1707 unit_name: "gram".to_string(),
1708 owning_type: gram_target,
1709 },
1710 &result,
1711 Some(&path),
1712 UnitResolutionContext::NamedMeasureOnly,
1713 );
1714 assert_eq!(steps.len(), 3);
1715 assert!(matches!(steps[0].role, ConversionTraceRole::Outcome));
1716 assert_eq!(steps[0].text, "2000 gram");
1717 assert!(matches!(steps[1].role, ConversionTraceRole::Rule));
1718 assert_eq!(steps[1].text, "1 kilogram is 1000 gram");
1719 assert!(matches!(steps[2].role, ConversionTraceRole::Source));
1720 assert_eq!(steps[2].text, "The measure of mass is 2 kilogram");
1721 assert_eq!(steps[2].data_ref, Some(path));
1722 }
1723
1724 #[test]
1725 fn build_conversion_steps_date_range() {
1726 let left = LiteralValue::date(date_time_to_semantic(&DateTimeValue {
1727 year: 2024,
1728 month: 6,
1729 day: 1,
1730 hour: 0,
1731 minute: 0,
1732 second: 0,
1733 microsecond: 0,
1734 timezone: None,
1735
1736 granularity: DateGranularity::Full,
1737 }));
1738 let right = LiteralValue::date(date_time_to_semantic(&DateTimeValue {
1739 year: 2024,
1740 month: 6,
1741 day: 15,
1742 hour: 0,
1743 minute: 0,
1744 second: 0,
1745 microsecond: 0,
1746 timezone: None,
1747
1748 granularity: DateGranularity::Full,
1749 }));
1750 let range = LiteralValue {
1751 value: ValueKind::Range(Box::new(left), Box::new(right)),
1752 lemma_type: Arc::new(LemmaType::primitive(TypeSpecification::date_range())),
1753 };
1754 let mut duration_units = MeasureUnits::new();
1755 duration_units.0.push(
1756 MeasureUnit::from_decimal_factor("days".to_string(), Decimal::from(86_400), vec![])
1757 .unwrap(),
1758 );
1759 let days_owning_type = Arc::new(LemmaType::primitive(TypeSpecification::Measure {
1760 minimum: None,
1761 maximum: None,
1762 decimals: None,
1763 units: duration_units,
1764 traits: vec![MeasureTrait::Duration],
1765 decomposition: Some(duration_decomposition()),
1766 help: String::new(),
1767 }));
1768 let result = LiteralValue::measure_with_type(
1769 rational_new(14, 1),
1770 "days".to_string(),
1771 Arc::new(LemmaType::primitive(TypeSpecification::measure())),
1772 );
1773 let path = DataPath::local("age".to_string());
1774 let steps = build_conversion_steps(
1775 &range,
1776 &SemanticConversionTarget::Unit {
1777 unit_name: "days".to_string(),
1778 owning_type: days_owning_type,
1779 },
1780 &result,
1781 Some(&path),
1782 UnitResolutionContext::WithIndex(&HashMap::new()),
1783 );
1784 assert_eq!(steps.len(), 3);
1785 assert!(steps[1].text.contains('−'));
1786 assert!(steps[1].text.contains("2024-06-15"));
1787 assert!(steps[1].text.contains("2024-06-01"));
1788 assert!(steps[1].text.contains("14"));
1789 assert!(steps[2].text.contains("The date range of age is"));
1790 }
1791
1792 #[test]
1793 fn build_conversion_steps_identity_omits_rule_and_source() {
1794 let mut units = MeasureUnits::new();
1795 units.0.push(
1796 MeasureUnit::from_decimal_factor("kilogram".to_string(), Decimal::ONE, vec![]).unwrap(),
1797 );
1798 let lemma_type = Arc::new(LemmaType::primitive(TypeSpecification::Measure {
1799 minimum: None,
1800 maximum: None,
1801 decimals: None,
1802 units,
1803 traits: vec![],
1804 decomposition: Default::default(),
1805 help: String::new(),
1806 }));
1807 let operand = LiteralValue::measure_with_type(
1808 rational_new(2, 1),
1809 "kilogram".to_string(),
1810 Arc::clone(&lemma_type),
1811 );
1812 let kilogram_target = Arc::clone(&lemma_type);
1813 let result =
1814 LiteralValue::measure_with_type(rational_new(2, 1), "kilogram".to_string(), lemma_type);
1815 let steps = build_conversion_steps(
1816 &operand,
1817 &SemanticConversionTarget::Unit {
1818 unit_name: "kilogram".to_string(),
1819 owning_type: kilogram_target,
1820 },
1821 &result,
1822 None,
1823 UnitResolutionContext::NamedMeasureOnly,
1824 );
1825 assert_eq!(steps.len(), 1);
1828 assert!(matches!(steps[0].role, ConversionTraceRole::Outcome));
1829 }
1830
1831 #[test]
1832 fn conversion_trace_step_roundtrip() {
1833 let step = ConversionTraceStep {
1834 role: ConversionTraceRole::Rule,
1835 text: "1 kilogram is 1000 gram".to_string(),
1836 data_ref: Some(DataPath::local("mass".to_string())),
1837 };
1838 assert_eq!(step.text, "1 kilogram is 1000 gram");
1839 assert!(matches!(step.role, ConversionTraceRole::Rule));
1840 }
1841
1842 #[test]
1843 fn explanation_for_compound_signature_uses_signature_factor() {
1844 let code = r#"spec t
1845uses lemma units
1846data money: measure
1847 -> unit eur 1
1848data rate: measure
1849 -> unit eur_per_minute eur/minute
1850data r: 40 eur_per_minute
1851data h: 2 hour
1852rule cost: (r * h) as eur
1853"#;
1854 let mut engine = Engine::new();
1855 engine
1856 .load(code, SourceType::Path(Arc::new(PathBuf::from("t.lemma"))))
1857 .expect("must load");
1858 let response = engine
1859 .run(None, "t", None, HashMap::new(), true, None)
1860 .expect("must eval");
1861 let cost_result = response.results.get("cost").expect("rule must exist");
1862 let display = cost_result
1863 .display
1864 .as_deref()
1865 .expect("must have display value");
1866 assert!(
1867 display.contains("4800") && display.contains("eur"),
1868 "expected 4800 eur, got: {display}"
1869 );
1870 }
1871
1872 #[test]
1873 fn render_veto_with_none_message_must_not_use_placeholder_text() {
1874 use crate::evaluation::operations::{OperationResult, VetoType};
1875
1876 let explanation = Explanation {
1877 rule: RulePath::new(vec![], "r".into()),
1878 result: OperationResult::Veto(VetoType::computation("test")),
1879 body: "expr".into(),
1880 causes: vec![],
1881 children: vec![ExplanationNode::Veto { message: None }],
1882 };
1883 let panic = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1884 format_explanation(&explanation);
1885 }));
1886 assert!(
1887 panic.is_err(),
1888 "veto node without message must crash, not render placeholder"
1889 );
1890 }
1891}