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
102pub 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}