Skip to main content

lemma/planning/
slice_interface.rs

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/// The resolved interface of a referenced spec within a single temporal slice.
13///
14/// Captures only what the caller actually uses: needed facts, referenced rules,
15/// and type definitions. Two SliceInterfaces are equal iff the caller sees the
16/// exact same contract from the referenced spec in both slices.
17#[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    /// Extract the interface of a referenced spec from a built execution plan.
32    ///
33    /// `segments` identifies the referenced spec (e.g. `[PathSegment { fact: "b", spec: "B" }]`).
34    /// Uses the plan's precomputed `needs_facts` to determine which facts matter.
35    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
129/// Collect fact names at `segments` depth that any root-level rule needs.
130///
131/// Uses the plan's precomputed `needs_facts` (transitive closure) and also
132/// extracts intermediate SpecRef traversal facts: if a needed FactPath or
133/// a referenced RulePath passes through a deeper segment, the linking fact at
134/// `segments` depth is itself a needed interface fact.
135fn 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    // RulePath references at deeper segments also imply an intermediate
156    // SpecRef fact at our level (e.g. `b.nested.val` means `nested` is needed).
157    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
167/// Collect rule names at `segments` depth that root-level rules directly reference.
168///
169/// Walks root-level rule expressions for RulePath references at the dep's depth.
170/// Internal dep rules (only reachable transitively within the dep) are excluded —
171/// the caller only cares about the dep rules it explicitly uses.
172fn 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
186/// Collect all RulePath references from root-level rule expressions.
187fn 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
236/// Validate that all temporal slices of a spec see the same interface
237/// from each referenced spec.
238pub(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
299/// Find all first-level referenced spec segments, plus nested ones reachable
300/// through the plan's facts/rules.
301fn 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
326/// Find the Arc<LemmaSpec> for a referenced spec in a plan by matching segments.
327fn 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}