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 main_spec.name.clone(),
228 std::sync::Arc::from("spec test\nfact x: 1"),
229 )),
230 None::<String>,
231 )]),
232 }
233 }
234
235 #[test]
236 fn test_basic_validation() {
237 let input = r#"spec person
238fact name: "John"
239fact age: 25
240rule is_adult: age >= 18"#;
241
242 let specs = 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 for spec in &specs {
248 let result = plan_single(spec, &specs, sources.clone());
249 assert!(
250 result.is_ok(),
251 "Basic validation should pass: {:?}",
252 result.err()
253 );
254 }
255 }
256
257 #[test]
258 fn test_duplicate_facts() {
259 let input = r#"spec person
260fact name: "John"
261fact name: "Jane""#;
262
263 let specs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
264
265 let mut sources = HashMap::new();
266 sources.insert("test.lemma".to_string(), input.to_string());
267
268 let result = plan_single(&specs[0], &specs, sources);
269
270 assert!(
271 result.is_err(),
272 "Duplicate facts should cause validation error"
273 );
274 let errors = result.unwrap_err();
275 let error_string = errors
276 .iter()
277 .map(|e| e.to_string())
278 .collect::<Vec<_>>()
279 .join(", ");
280 assert!(
281 error_string.contains("Duplicate fact"),
282 "Error should mention duplicate fact: {}",
283 error_string
284 );
285 assert!(error_string.contains("name"));
286 }
287
288 #[test]
289 fn test_duplicate_rules() {
290 let input = r#"spec person
291fact age: 25
292rule is_adult: age >= 18
293rule is_adult: age >= 21"#;
294
295 let specs = 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(&specs[0], &specs, sources);
301
302 assert!(
303 result.is_err(),
304 "Duplicate rules 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("Duplicate rule"),
314 "Error should mention duplicate rule: {}",
315 error_string
316 );
317 assert!(error_string.contains("is_adult"));
318 }
319
320 #[test]
321 fn test_circular_dependency() {
322 let input = r#"spec test
323rule a: b
324rule b: a"#;
325
326 let specs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
327
328 let mut sources = HashMap::new();
329 sources.insert("test.lemma".to_string(), input.to_string());
330
331 let result = plan_single(&specs[0], &specs, sources);
332
333 assert!(
334 result.is_err(),
335 "Circular dependency should cause validation error"
336 );
337 let errors = result.unwrap_err();
338 let error_string = errors
339 .iter()
340 .map(|e| e.to_string())
341 .collect::<Vec<_>>()
342 .join(", ");
343 assert!(error_string.contains("Circular dependency") || error_string.contains("circular"));
344 }
345
346 #[test]
347 fn test_unified_references_work() {
348 let input = r#"spec test
349fact age: 25
350rule is_adult: age >= 18
351rule test1: age
352rule test2: is_adult"#;
353
354 let specs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
355
356 let mut sources = HashMap::new();
357 sources.insert("test.lemma".to_string(), input.to_string());
358
359 let result = plan_single(&specs[0], &specs, sources);
360
361 assert!(
362 result.is_ok(),
363 "Unified references should work: {:?}",
364 result.err()
365 );
366 }
367
368 #[test]
369 fn test_multiple_specs() {
370 let input = r#"spec person
371fact name: "John"
372fact age: 25
373
374spec company
375fact name: "Acme Corp"
376fact employee: spec person"#;
377
378 let specs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
379
380 let mut sources = HashMap::new();
381 sources.insert("test.lemma".to_string(), input.to_string());
382
383 let result = plan_single(&specs[0], &specs, sources);
384
385 assert!(
386 result.is_ok(),
387 "Multiple specs should validate successfully: {:?}",
388 result.err()
389 );
390 }
391
392 #[test]
393 fn test_invalid_spec_reference() {
394 let input = r#"spec person
395fact name: "John"
396fact contract: spec nonexistent"#;
397
398 let specs = parse(input, "test.lemma", &ResourceLimits::default()).unwrap();
399
400 let mut sources = HashMap::new();
401 sources.insert("test.lemma".to_string(), input.to_string());
402
403 let result = plan_single(&specs[0], &specs, sources);
404
405 assert!(
406 result.is_err(),
407 "Invalid spec reference should cause validation error"
408 );
409 let errors = result.unwrap_err();
410 let error_string = errors
411 .iter()
412 .map(|e| e.to_string())
413 .collect::<Vec<_>>()
414 .join(", ");
415 assert!(
416 error_string.contains("not found")
417 || error_string.contains("Spec")
418 || (error_string.contains("nonexistent") && error_string.contains("depends")),
419 "Error should mention spec reference issue: {}",
420 error_string
421 );
422 assert!(error_string.contains("nonexistent"));
423 }
424
425 #[test]
426 fn test_type_declaration_empty_base_returns_lemma_error() {
427 let mut spec = LemmaSpec::new("test".to_string());
428 let source = Source::new(
429 "test.lemma",
430 Span {
431 start: 0,
432 end: 10,
433 line: 1,
434 col: 0,
435 },
436 "test",
437 Arc::from("fact x: []"),
438 );
439 spec.facts.push(LemmaFact::new(
440 Reference {
441 segments: vec![],
442 name: "x".to_string(),
443 },
444 FactValue::TypeDeclaration {
445 base: String::new(),
446 constraints: None,
447 from: None,
448 },
449 source,
450 ));
451
452 let specs = vec![spec.clone()];
453 let mut sources = HashMap::new();
454 sources.insert(
455 "test.lemma".to_string(),
456 "spec test\nfact x: []".to_string(),
457 );
458
459 let result = plan_single(&spec, &specs, sources);
460 assert!(
461 result.is_err(),
462 "TypeDeclaration with empty base should fail planning"
463 );
464 let errors = result.unwrap_err();
465 let combined = errors
466 .iter()
467 .map(|e| e.to_string())
468 .collect::<Vec<_>>()
469 .join("\n");
470 assert!(
471 combined.contains("TypeDeclaration base cannot be empty"),
472 "Error should mention empty base; got: {}",
473 combined
474 );
475 }
476
477 #[test]
478 fn test_fact_binding_with_custom_type_resolves_in_correct_spec_context() {
479 let code = r#"
490spec one
491type money: number
492fact x: [money]
493
494spec two
495fact one: spec one
496fact one.x: 7
497rule getx: one.x
498"#;
499
500 let specs = parse(code, "test.lemma", &ResourceLimits::default()).unwrap();
501 let spec_two = specs.iter().find(|d| d.name == "two").unwrap();
502
503 let mut sources = HashMap::new();
504 sources.insert("test.lemma".to_string(), code.to_string());
505 let execution_plan =
506 plan_single(spec_two, &specs, sources).expect("planning should succeed");
507
508 let one_x_path = FactPath {
510 segments: vec![PathSegment {
511 fact: "one".to_string(),
512 spec: "one".to_string(),
513 }],
514 fact: "x".to_string(),
515 };
516
517 let one_x_type = execution_plan
518 .facts
519 .get(&one_x_path)
520 .and_then(|d| d.schema_type())
521 .expect("one.x should have a resolved type");
522
523 assert_eq!(
524 one_x_type.name(),
525 "money",
526 "one.x should have type 'money', got: {}",
527 one_x_type.name()
528 );
529 assert!(one_x_type.is_number(), "money should be number-based");
530 }
531
532 #[test]
533 fn test_plan_with_registry_style_spec_names() {
534 let source = r#"spec @user/workspace/somespec
535fact quantity: 10
536
537spec user/workspace/example
538fact inventory: spec @user/workspace/somespec
539rule total_quantity: inventory.quantity"#;
540
541 let specs = parse(source, "registry_bundle.lemma", &ResourceLimits::default()).unwrap();
542 assert_eq!(specs.len(), 2);
543
544 let example_spec = specs
545 .iter()
546 .find(|d| d.name == "user/workspace/example")
547 .expect("should find user/workspace/example");
548
549 let mut sources = HashMap::new();
550 sources.insert("registry_bundle.lemma".to_string(), source.to_string());
551
552 let result = plan_single(example_spec, &specs, sources);
553 assert!(
554 result.is_ok(),
555 "Planning with @... spec names should succeed: {:?}",
556 result.err()
557 );
558 }
559
560 #[test]
561 fn test_multiple_independent_errors_are_all_reported() {
562 let source = r#"spec demo
565type money from nonexistent_type_source
566fact helper: spec nonexistent_spec
567fact price: 10
568rule total: helper.value + price"#;
569
570 let specs = parse(source, "test.lemma", &ResourceLimits::default()).unwrap();
571
572 let mut sources = HashMap::new();
573 sources.insert("test.lemma".to_string(), source.to_string());
574
575 let result = plan_single(&specs[0], &specs, sources);
576 assert!(result.is_err(), "Planning should fail with multiple errors");
577
578 let errors = result.unwrap_err();
579 let all_messages: Vec<String> = errors.iter().map(|e| e.to_string()).collect();
580 let combined = all_messages.join("\n");
581
582 assert!(
584 combined.contains("Unknown type") && combined.contains("money"),
585 "Should report type import error for 'money'. Got:\n{}",
586 combined
587 );
588
589 assert!(
591 combined.contains("nonexistent_spec"),
592 "Should report spec reference error for 'nonexistent_spec'. Got:\n{}",
593 combined
594 );
595
596 assert!(
598 errors.len() >= 2,
599 "Expected at least 2 errors, got {}: {}",
600 errors.len(),
601 combined
602 );
603 }
604
605 #[test]
606 fn test_type_error_does_not_suppress_cross_spec_fact_error() {
607 let source = r#"spec demo
611type currency from missing_spec
612fact ext: spec also_missing
613rule val: ext.some_fact"#;
614
615 let specs = parse(source, "test.lemma", &ResourceLimits::default()).unwrap();
616
617 let mut sources = HashMap::new();
618 sources.insert("test.lemma".to_string(), source.to_string());
619
620 let result = plan_single(&specs[0], &specs, sources);
621 assert!(result.is_err());
622
623 let errors = result.unwrap_err();
624 let combined: String = errors
625 .iter()
626 .map(|e| e.to_string())
627 .collect::<Vec<_>>()
628 .join("\n");
629
630 assert!(
632 combined.contains("currency"),
633 "Should report type error about 'currency'. Got:\n{}",
634 combined
635 );
636
637 assert!(
639 combined.contains("also_missing"),
640 "Should report error about 'also_missing'. Got:\n{}",
641 combined
642 );
643 }
644}