Skip to main content

lemma/evaluation/
evaluation_trace.rs

1use crate::evaluation::operations::{ComputationKind, OperationResult};
2use crate::planning::semantics::{DataPath, LiteralValue, RulePath, Source};
3use serde::Serialize;
4use std::collections::HashSet;
5use std::sync::Arc;
6
7#[derive(Debug, Clone)]
8pub struct EvaluationTrace {
9    pub rule_path: RulePath,
10    pub source: Option<Source>,
11    pub result: OperationResult,
12    pub tree: Arc<TraceNode>,
13}
14
15impl Serialize for EvaluationTrace {
16    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
17    where
18        S: serde::Serializer,
19    {
20        let mut expanded = HashSet::new();
21        SerializedEvaluationTrace {
22            rule: self.rule_path.rule.clone(),
23            result: match &self.result {
24                OperationResult::Value(value) => value.display_value(),
25                OperationResult::Veto(veto) => veto.to_string(),
26            },
27            tree: serialize_trace_node(&self.tree, &mut expanded),
28        }
29        .serialize(serializer)
30    }
31}
32
33#[derive(Debug, Clone)]
34pub enum ConversionTraceRole {
35    Outcome,
36    Rule,
37    Source,
38}
39
40#[derive(Debug, Clone)]
41pub struct ConversionTraceStep {
42    pub role: ConversionTraceRole,
43    pub text: String,
44    pub data_ref: Option<DataPath>,
45}
46
47#[derive(Debug, Clone)]
48pub enum TraceNode {
49    Value {
50        value: LiteralValue,
51        source: TraceValueSource,
52        source_location: Option<Source>,
53    },
54    RuleReference {
55        rule_path: RulePath,
56        result: OperationResult,
57        source_location: Option<Source>,
58        expansion: Arc<TraceNode>,
59    },
60    Computation {
61        kind: ComputationKind,
62        conversion_steps: Vec<ConversionTraceStep>,
63        expression: String,
64        result: LiteralValue,
65        source_location: Option<Source>,
66        operands: Vec<TraceNode>,
67    },
68    Branches {
69        matched: Box<TraceBranch>,
70        non_matched: Vec<TraceNonMatchedBranch>,
71        source_location: Option<Source>,
72    },
73    Veto {
74        message: Option<String>,
75        source_location: Option<Source>,
76    },
77}
78
79#[derive(Debug, Clone)]
80pub enum TraceValueSource {
81    Data { data_ref: DataPath },
82    Literal,
83    Computed,
84}
85
86#[derive(Debug, Clone)]
87pub struct TraceBranch {
88    pub condition: Option<Box<TraceNode>>,
89    pub result: Box<TraceNode>,
90    pub clause_index: Option<usize>,
91    pub source_location: Option<Source>,
92}
93
94#[derive(Debug, Clone)]
95pub struct TraceNonMatchedBranch {
96    pub condition: Box<TraceNode>,
97    pub result: Option<Box<TraceNode>>,
98    pub clause_index: Option<usize>,
99    pub source_location: Option<Source>,
100}
101
102/// Human-readable expression text for a trace node used when composing parent expressions.
103pub fn trace_expression(node: &TraceNode) -> String {
104    match node {
105        TraceNode::Computation { expression, .. } => expression.clone(),
106        TraceNode::Value { value, source, .. } => match source {
107            TraceValueSource::Data { data_ref } => {
108                format!("{} is {}", data_ref, value.display_value())
109            }
110            TraceValueSource::Literal | TraceValueSource::Computed => value.display_value(),
111        },
112        TraceNode::RuleReference { rule_path, .. } => rule_path.to_string(),
113        TraceNode::Branches { .. } | TraceNode::Veto { .. } => {
114            panic!(
115                "BUG: trace expression must come from Computation, Value, or RuleReference, got {node:?}"
116            )
117        }
118    }
119}
120
121#[derive(Serialize)]
122struct SerializedEvaluationTrace {
123    rule: String,
124    result: String,
125    tree: SerializedTraceNode,
126}
127
128#[derive(Debug, Clone, Serialize)]
129#[serde(tag = "type", rename_all = "snake_case")]
130enum SerializedTraceNode {
131    Value {
132        display: String,
133        #[serde(skip_serializing_if = "Option::is_none")]
134        data: Option<String>,
135    },
136    RuleReference {
137        rule: String,
138        result: String,
139        #[serde(skip_serializing_if = "Option::is_none")]
140        tree: Option<Box<SerializedTraceNode>>,
141    },
142    Computation {
143        expression: String,
144        result: String,
145        operands: Vec<SerializedTraceNode>,
146    },
147    Branches {
148        matched: SerializedTraceBranch,
149        non_matched: Vec<SerializedTraceBranch>,
150    },
151    Conversion {
152        steps: Vec<SerializedConversionStep>,
153        operands: Vec<SerializedTraceNode>,
154    },
155    Veto {
156        #[serde(skip_serializing_if = "Option::is_none")]
157        message: Option<String>,
158    },
159}
160
161#[derive(Debug, Clone, Serialize)]
162struct SerializedTraceBranch {
163    #[serde(skip_serializing_if = "Option::is_none")]
164    condition: Option<String>,
165    #[serde(skip_serializing_if = "Option::is_none")]
166    tree: Option<Box<SerializedTraceNode>>,
167}
168
169#[derive(Debug, Clone, Serialize)]
170struct SerializedConversionStep {
171    role: String,
172    text: String,
173}
174
175fn serialize_trace_node(node: &TraceNode, expanded: &mut HashSet<String>) -> SerializedTraceNode {
176    match node {
177        TraceNode::Value { value, source, .. } => {
178            let data = match source {
179                TraceValueSource::Data { data_ref } => Some(data_ref.to_string()),
180                _ => None,
181            };
182            SerializedTraceNode::Value {
183                display: value.display_value(),
184                data,
185            }
186        }
187        TraceNode::RuleReference {
188            rule_path,
189            result,
190            expansion,
191            ..
192        } => {
193            let rule_name = rule_path.to_string();
194            let tree = if expanded.insert(rule_name.clone()) {
195                Some(Box::new(serialize_trace_node(expansion, expanded)))
196            } else {
197                None
198            };
199            SerializedTraceNode::RuleReference {
200                rule: rule_name,
201                result: match result {
202                    OperationResult::Value(value) => value.display_value(),
203                    OperationResult::Veto(veto) => veto.to_string(),
204                },
205                tree,
206            }
207        }
208        TraceNode::Computation {
209            kind,
210            conversion_steps,
211            expression,
212            result,
213            operands,
214            ..
215        } => {
216            let ops = operands
217                .iter()
218                .map(|operand| serialize_trace_node(operand, expanded))
219                .collect();
220            if matches!(kind, ComputationKind::UnitConversion { .. }) {
221                SerializedTraceNode::Conversion {
222                    steps: conversion_steps
223                        .iter()
224                        .map(|step| SerializedConversionStep {
225                            role: match step.role {
226                                ConversionTraceRole::Outcome => "outcome".to_string(),
227                                ConversionTraceRole::Rule => "rule".to_string(),
228                                ConversionTraceRole::Source => "source".to_string(),
229                            },
230                            text: step.text.clone(),
231                        })
232                        .collect(),
233                    operands: ops,
234                }
235            } else {
236                SerializedTraceNode::Computation {
237                    expression: expression.clone(),
238                    result: result.display_value(),
239                    operands: ops,
240                }
241            }
242        }
243        TraceNode::Branches {
244            matched,
245            non_matched,
246            ..
247        } => SerializedTraceNode::Branches {
248            matched: SerializedTraceBranch {
249                condition: matched
250                    .condition
251                    .as_ref()
252                    .map(|condition| branch_condition_expression(condition)),
253                tree: Some(Box::new(serialize_trace_node(&matched.result, expanded))),
254            },
255            non_matched: non_matched
256                .iter()
257                .map(|branch| SerializedTraceBranch {
258                    condition: Some(branch_condition_expression(&branch.condition)),
259                    tree: branch
260                        .result
261                        .as_ref()
262                        .map(|result| Box::new(serialize_trace_node(result, expanded))),
263                })
264                .collect(),
265        },
266        TraceNode::Veto { message, .. } => SerializedTraceNode::Veto {
267            message: message.clone(),
268        },
269    }
270}
271
272fn branch_condition_expression(node: &TraceNode) -> String {
273    match node {
274        TraceNode::Computation { expression, .. } => expression.clone(),
275        TraceNode::Value { value, .. } => value.display_value(),
276        TraceNode::RuleReference { rule_path, .. } => rule_path.to_string(),
277        TraceNode::Branches { .. } | TraceNode::Veto { .. } => {
278            panic!(
279                "BUG: branch condition must be Computation, Value, or RuleReference, got {node:?}"
280            )
281        }
282    }
283}