1pub mod execution_plan;
9pub mod graph;
10pub mod semantics;
11pub mod types;
12pub mod validation;
13
14pub use execution_plan::{Branch, DocumentSchema, ExecutableRule, ExecutionPlan};
15pub use semantics::{
16 ArithmeticComputation, ComparisonComputation, Expression, ExpressionKind, Fact, FactData,
17 FactPath, FactValue, LemmaType, LiteralValue, LogicalComputation, MathematicalComputation,
18 NegationType, PathSegment, RulePath, Source, Span, TypeExtends, ValueKind, VetoExpression,
19};
20pub use types::TypeRegistry;
21
22use crate::parsing::ast::LemmaDoc;
23use crate::LemmaError;
24use std::collections::HashMap;
25
26pub fn plan(
42 docs_to_plan: &[&LemmaDoc],
43 all_docs: &[LemmaDoc],
44 sources: HashMap<String, String>,
45) -> (HashMap<String, ExecutionPlan>, Vec<LemmaError>) {
46 let mut plans = HashMap::new();
47 let mut errors: Vec<LemmaError> = Vec::new();
48
49 let (prepared, type_errors) = graph::Graph::prepare_types(all_docs, &sources);
51 errors.extend(type_errors);
52
53 for doc in docs_to_plan {
55 match graph::Graph::build(doc, all_docs, sources.clone(), &prepared) {
56 Ok(graph) => {
57 let execution_plan = execution_plan::build_execution_plan(&graph, &doc.name);
58 let value_errors =
59 execution_plan::validate_literal_facts_against_types(&execution_plan);
60 if value_errors.is_empty() {
61 plans.insert(doc.name.clone(), execution_plan);
62 } else {
63 errors.extend(value_errors);
64 }
65 }
66 Err(doc_errors) => errors.extend(doc_errors),
67 }
68 }
69
70 (plans, errors)
71}
72
73#[cfg(test)]
78mod internal_tests {
79 use super::plan;
80 use crate::parsing::ast::{FactReference, FactValue, LemmaDoc, LemmaFact, Span};
81 use crate::parsing::source::Source;
82 use crate::planning::execution_plan::ExecutionPlan;
83 use crate::planning::semantics::{FactPath, PathSegment};
84 use crate::{parse, LemmaError, ResourceLimits};
85 use std::collections::HashMap;
86 use std::sync::Arc;
87
88 fn plan_single(
90 main_doc: &LemmaDoc,
91 all_docs: &[LemmaDoc],
92 sources: HashMap<String, String>,
93 ) -> Result<ExecutionPlan, Vec<LemmaError>> {
94 let docs_to_plan: Vec<&LemmaDoc> = vec![main_doc];
95 let (mut plans, errors) = plan(&docs_to_plan, all_docs, sources);
96 if !errors.is_empty() {
97 Err(errors)
98 } else {
99 plans.remove(&main_doc.name).map(Ok).unwrap_or_else(|| {
100 Err(vec![LemmaError::engine(
101 format!(
102 "No execution plan produced for document '{}'",
103 main_doc.name
104 ),
105 Some(crate::planning::semantics::Source::new(
106 "<test>",
107 crate::planning::semantics::Span {
108 start: 0,
109 end: 0,
110 line: 1,
111 col: 0,
112 },
113 main_doc.name.clone(),
114 std::sync::Arc::from("doc test\nfact x = 1"),
115 )),
116 None::<String>,
117 )])
118 })
119 }
120 }
121
122 #[test]
123 fn test_basic_validation() {
124 let input = r#"doc person
125fact name = "John"
126fact age = 25
127rule is_adult = age >= 18"#;
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 for doc in &docs {
135 let result = plan_single(doc, &docs, sources.clone());
136 assert!(
137 result.is_ok(),
138 "Basic validation should pass: {:?}",
139 result.err()
140 );
141 }
142 }
143
144 #[test]
145 fn test_duplicate_facts() {
146 let input = r#"doc person
147fact name = "John"
148fact name = "Jane""#;
149
150 let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
151
152 let mut sources = HashMap::new();
153 sources.insert("test.lemma".to_string(), input.to_string());
154
155 let result = plan_single(&docs[0], &docs, sources);
156
157 assert!(
158 result.is_err(),
159 "Duplicate facts should cause validation error"
160 );
161 let errors = result.unwrap_err();
162 let error_string = errors
163 .iter()
164 .map(|e| e.to_string())
165 .collect::<Vec<_>>()
166 .join(", ");
167 assert!(
168 error_string.contains("Duplicate fact"),
169 "Error should mention duplicate fact: {}",
170 error_string
171 );
172 assert!(error_string.contains("name"));
173 }
174
175 #[test]
176 fn test_duplicate_rules() {
177 let input = r#"doc person
178fact age = 25
179rule is_adult = age >= 18
180rule is_adult = age >= 21"#;
181
182 let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
183
184 let mut sources = HashMap::new();
185 sources.insert("test.lemma".to_string(), input.to_string());
186
187 let result = plan_single(&docs[0], &docs, sources);
188
189 assert!(
190 result.is_err(),
191 "Duplicate rules should cause validation error"
192 );
193 let errors = result.unwrap_err();
194 let error_string = errors
195 .iter()
196 .map(|e| e.to_string())
197 .collect::<Vec<_>>()
198 .join(", ");
199 assert!(
200 error_string.contains("Duplicate rule"),
201 "Error should mention duplicate rule: {}",
202 error_string
203 );
204 assert!(error_string.contains("is_adult"));
205 }
206
207 #[test]
208 fn test_circular_dependency() {
209 let input = r#"doc test
210rule a = b?
211rule b = a?"#;
212
213 let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
214
215 let mut sources = HashMap::new();
216 sources.insert("test.lemma".to_string(), input.to_string());
217
218 let result = plan_single(&docs[0], &docs, sources);
219
220 assert!(
221 result.is_err(),
222 "Circular dependency should cause validation error"
223 );
224 let errors = result.unwrap_err();
225 let error_string = errors
226 .iter()
227 .map(|e| e.to_string())
228 .collect::<Vec<_>>()
229 .join(", ");
230 assert!(error_string.contains("Circular dependency") || error_string.contains("circular"));
231 }
232
233 #[test]
234 fn test_reference_type_errors() {
235 let input = r#"doc test
236fact age = 25
237rule is_adult = age >= 18
238rule test1 = age?
239rule test2 = is_adult"#;
240
241 let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
242
243 let mut sources = HashMap::new();
244 sources.insert("test.lemma".to_string(), input.to_string());
245
246 let result = plan_single(&docs[0], &docs, sources);
247
248 assert!(
249 result.is_err(),
250 "Reference type errors should cause validation error"
251 );
252 let errors = result.unwrap_err();
253 let error_string = errors
254 .iter()
255 .map(|e| e.to_string())
256 .collect::<Vec<_>>()
257 .join(", ");
258 assert!(
259 error_string.contains("is a rule, not a fact") || error_string.contains("Reference"),
260 "Error should mention reference issue: {}",
261 error_string
262 );
263 }
264
265 #[test]
266 fn test_multiple_documents() {
267 let input = r#"doc person
268fact name = "John"
269fact age = 25
270
271doc company
272fact name = "Acme Corp"
273fact employee = doc person"#;
274
275 let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
276
277 let mut sources = HashMap::new();
278 sources.insert("test.lemma".to_string(), input.to_string());
279
280 let result = plan_single(&docs[0], &docs, sources);
281
282 assert!(
283 result.is_ok(),
284 "Multiple documents should validate successfully: {:?}",
285 result.err()
286 );
287 }
288
289 #[test]
290 fn test_invalid_document_reference() {
291 let input = r#"doc person
292fact name = "John"
293fact contract = doc nonexistent"#;
294
295 let docs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
296
297 let mut sources = HashMap::new();
298 sources.insert("test.lemma".to_string(), input.to_string());
299
300 let result = plan_single(&docs[0], &docs, sources);
301
302 assert!(
303 result.is_err(),
304 "Invalid document reference should cause validation error"
305 );
306 let errors = result.unwrap_err();
307 let error_string = errors
308 .iter()
309 .map(|e| e.to_string())
310 .collect::<Vec<_>>()
311 .join(", ");
312 assert!(
313 error_string.contains("not found") || error_string.contains("Document"),
314 "Error should mention document reference issue: {}",
315 error_string
316 );
317 assert!(error_string.contains("nonexistent"));
318 }
319
320 #[test]
321 fn test_type_declaration_empty_base_returns_lemma_error() {
322 let mut doc = LemmaDoc::new("test".to_string());
323 let source = Source::new(
324 "test.lemma",
325 Span {
326 start: 0,
327 end: 10,
328 line: 1,
329 col: 0,
330 },
331 "test",
332 Arc::from("fact x = []"),
333 );
334 doc.facts.push(LemmaFact::new(
335 FactReference {
336 segments: vec![],
337 fact: "x".to_string(),
338 },
339 FactValue::TypeDeclaration {
340 base: String::new(),
341 constraints: None,
342 from: None,
343 },
344 source,
345 ));
346
347 let docs = vec![doc.clone()];
348 let mut sources = HashMap::new();
349 sources.insert(
350 "test.lemma".to_string(),
351 "doc test\nfact x = []".to_string(),
352 );
353
354 let result = plan_single(&doc, &docs, sources);
355 assert!(
356 result.is_err(),
357 "TypeDeclaration with empty base should fail planning"
358 );
359 let errors = result.unwrap_err();
360 let combined = errors
361 .iter()
362 .map(|e| e.to_string())
363 .collect::<Vec<_>>()
364 .join("\n");
365 assert!(
366 combined.contains("TypeDeclaration base cannot be empty"),
367 "Error should mention empty base; got: {}",
368 combined
369 );
370 }
371
372 #[test]
373 fn test_fact_binding_with_custom_type_resolves_in_correct_document_context() {
374 let code = r#"
385doc one
386type money = number
387fact x = [money]
388
389doc two
390fact one = doc one
391fact one.x = 7
392rule getx = one.x
393"#;
394
395 let docs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
396 let doc_two = docs.iter().find(|d| d.name == "two").unwrap();
397
398 let mut sources = HashMap::new();
399 sources.insert("test.lemma".to_string(), code.to_string());
400 let execution_plan = plan_single(doc_two, &docs, sources).expect("planning should succeed");
401
402 let one_x_path = FactPath {
404 segments: vec![PathSegment {
405 fact: "one".to_string(),
406 doc: "one".to_string(),
407 }],
408 fact: "x".to_string(),
409 };
410
411 let one_x_type = execution_plan
412 .facts
413 .get(&one_x_path)
414 .and_then(|d| d.schema_type())
415 .expect("one.x should have a resolved type");
416
417 assert_eq!(
418 one_x_type.name(),
419 "money",
420 "one.x should have type 'money', got: {}",
421 one_x_type.name()
422 );
423 assert!(one_x_type.is_number(), "money should be number-based");
424 }
425
426 #[test]
427 fn test_plan_with_registry_style_doc_names() {
428 let source = r#"doc user/workspace/somedoc
429fact quantity = 10
430
431doc user/workspace/example
432fact inventory = doc @user/workspace/somedoc
433rule total_quantity = inventory.quantity"#;
434
435 let docs = parse(source, "registry_bundle.lemma", &ResourceLimits::default()).unwrap();
436 assert_eq!(docs.len(), 2);
437
438 let example_doc = docs
439 .iter()
440 .find(|d| d.name == "user/workspace/example")
441 .expect("should find user/workspace/example");
442
443 let mut sources = HashMap::new();
444 sources.insert("registry_bundle.lemma".to_string(), source.to_string());
445
446 let result = plan_single(example_doc, &docs, sources);
447 assert!(
448 result.is_ok(),
449 "Planning with @... document names should succeed: {:?}",
450 result.err()
451 );
452 }
453
454 #[test]
455 fn test_multiple_independent_errors_are_all_reported() {
456 let source = r#"doc demo
459type money from nonexistent_type_source
460fact helper = doc nonexistent_doc
461fact price = 10
462rule total = helper.value + price"#;
463
464 let docs = parse(source, "test.lemma", &ResourceLimits::default()).unwrap();
465
466 let mut sources = HashMap::new();
467 sources.insert("test.lemma".to_string(), source.to_string());
468
469 let result = plan_single(&docs[0], &docs, sources);
470 assert!(result.is_err(), "Planning should fail with multiple errors");
471
472 let errors = result.unwrap_err();
473 let all_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
474 let combined = all_messages.join("\n");
475
476 assert!(
478 combined.contains("Unknown type") && combined.contains("money"),
479 "Should report type import error for 'money'. Got:\n{}",
480 combined
481 );
482
483 assert!(
485 combined.contains("nonexistent_doc"),
486 "Should report doc reference error for 'nonexistent_doc'. Got:\n{}",
487 combined
488 );
489
490 assert!(
492 errors.len() >= 2,
493 "Expected at least 2 errors, got {}: {}",
494 errors.len(),
495 combined
496 );
497 }
498
499 #[test]
500 fn test_type_error_does_not_suppress_cross_doc_fact_error() {
501 let source = r#"doc demo
505type currency from missing_doc
506fact ext = doc also_missing
507rule val = ext.some_fact"#;
508
509 let docs = parse(source, "test.lemma", &ResourceLimits::default()).unwrap();
510
511 let mut sources = HashMap::new();
512 sources.insert("test.lemma".to_string(), source.to_string());
513
514 let result = plan_single(&docs[0], &docs, sources);
515 assert!(result.is_err());
516
517 let errors = result.unwrap_err();
518 let combined: String = errors
519 .iter()
520 .map(|e| e.to_string())
521 .collect::<Vec<_>>()
522 .join("\n");
523
524 assert!(
526 combined.contains("currency"),
527 "Should report type error about 'currency'. Got:\n{}",
528 combined
529 );
530
531 assert!(
533 combined.contains("also_missing"),
534 "Should report error about 'also_missing'. Got:\n{}",
535 combined
536 );
537 }
538}