1pub mod content_hash;
9pub mod execution_plan;
10pub mod graph;
11pub mod semantics;
12pub mod slice_interface;
13pub mod temporal;
14pub mod types;
15pub mod validation;
16pub use execution_plan::{Branch, ExecutableRule, ExecutionPlan, SpecSchema};
17pub use semantics::{
18 negated_comparison, ArithmeticComputation, ComparisonComputation, Expression, ExpressionKind,
19 Fact, FactData, FactPath, FactValue, LemmaType, LiteralValue, LogicalComputation,
20 MathematicalComputation, NegationType, PathSegment, RulePath, Source, Span, TypeExtends,
21 ValueKind, VetoExpression,
22};
23pub use types::TypeResolver;
24
25use crate::engine::Context;
26use crate::parsing::ast::LemmaSpec;
27use crate::Error;
28use std::collections::HashMap;
29use std::sync::Arc;
30
31#[derive(Debug, Clone)]
33pub struct SpecPlanningResult {
34 pub spec: Arc<LemmaSpec>,
36 pub plans: Vec<ExecutionPlan>,
38 pub errors: Vec<Error>,
40 pub hash_pin: String,
42}
43
44#[derive(Debug, Clone)]
46pub struct PlanningResult {
47 pub per_spec: Vec<SpecPlanningResult>,
49 pub global_errors: Vec<Error>,
51}
52
53pub fn plan(context: &Context, sources: HashMap<String, String>) -> PlanningResult {
64 let mut global_errors: Vec<Error> = Vec::new();
65 global_errors.extend(temporal::validate_temporal_coverage(context));
66
67 let mut type_resolver = TypeResolver::new();
68 let all_specs: Vec<_> = context.iter().collect();
69 for spec_arc in &all_specs {
70 global_errors.extend(type_resolver.register_all(spec_arc));
71 }
72 let (mut resolved_types, type_errors) = type_resolver.resolve(all_specs.clone());
73 global_errors.extend(type_errors);
74
75 let mut per_spec: Vec<SpecPlanningResult> = Vec::new();
76
77 if !global_errors.is_empty() {
78 return PlanningResult {
79 per_spec,
80 global_errors,
81 };
82 }
83
84 let spec_hashes: graph::SpecContentHashes = all_specs
87 .iter()
88 .map(|s| (graph::spec_hash_key(s), content_hash::hash_spec(s, &[])))
89 .collect();
90
91 for spec_arc in &all_specs {
92 let slices = temporal::compute_temporal_slices(spec_arc, context);
93 let mut spec_plans: Vec<ExecutionPlan> = Vec::new();
94 let mut spec_errors: Vec<Error> = Vec::new();
95 let mut slice_resolved_types: Vec<HashMap<Arc<LemmaSpec>, types::ResolvedSpecTypes>> =
96 Vec::new();
97
98 for slice in &slices {
99 match graph::Graph::build(
100 spec_arc,
101 context,
102 sources.clone(),
103 &type_resolver,
104 &resolved_types,
105 slice.from.clone(),
106 &spec_hashes,
107 ) {
108 Ok((graph, slice_types)) => {
109 for (arc, types) in &slice_types {
110 resolved_types.insert(Arc::clone(arc), types.clone());
111 }
112 let execution_plan = execution_plan::build_execution_plan(
113 &graph,
114 spec_arc.name.as_str(),
115 slice.from.clone(),
116 slice.to.clone(),
117 );
118 let value_errors =
119 execution_plan::validate_literal_facts_against_types(&execution_plan);
120 if value_errors.is_empty() {
121 spec_plans.push(execution_plan);
122 } else {
123 spec_errors.extend(value_errors);
124 }
125 slice_resolved_types.push(slice_types);
126 }
127 Err(build_errors) => {
128 spec_errors.extend(build_errors);
129 }
130 }
131 }
132
133 if spec_errors.is_empty() && spec_plans.len() > 1 {
134 spec_errors.extend(slice_interface::validate_slice_interfaces(
135 &spec_arc.name,
136 &spec_plans,
137 &slice_resolved_types,
138 ));
139 }
140
141 let hash = spec_hashes
142 .get(&graph::spec_hash_key(spec_arc))
143 .cloned()
144 .unwrap_or_else(|| {
145 unreachable!("BUG: spec '{}' missing from spec_hashes", spec_arc.name)
146 });
147
148 per_spec.push(SpecPlanningResult {
149 spec: Arc::clone(spec_arc),
150 plans: spec_plans,
151 errors: spec_errors,
152 hash_pin: hash,
153 });
154 }
155
156 PlanningResult {
157 per_spec,
158 global_errors,
159 }
160}
161
162#[cfg(test)]
167mod internal_tests {
168 use super::plan;
169 use crate::engine::Context;
170 use crate::parsing::ast::{FactValue, LemmaFact, LemmaSpec, Reference, Span};
171 use crate::parsing::source::Source;
172 use crate::planning::execution_plan::ExecutionPlan;
173 use crate::planning::semantics::{FactPath, PathSegment};
174 use crate::{parse, Error, ResourceLimits};
175 use std::collections::HashMap;
176 use std::sync::Arc;
177
178 fn plan_single(
180 main_spec: &LemmaSpec,
181 all_specs: &[LemmaSpec],
182 sources: HashMap<String, String>,
183 ) -> Result<ExecutionPlan, Vec<Error>> {
184 let mut ctx = Context::new();
185 for spec in all_specs {
186 if let Err(e) = ctx.insert_spec(Arc::new(spec.clone())) {
187 return Err(vec![e]);
188 }
189 }
190 let main_spec_arc = ctx
191 .get_spec_effective_from(main_spec.name.as_str(), main_spec.effective_from())
192 .expect("main_spec must be in all_specs");
193 let result = plan(&ctx, sources);
194 let all_errors: Vec<Error> = result
195 .global_errors
196 .into_iter()
197 .chain(
198 result
199 .per_spec
200 .iter()
201 .flat_map(|r| r.errors.clone())
202 .collect::<Vec<_>>(),
203 )
204 .collect();
205 if !all_errors.is_empty() {
206 return Err(all_errors);
207 }
208 match result
209 .per_spec
210 .into_iter()
211 .find(|r| Arc::ptr_eq(&r.spec, &main_spec_arc))
212 {
213 Some(spec_result) if !spec_result.plans.is_empty() => {
214 let mut plans = spec_result.plans;
215 Ok(plans.remove(0))
216 }
217 _ => Err(vec![Error::validation(
218 format!("No execution plan produced for spec '{}'", main_spec.name),
219 Some(crate::planning::semantics::Source::new(
220 "<test>",
221 crate::planning::semantics::Span {
222 start: 0,
223 end: 0,
224 line: 1,
225 col: 0,
226 },
227 std::sync::Arc::from("spec test\nfact x: 1"),
228 )),
229 None::<String>,
230 )]),
231 }
232 }
233
234 #[test]
235 fn test_basic_validation() {
236 let input = r#"spec person
237fact name: "John"
238fact age: 25
239rule is_adult: age >= 18"#;
240
241 let specs = parse(input, "test.lemma", &ResourceLimits::default())
242 .unwrap()
243 .specs;
244
245 let mut sources = HashMap::new();
246 sources.insert("test.lemma".to_string(), input.to_string());
247
248 for spec in &specs {
249 let result = plan_single(spec, &specs, sources.clone());
250 assert!(
251 result.is_ok(),
252 "Basic validation should pass: {:?}",
253 result.err()
254 );
255 }
256 }
257
258 #[test]
259 fn test_duplicate_facts() {
260 let input = r#"spec person
261fact name: "John"
262fact name: "Jane""#;
263
264 let specs = parse(input, "test.lemma", &ResourceLimits::default())
265 .unwrap()
266 .specs;
267
268 let mut sources = HashMap::new();
269 sources.insert("test.lemma".to_string(), input.to_string());
270
271 let result = plan_single(&specs[0], &specs, sources);
272
273 assert!(
274 result.is_err(),
275 "Duplicate facts should cause validation error"
276 );
277 let errors = result.unwrap_err();
278 let error_string = errors
279 .iter()
280 .map(|e| e.to_string())
281 .collect::<Vec<_>>()
282 .join(", ");
283 assert!(
284 error_string.contains("Duplicate fact"),
285 "Error should mention duplicate fact: {}",
286 error_string
287 );
288 assert!(error_string.contains("name"));
289 }
290
291 #[test]
292 fn test_duplicate_rules() {
293 let input = r#"spec person
294fact age: 25
295rule is_adult: age >= 18
296rule is_adult: age >= 21"#;
297
298 let specs = parse(input, "test.lemma", &ResourceLimits::default())
299 .unwrap()
300 .specs;
301
302 let mut sources = HashMap::new();
303 sources.insert("test.lemma".to_string(), input.to_string());
304
305 let result = plan_single(&specs[0], &specs, sources);
306
307 assert!(
308 result.is_err(),
309 "Duplicate rules should cause validation error"
310 );
311 let errors = result.unwrap_err();
312 let error_string = errors
313 .iter()
314 .map(|e| e.to_string())
315 .collect::<Vec<_>>()
316 .join(", ");
317 assert!(
318 error_string.contains("Duplicate rule"),
319 "Error should mention duplicate rule: {}",
320 error_string
321 );
322 assert!(error_string.contains("is_adult"));
323 }
324
325 #[test]
326 fn test_circular_dependency() {
327 let input = r#"spec test
328rule a: b
329rule b: a"#;
330
331 let specs = parse(input, "test.lemma", &ResourceLimits::default())
332 .unwrap()
333 .specs;
334
335 let mut sources = HashMap::new();
336 sources.insert("test.lemma".to_string(), input.to_string());
337
338 let result = plan_single(&specs[0], &specs, sources);
339
340 assert!(
341 result.is_err(),
342 "Circular dependency should cause validation error"
343 );
344 let errors = result.unwrap_err();
345 let error_string = errors
346 .iter()
347 .map(|e| e.to_string())
348 .collect::<Vec<_>>()
349 .join(", ");
350 assert!(error_string.contains("Circular dependency") || error_string.contains("circular"));
351 }
352
353 #[test]
354 fn test_unified_references_work() {
355 let input = r#"spec test
356fact age: 25
357rule is_adult: age >= 18
358rule test1: age
359rule test2: is_adult"#;
360
361 let specs = parse(input, "test.lemma", &ResourceLimits::default())
362 .unwrap()
363 .specs;
364
365 let mut sources = HashMap::new();
366 sources.insert("test.lemma".to_string(), input.to_string());
367
368 let result = plan_single(&specs[0], &specs, sources);
369
370 assert!(
371 result.is_ok(),
372 "Unified references should work: {:?}",
373 result.err()
374 );
375 }
376
377 #[test]
378 fn test_multiple_specs() {
379 let input = r#"spec person
380fact name: "John"
381fact age: 25
382
383spec company
384fact name: "Acme Corp"
385fact employee: spec person"#;
386
387 let specs = parse(input, "test.lemma", &ResourceLimits::default())
388 .unwrap()
389 .specs;
390
391 let mut sources = HashMap::new();
392 sources.insert("test.lemma".to_string(), input.to_string());
393
394 let result = plan_single(&specs[0], &specs, sources);
395
396 assert!(
397 result.is_ok(),
398 "Multiple specs should validate successfully: {:?}",
399 result.err()
400 );
401 }
402
403 #[test]
404 fn test_invalid_spec_reference() {
405 let input = r#"spec person
406fact name: "John"
407fact contract: spec nonexistent"#;
408
409 let specs = parse(input, "test.lemma", &ResourceLimits::default())
410 .unwrap()
411 .specs;
412
413 let mut sources = HashMap::new();
414 sources.insert("test.lemma".to_string(), input.to_string());
415
416 let result = plan_single(&specs[0], &specs, sources);
417
418 assert!(
419 result.is_err(),
420 "Invalid spec reference should cause validation error"
421 );
422 let errors = result.unwrap_err();
423 let error_string = errors
424 .iter()
425 .map(|e| e.to_string())
426 .collect::<Vec<_>>()
427 .join(", ");
428 assert!(
429 error_string.contains("not found")
430 || error_string.contains("Spec")
431 || (error_string.contains("nonexistent") && error_string.contains("depends")),
432 "Error should mention spec reference issue: {}",
433 error_string
434 );
435 assert!(error_string.contains("nonexistent"));
436 }
437
438 #[test]
439 fn test_type_declaration_empty_base_returns_lemma_error() {
440 let mut spec = LemmaSpec::new("test".to_string());
441 let source = Source::new(
442 "test.lemma",
443 Span {
444 start: 0,
445 end: 10,
446 line: 1,
447 col: 0,
448 },
449 Arc::from("fact x: []"),
450 );
451 spec.facts.push(LemmaFact::new(
452 Reference {
453 segments: vec![],
454 name: "x".to_string(),
455 },
456 FactValue::TypeDeclaration {
457 base: String::new(),
458 constraints: None,
459 from: None,
460 },
461 source,
462 ));
463
464 let specs = vec![spec.clone()];
465 let mut sources = HashMap::new();
466 sources.insert(
467 "test.lemma".to_string(),
468 "spec test\nfact x: []".to_string(),
469 );
470
471 let result = plan_single(&spec, &specs, sources);
472 assert!(
473 result.is_err(),
474 "TypeDeclaration with empty base should fail planning"
475 );
476 let errors = result.unwrap_err();
477 let combined = errors
478 .iter()
479 .map(|e| e.to_string())
480 .collect::<Vec<_>>()
481 .join("\n");
482 assert!(
483 combined.contains("TypeDeclaration base cannot be empty"),
484 "Error should mention empty base; got: {}",
485 combined
486 );
487 }
488
489 #[test]
490 fn test_fact_binding_with_custom_type_resolves_in_correct_spec_context() {
491 let code = r#"
502spec one
503type money: number
504fact x: [money]
505
506spec two
507fact one: spec one
508fact one.x: 7
509rule getx: one.x
510"#;
511
512 let specs = parse(code, "test.lemma", &ResourceLimits::default())
513 .unwrap()
514 .specs;
515 let spec_two = specs.iter().find(|d| d.name == "two").unwrap();
516
517 let mut sources = HashMap::new();
518 sources.insert("test.lemma".to_string(), code.to_string());
519 let execution_plan =
520 plan_single(spec_two, &specs, sources).expect("planning should succeed");
521
522 let one_x_path = FactPath {
524 segments: vec![PathSegment {
525 fact: "one".to_string(),
526 spec: "one".to_string(),
527 }],
528 fact: "x".to_string(),
529 };
530
531 let one_x_type = execution_plan
532 .facts
533 .get(&one_x_path)
534 .and_then(|d| d.schema_type())
535 .expect("one.x should have a resolved type");
536
537 assert_eq!(
538 one_x_type.name(),
539 "money",
540 "one.x should have type 'money', got: {}",
541 one_x_type.name()
542 );
543 assert!(one_x_type.is_number(), "money should be number-based");
544 }
545
546 #[test]
547 fn test_plan_with_registry_style_spec_names() {
548 let source = r#"spec @user/workspace/somespec
549fact quantity: 10
550
551spec user/workspace/example
552fact inventory: spec @user/workspace/somespec
553rule total_quantity: inventory.quantity"#;
554
555 let specs = parse(source, "registry_bundle.lemma", &ResourceLimits::default())
556 .unwrap()
557 .specs;
558 assert_eq!(specs.len(), 2);
559
560 let example_spec = specs
561 .iter()
562 .find(|d| d.name == "user/workspace/example")
563 .expect("should find user/workspace/example");
564
565 let mut sources = HashMap::new();
566 sources.insert("registry_bundle.lemma".to_string(), source.to_string());
567
568 let result = plan_single(example_spec, &specs, sources);
569 assert!(
570 result.is_ok(),
571 "Planning with @... spec names should succeed: {:?}",
572 result.err()
573 );
574 }
575
576 #[test]
577 fn test_multiple_independent_errors_are_all_reported() {
578 let source = r#"spec demo
581type money from nonexistent_type_source
582fact helper: spec nonexistent_spec
583fact price: 10
584rule total: helper.value + price"#;
585
586 let specs = parse(source, "test.lemma", &ResourceLimits::default())
587 .unwrap()
588 .specs;
589
590 let mut sources = HashMap::new();
591 sources.insert("test.lemma".to_string(), source.to_string());
592
593 let result = plan_single(&specs[0], &specs, sources);
594 assert!(result.is_err(), "Planning should fail with multiple errors");
595
596 let errors = result.unwrap_err();
597 let all_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
598 let combined = all_messages.join("\n");
599
600 assert!(
602 combined.contains("Unknown type") && combined.contains("money"),
603 "Should report type import error for 'money'. Got:\n{}",
604 combined
605 );
606
607 assert!(
609 combined.contains("nonexistent_spec"),
610 "Should report spec reference error for 'nonexistent_spec'. Got:\n{}",
611 combined
612 );
613
614 assert!(
616 errors.len() >= 2,
617 "Expected at least 2 errors, got {}: {}",
618 errors.len(),
619 combined
620 );
621 }
622
623 #[test]
624 fn test_type_error_does_not_suppress_cross_spec_fact_error() {
625 let source = r#"spec demo
629type currency from missing_spec
630fact ext: spec also_missing
631rule val: ext.some_fact"#;
632
633 let specs = parse(source, "test.lemma", &ResourceLimits::default())
634 .unwrap()
635 .specs;
636
637 let mut sources = HashMap::new();
638 sources.insert("test.lemma".to_string(), source.to_string());
639
640 let result = plan_single(&specs[0], &specs, sources);
641 assert!(result.is_err());
642
643 let errors = result.unwrap_err();
644 let combined: String = errors
645 .iter()
646 .map(|e| e.to_string())
647 .collect::<Vec<_>>()
648 .join("\n");
649
650 assert!(
652 combined.contains("currency"),
653 "Should report type error about 'currency'. Got:\n{}",
654 combined
655 );
656
657 assert!(
659 combined.contains("also_missing"),
660 "Should report error about 'also_missing'. Got:\n{}",
661 combined
662 );
663 }
664}