Skip to main content

lemma/planning/
mod.rs

1//! Planning module for Lemma documents
2//!
3//! This module performs complete static analysis and builds execution plans:
4//! - Builds Graph with facts and rules (validated, with types computed)
5//! - Builds ExecutionPlan from Graph (topologically sorted, ready for evaluation)
6//! - Validates document structure and references
7
8pub mod execution_plan;
9pub mod graph;
10pub mod types;
11pub mod validation;
12
13pub use execution_plan::{Branch, ExecutableRule, ExecutionPlan};
14pub use types::TypeRegistry;
15
16use crate::semantic::LemmaDoc;
17use crate::LemmaError;
18use std::collections::HashMap;
19
20/// Builds an execution plan from Lemma documents.
21///
22/// The `sources` parameter maps source IDs (filenames) to their source code,
23/// needed for extracting original expression text in proofs.
24pub fn plan(
25    main_doc: &LemmaDoc,
26    all_docs: &[LemmaDoc],
27    sources: HashMap<String, String>,
28) -> Result<ExecutionPlan, Vec<LemmaError>> {
29    validate_all_documents(all_docs)?;
30
31    let graph = graph::Graph::build(main_doc, all_docs, sources)?;
32    let execution_plan = execution_plan::build_execution_plan(&graph, &main_doc.name);
33    let value_errors = execution_plan::validate_literal_facts_against_types(&execution_plan);
34    if !value_errors.is_empty() {
35        return Err(value_errors);
36    }
37    Ok(execution_plan)
38}
39
40/// Validate all documents
41fn validate_all_documents(all_docs: &[LemmaDoc]) -> Result<(), Vec<LemmaError>> {
42    let mut errors = Vec::new();
43
44    // Pass all_docs to validate_types so cross-document type imports can resolve
45    for doc in all_docs {
46        if let Err(doc_errors) = validation::validate_types(doc, Some(all_docs)) {
47            errors.extend(doc_errors);
48        }
49    }
50
51    if errors.is_empty() {
52        Ok(())
53    } else {
54        Err(errors)
55    }
56}
57
58// ============================================================================
59// Tests
60// ============================================================================
61
62#[cfg(test)]
63mod internal_tests {
64    use super::plan;
65    use crate::semantic::{FactPath, PathSegment};
66    use crate::{parse, ResourceLimits};
67    use std::collections::HashMap;
68
69    #[test]
70    fn test_basic_validation() {
71        let input = r#"doc person
72fact name = "John"
73fact age = 25
74rule is_adult = age >= 18"#;
75
76        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
77
78        let mut sources = HashMap::new();
79        sources.insert("test.lemma".to_string(), input.to_string());
80
81        for doc in &docs {
82            let result = plan(doc, &docs, sources.clone());
83            assert!(
84                result.is_ok(),
85                "Basic validation should pass: {:?}",
86                result.err()
87            );
88        }
89    }
90
91    #[test]
92    fn test_duplicate_facts() {
93        let input = r#"doc person
94fact name = "John"
95fact name = "Jane""#;
96
97        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
98
99        let mut sources = HashMap::new();
100        sources.insert("test.lemma".to_string(), input.to_string());
101
102        let result = plan(&docs[0], &docs, sources);
103
104        assert!(
105            result.is_err(),
106            "Duplicate facts should cause validation error"
107        );
108        let errors = result.unwrap_err();
109        let error_string = errors
110            .iter()
111            .map(|e| e.to_string())
112            .collect::<Vec<_>>()
113            .join(", ");
114        assert!(
115            error_string.contains("Duplicate fact"),
116            "Error should mention duplicate fact: {}",
117            error_string
118        );
119        assert!(error_string.contains("name"));
120    }
121
122    #[test]
123    fn test_duplicate_rules() {
124        let input = r#"doc person
125fact age = 25
126rule is_adult = age >= 18
127rule is_adult = age >= 21"#;
128
129        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
130
131        let mut sources = HashMap::new();
132        sources.insert("test.lemma".to_string(), input.to_string());
133
134        let result = plan(&docs[0], &docs, sources);
135
136        assert!(
137            result.is_err(),
138            "Duplicate rules should cause validation error"
139        );
140        let errors = result.unwrap_err();
141        let error_string = errors
142            .iter()
143            .map(|e| e.to_string())
144            .collect::<Vec<_>>()
145            .join(", ");
146        assert!(
147            error_string.contains("Duplicate rule"),
148            "Error should mention duplicate rule: {}",
149            error_string
150        );
151        assert!(error_string.contains("is_adult"));
152    }
153
154    #[test]
155    fn test_circular_dependency() {
156        let input = r#"doc test
157rule a = b?
158rule b = a?"#;
159
160        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
161
162        let mut sources = HashMap::new();
163        sources.insert("test.lemma".to_string(), input.to_string());
164
165        let result = plan(&docs[0], &docs, sources);
166
167        assert!(
168            result.is_err(),
169            "Circular dependency should cause validation error"
170        );
171        let errors = result.unwrap_err();
172        let error_string = errors
173            .iter()
174            .map(|e| e.to_string())
175            .collect::<Vec<_>>()
176            .join(", ");
177        assert!(error_string.contains("Circular dependency") || error_string.contains("circular"));
178    }
179
180    #[test]
181    fn test_reference_type_errors() {
182        let input = r#"doc test
183fact age = 25
184rule is_adult = age >= 18
185rule test1 = age?
186rule test2 = is_adult"#;
187
188        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
189
190        let mut sources = HashMap::new();
191        sources.insert("test.lemma".to_string(), input.to_string());
192
193        let result = plan(&docs[0], &docs, sources);
194
195        assert!(
196            result.is_err(),
197            "Reference type errors should cause validation error"
198        );
199        let errors = result.unwrap_err();
200        let error_string = errors
201            .iter()
202            .map(|e| e.to_string())
203            .collect::<Vec<_>>()
204            .join(", ");
205        assert!(
206            error_string.contains("is a rule, not a fact") || error_string.contains("Reference"),
207            "Error should mention reference issue: {}",
208            error_string
209        );
210    }
211
212    #[test]
213    fn test_multiple_documents() {
214        let input = r#"doc person
215fact name = "John"
216fact age = 25
217
218doc company
219fact name = "Acme Corp"
220fact employee = doc person"#;
221
222        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
223
224        let mut sources = HashMap::new();
225        sources.insert("test.lemma".to_string(), input.to_string());
226
227        let result = plan(&docs[0], &docs, sources);
228
229        assert!(
230            result.is_ok(),
231            "Multiple documents should validate successfully: {:?}",
232            result.err()
233        );
234    }
235
236    #[test]
237    fn test_invalid_document_reference() {
238        let input = r#"doc person
239fact name = "John"
240fact contract = doc nonexistent"#;
241
242        let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
243
244        let mut sources = HashMap::new();
245        sources.insert("test.lemma".to_string(), input.to_string());
246
247        let result = plan(&docs[0], &docs, sources);
248
249        assert!(
250            result.is_err(),
251            "Invalid document reference should cause validation error"
252        );
253        let errors = result.unwrap_err();
254        let error_string = errors
255            .iter()
256            .map(|e| e.to_string())
257            .collect::<Vec<_>>()
258            .join(", ");
259        assert!(
260            error_string.contains("not found") || error_string.contains("Document"),
261            "Error should mention document reference issue: {}",
262            error_string
263        );
264        assert!(error_string.contains("nonexistent"));
265    }
266
267    #[test]
268    fn test_fact_override_with_custom_type_resolves_in_correct_document_context() {
269        // This is a planning-level test: ensure fact overrides resolve custom types correctly
270        // when the type is defined in a different document than the override.
271        //
272        // doc one:
273        //   type money = number
274        //   fact x = [money]
275        // doc two:
276        //   fact one = doc one
277        //   fact one.x = 7
278        //   rule getx = one.x
279        let code = r#"
280doc one
281type money = number
282fact x = [money]
283
284doc two
285fact one = doc one
286fact one.x = 7
287rule getx = one.x
288"#;
289
290        let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
291        let doc_two = docs.iter().find(|d| d.name == "two").unwrap();
292
293        let execution_plan = plan(doc_two, &docs, HashMap::new()).expect("planning should succeed");
294
295        // Verify that one.x has type 'money' (resolved from doc one)
296        let one_x_path = FactPath {
297            segments: vec![PathSegment {
298                fact: "one".to_string(),
299                doc: "one".to_string(),
300            }],
301            fact: "x".to_string(),
302        };
303
304        let one_x_type = execution_plan
305            .fact_schema
306            .get(&one_x_path)
307            .expect("one.x should have a resolved type");
308
309        assert_eq!(
310            one_x_type.name(),
311            "money",
312            "one.x should have type 'money', got: {}",
313            one_x_type.name()
314        );
315        assert!(one_x_type.is_number(), "money should be number-based");
316    }
317}