1use crate::parsing::ast::{LemmaSpec, Span};
2use crate::parsing::source::Source;
3use crate::planning::execution_plan::ExecutionPlan;
4use crate::planning::semantics::{ExpressionKind, FactData, LemmaType, PathSegment, RulePath};
5use crate::planning::types::ResolvedSpecTypes;
6use crate::Error;
7use std::collections::{BTreeMap, HashMap, HashSet};
8use std::sync::Arc;
9
10type ResolvedTypesMap = HashMap<Arc<LemmaSpec>, ResolvedSpecTypes>;
11
12#[derive(Debug, Clone, PartialEq, Eq)]
18pub struct SliceInterface {
19 pub facts: BTreeMap<String, FactKind>,
20 pub rules: BTreeMap<String, LemmaType>,
21 pub types: BTreeMap<String, LemmaType>,
22}
23
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum FactKind {
26 Value(Box<LemmaType>),
27 SpecRef { spec_name: String },
28}
29
30impl SliceInterface {
31 pub(crate) fn from_plan(
36 plan: &ExecutionPlan,
37 segments: &[PathSegment],
38 resolved_types: &ResolvedTypesMap,
39 ref_spec: &Arc<LemmaSpec>,
40 ) -> Self {
41 let needed_at_segments = collect_needed_facts_at_segments(plan, segments);
42
43 let mut facts = BTreeMap::new();
44 for (path, data) in &plan.facts {
45 if path.segments != *segments {
46 continue;
47 }
48 if !needed_at_segments.contains(path.fact.as_str()) {
49 continue;
50 }
51 let kind = match data {
52 FactData::Value { value, .. } => {
53 FactKind::Value(Box::new(value.lemma_type.clone()))
54 }
55 FactData::TypeDeclaration { resolved_type, .. } => {
56 FactKind::Value(Box::new(resolved_type.clone()))
57 }
58 FactData::SpecRef { spec, .. } => FactKind::SpecRef {
59 spec_name: spec.name.clone(),
60 },
61 };
62 facts.insert(path.fact.clone(), kind);
63 }
64
65 let referenced_rules = collect_referenced_rules_at_segments(plan, segments);
66 let mut rules = BTreeMap::new();
67 for rule in &plan.rules {
68 if rule.path.segments != *segments {
69 continue;
70 }
71 if !referenced_rules.contains(rule.name.as_str()) {
72 continue;
73 }
74 rules.insert(rule.name.clone(), rule.rule_type.clone());
75 }
76
77 let mut types = BTreeMap::new();
78 if let Some(spec_types) = resolved_types.get(ref_spec) {
79 for (name, lemma_type) in &spec_types.named_types {
80 types.insert(name.clone(), lemma_type.clone());
81 }
82 }
83
84 SliceInterface {
85 facts,
86 rules,
87 types,
88 }
89 }
90
91 pub fn diff(&self, other: &SliceInterface) -> Vec<String> {
92 let mut diffs = Vec::new();
93 diff_map("fact", &self.facts, &other.facts, &mut diffs, |a, b| a != b);
94 diff_map("rule", &self.rules, &other.rules, &mut diffs, |a, b| a != b);
95 diff_map("type", &self.types, &other.types, &mut diffs, |a, b| a != b);
96 diffs
97 }
98}
99
100fn diff_map<V: std::fmt::Debug>(
101 label: &str,
102 a: &BTreeMap<String, V>,
103 b: &BTreeMap<String, V>,
104 diffs: &mut Vec<String>,
105 changed: impl Fn(&V, &V) -> bool,
106) {
107 for key in a.keys() {
108 if !b.contains_key(key) {
109 diffs.push(format!("{} '{}' removed", label, key));
110 }
111 }
112 for key in b.keys() {
113 if !a.contains_key(key) {
114 diffs.push(format!("{} '{}' added", label, key));
115 }
116 }
117 for (key, val_a) in a {
118 if let Some(val_b) = b.get(key) {
119 if changed(val_a, val_b) {
120 diffs.push(format!(
121 "{} '{}' changed: {:?} -> {:?}",
122 label, key, val_a, val_b
123 ));
124 }
125 }
126 }
127}
128
129fn collect_needed_facts_at_segments<'a>(
136 plan: &'a ExecutionPlan,
137 segments: &[PathSegment],
138) -> HashSet<&'a str> {
139 let mut needed = HashSet::new();
140
141 for rule in &plan.rules {
142 if !rule.path.segments.is_empty() {
143 continue;
144 }
145 for fp in &rule.needs_facts {
146 if fp.segments == *segments {
147 needed.insert(fp.fact.as_str());
148 }
149 if fp.segments.len() > segments.len() && fp.segments[..segments.len()] == *segments {
150 needed.insert(fp.segments[segments.len()].fact.as_str());
151 }
152 }
153 }
154
155 let referenced_rules = collect_root_rule_paths(plan);
158 for rp in &referenced_rules {
159 if rp.segments.len() > segments.len() && rp.segments[..segments.len()] == *segments {
160 needed.insert(rp.segments[segments.len()].fact.as_str());
161 }
162 }
163
164 needed
165}
166
167fn collect_referenced_rules_at_segments<'a>(
173 plan: &'a ExecutionPlan,
174 segments: &[PathSegment],
175) -> HashSet<&'a str> {
176 let mut referenced = HashSet::new();
177 let all_rule_paths = collect_root_rule_paths(plan);
178 for rp in &all_rule_paths {
179 if rp.segments == *segments {
180 referenced.insert(rp.rule.as_str());
181 }
182 }
183 referenced
184}
185
186fn collect_root_rule_paths(plan: &ExecutionPlan) -> Vec<&RulePath> {
188 let mut paths = Vec::new();
189 for rule in &plan.rules {
190 if !rule.path.segments.is_empty() {
191 continue;
192 }
193 for branch in &rule.branches {
194 collect_rule_paths_from_expr(&branch.result, &mut paths);
195 if let Some(cond) = &branch.condition {
196 collect_rule_paths_from_expr(cond, &mut paths);
197 }
198 }
199 }
200 paths
201}
202
203fn collect_rule_paths_from_expr<'a>(
204 expr: &'a crate::planning::semantics::Expression,
205 out: &mut Vec<&'a RulePath>,
206) {
207 match &expr.kind {
208 ExpressionKind::RulePath(rp) => out.push(rp),
209 ExpressionKind::LogicalAnd(l, r)
210 | ExpressionKind::Arithmetic(l, _, r)
211 | ExpressionKind::Comparison(l, _, r) => {
212 collect_rule_paths_from_expr(l, out);
213 collect_rule_paths_from_expr(r, out);
214 }
215 ExpressionKind::UnitConversion(inner, _)
216 | ExpressionKind::LogicalNegation(inner, _)
217 | ExpressionKind::MathematicalComputation(_, inner) => {
218 collect_rule_paths_from_expr(inner, out);
219 }
220 ExpressionKind::DateRelative(_, date_expr, tolerance) => {
221 collect_rule_paths_from_expr(date_expr, out);
222 if let Some(tol) = tolerance {
223 collect_rule_paths_from_expr(tol, out);
224 }
225 }
226 ExpressionKind::DateCalendar(_, _, date_expr) => {
227 collect_rule_paths_from_expr(date_expr, out);
228 }
229 ExpressionKind::Literal(_)
230 | ExpressionKind::FactPath(_)
231 | ExpressionKind::Veto(_)
232 | ExpressionKind::Now => {}
233 }
234}
235
236pub(crate) fn validate_slice_interfaces(
239 spec: &LemmaSpec,
240 slice_plans: &[ExecutionPlan],
241 resolved_types_per_slice: &[ResolvedTypesMap],
242) -> Vec<Error> {
243 let spec_name = &spec.name;
244 if slice_plans.len() <= 1 {
245 return Vec::new();
246 }
247
248 let attr = spec.attribute.as_deref().unwrap_or(spec_name.as_str());
249 let span = Span {
250 start: 0,
251 end: 0,
252 line: spec.start_line.max(1),
253 col: 0,
254 };
255 let source_loc = Source::new(attr, span);
256
257 let ref_segments = collect_ref_spec_segments(&slice_plans[0]);
258
259 let mut errors = Vec::new();
260
261 for (segments, ref_spec_arc) in &ref_segments {
262 let first_interface = SliceInterface::from_plan(
263 &slice_plans[0],
264 segments,
265 &resolved_types_per_slice[0],
266 ref_spec_arc,
267 );
268
269 for (i, plan) in slice_plans.iter().enumerate().skip(1) {
270 let ref_spec_in_slice = find_ref_spec_in_plan(plan, segments);
271 let ref_spec = ref_spec_in_slice.as_ref().unwrap_or(ref_spec_arc);
272 let slice_interface =
273 SliceInterface::from_plan(plan, segments, &resolved_types_per_slice[i], ref_spec);
274
275 if first_interface != slice_interface {
276 let diffs = first_interface.diff(&slice_interface);
277 let diff_detail = if diffs.is_empty() {
278 String::new()
279 } else {
280 format!(": {}", diffs.join(", "))
281 };
282 errors.push(Error::validation(
283 format!(
284 "Referenced spec '{}' changed its interface between temporal slices of '{}'{}\n\
285 Create a new temporal version of '{}' to handle the interface change.",
286 ref_spec_arc.name, spec_name, diff_detail, spec_name
287 ),
288 Some(source_loc.clone()),
289 None::<String>,
290 ));
291 break;
292 }
293 }
294 }
295
296 errors
297}
298
299fn collect_ref_spec_segments(plan: &ExecutionPlan) -> Vec<(Vec<PathSegment>, Arc<LemmaSpec>)> {
302 let mut seen = HashSet::new();
303 let mut result = Vec::new();
304
305 for (path, data) in &plan.facts {
306 if let FactData::SpecRef { spec, .. } = data {
307 let mut seg = path.segments.clone();
308 seg.push(PathSegment {
309 fact: path.fact.clone(),
310 spec: spec.name.clone(),
311 });
312 let key = seg
313 .iter()
314 .map(|s| format!("{}.{}", s.fact, s.spec))
315 .collect::<Vec<_>>()
316 .join("/");
317 if seen.insert(key) {
318 result.push((seg, Arc::clone(spec)));
319 }
320 }
321 }
322
323 result
324}
325
326fn find_ref_spec_in_plan(plan: &ExecutionPlan, segments: &[PathSegment]) -> Option<Arc<LemmaSpec>> {
328 if segments.is_empty() {
329 return None;
330 }
331 let parent_segments = &segments[..segments.len() - 1];
332 let target_seg = &segments[segments.len() - 1];
333
334 for (path, data) in &plan.facts {
335 if let FactData::SpecRef { spec, .. } = data {
336 if path.segments == *parent_segments
337 && path.fact == target_seg.fact
338 && spec.name == target_seg.spec
339 {
340 return Some(Arc::clone(spec));
341 }
342 }
343 }
344 None
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::planning::semantics::primitive_number;
351
352 #[test]
353 fn identical_interfaces_are_equal() {
354 let mut facts = BTreeMap::new();
355 facts.insert(
356 "x".to_string(),
357 FactKind::Value(Box::new(primitive_number().clone())),
358 );
359
360 let mut rules = BTreeMap::new();
361 rules.insert("z".to_string(), primitive_number().clone());
362
363 let a = SliceInterface {
364 facts: facts.clone(),
365 rules: rules.clone(),
366 types: BTreeMap::new(),
367 };
368 let b = SliceInterface {
369 facts,
370 rules,
371 types: BTreeMap::new(),
372 };
373 assert_eq!(a, b);
374 assert!(a.diff(&b).is_empty());
375 }
376
377 #[test]
378 fn added_fact_detected() {
379 let a = SliceInterface {
380 facts: BTreeMap::new(),
381 rules: BTreeMap::new(),
382 types: BTreeMap::new(),
383 };
384
385 let mut facts_b = BTreeMap::new();
386 facts_b.insert(
387 "y".to_string(),
388 FactKind::Value(Box::new(primitive_number().clone())),
389 );
390 let b = SliceInterface {
391 facts: facts_b,
392 rules: BTreeMap::new(),
393 types: BTreeMap::new(),
394 };
395
396 assert_ne!(a, b);
397 let diffs = a.diff(&b);
398 assert!(diffs.iter().any(|d| d.contains("'y' added")));
399 }
400
401 #[test]
402 fn removed_rule_detected() {
403 let mut rules_a = BTreeMap::new();
404 rules_a.insert("z".to_string(), primitive_number().clone());
405 let a = SliceInterface {
406 facts: BTreeMap::new(),
407 rules: rules_a,
408 types: BTreeMap::new(),
409 };
410 let b = SliceInterface {
411 facts: BTreeMap::new(),
412 rules: BTreeMap::new(),
413 types: BTreeMap::new(),
414 };
415
416 assert_ne!(a, b);
417 let diffs = a.diff(&b);
418 assert!(diffs.iter().any(|d| d.contains("'z' removed")));
419 }
420}