1pub 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
20pub 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
40fn validate_all_documents(all_docs: &[LemmaDoc]) -> Result<(), Vec<LemmaError>> {
42 let mut errors = Vec::new();
43
44 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#[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 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 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}