1use crate::parsing::ast::Span;
2use crate::parsing::source::Source;
3use crate::planning::types::{ResolvedDocumentTypes, TypeRegistry};
4use crate::planning::validation::validate_type_specifications;
5use crate::semantic::{
6 standard_boolean, standard_duration, standard_number, standard_ratio, ArithmeticComputation,
7 ConversionTarget, Expression, ExpressionKind, FactPath, FactReference, FactValue, LemmaDoc,
8 LemmaFact, LemmaRule, LemmaType, LiteralValue, PathSegment, RulePath, TypeDef,
9 TypeSpecification,
10};
11use crate::LemmaError;
12use indexmap::IndexMap;
13use std::collections::{HashMap, HashSet, VecDeque};
14use std::sync::Arc;
15
16#[derive(Debug)]
17pub(crate) struct Graph {
18 facts: IndexMap<FactPath, LemmaFact>,
19 rules: IndexMap<RulePath, RuleNode>,
20 sources: HashMap<String, String>,
21 execution_order: Vec<RulePath>,
22 all_docs: HashMap<String, LemmaDoc>, resolved_types: HashMap<String, ResolvedDocumentTypes>,
24}
25
26impl Graph {
27 pub(crate) fn facts(&self) -> &IndexMap<FactPath, LemmaFact> {
28 &self.facts
29 }
30
31 pub(crate) fn rules(&self) -> &IndexMap<RulePath, RuleNode> {
32 &self.rules
33 }
34
35 pub(crate) fn rules_mut(&mut self) -> &mut IndexMap<RulePath, RuleNode> {
36 &mut self.rules
37 }
38
39 pub(crate) fn sources(&self) -> &HashMap<String, String> {
40 &self.sources
41 }
42
43 pub(crate) fn execution_order(&self) -> &[RulePath] {
44 &self.execution_order
45 }
46
47 pub(crate) fn all_docs(&self) -> &HashMap<String, LemmaDoc> {
48 &self.all_docs
49 }
50
51 pub(crate) fn resolved_types(&self) -> &HashMap<String, ResolvedDocumentTypes> {
52 &self.resolved_types
53 }
54
55 fn resolve_standard_type(name: &str) -> Option<TypeSpecification> {
57 match name {
58 "boolean" => Some(TypeSpecification::boolean()),
59 "scale" => Some(TypeSpecification::scale()),
60 "number" => Some(TypeSpecification::number()),
61 "ratio" => Some(TypeSpecification::ratio()),
62 "text" => Some(TypeSpecification::text()),
63 "date" => Some(TypeSpecification::date()),
64 "time" => Some(TypeSpecification::time()),
65 "duration" => Some(TypeSpecification::duration()),
66 "percent" => Some(TypeSpecification::ratio()),
67 _ => None,
68 }
69 }
70
71 pub(crate) fn resolve_type_declaration(
81 &self,
82 type_decl: &FactValue,
83 decl_source: &Source,
84 context_doc: &str,
85 ) -> Result<LemmaType, LemmaError> {
86 let FactValue::TypeDeclaration {
87 base,
88 overrides,
89 from,
90 } = type_decl
91 else {
92 unreachable!("BUG: resolve_type_declaration called with non-TypeDeclaration FactValue");
93 };
94
95 let source_doc = from.as_deref().unwrap_or(context_doc);
98
99 let base_lemma_type = if let Some(specs) = Self::resolve_standard_type(base) {
101 LemmaType::without_name(specs)
103 } else {
104 let document_types = self.resolved_types.get(source_doc).ok_or_else(|| {
106 LemmaError::engine(
107 format!("Resolved types not found for document '{}'", source_doc),
108 decl_source.span.clone(),
109 decl_source.attribute.clone(),
110 self.source_text_for(decl_source),
111 decl_source.doc_name.clone(),
112 self.doc_start_line_for(decl_source),
113 None::<String>,
114 )
115 })?;
116
117 document_types
118 .named_types
119 .get(base)
120 .ok_or_else(|| {
121 LemmaError::engine(
122 format!("Unknown type: '{}'. Type must be defined before use.", base),
123 decl_source.span.clone(),
124 decl_source.attribute.clone(),
125 self.source_text_for(decl_source),
126 decl_source.doc_name.clone(),
127 self.doc_start_line_for(decl_source),
128 None::<String>,
129 )
130 })?
131 .clone()
132 };
133
134 let mut specs = base_lemma_type.specifications;
136 if let Some(ref overrides_vec) = overrides {
137 for (command, args) in overrides_vec {
138 specs = specs.apply_override(command, args).map_err(|e| {
139 LemmaError::engine(
140 format!("Invalid command '{}' for type '{}': {}", command, base, e),
141 decl_source.span.clone(),
142 decl_source.attribute.clone(),
143 self.source_text_for(decl_source),
144 decl_source.doc_name.clone(),
145 self.doc_start_line_for(decl_source),
146 None::<String>,
147 )
148 })?;
149 }
150 }
151
152 let lemma_type = if let Some(name) = base_lemma_type.name {
155 LemmaType::new(name, specs)
156 } else {
157 LemmaType::without_name(specs)
158 };
159
160 Ok(lemma_type)
161 }
162
163 fn source_text_for(&self, source: &Source) -> Arc<str> {
164 let source_text = self.sources.get(&source.attribute).unwrap_or_else(|| {
165 unreachable!(
166 "BUG: missing sources entry for attribute '{}' (doc '{}')",
167 source.attribute, source.doc_name
168 )
169 });
170 Arc::from(source_text.as_str())
171 }
172
173 fn doc_start_line_for(&self, source: &Source) -> usize {
174 self.all_docs
175 .get(&source.doc_name)
176 .map(|d| d.start_line)
177 .unwrap_or_else(|| {
178 unreachable!(
179 "BUG: missing document '{}' while computing error doc_start_line",
180 source.doc_name
181 )
182 })
183 }
184
185 fn topological_sort(&self) -> Result<Vec<RulePath>, Vec<LemmaError>> {
186 let mut in_degree: HashMap<RulePath, usize> = HashMap::new();
187 let mut dependents: HashMap<RulePath, Vec<RulePath>> = HashMap::new();
188 let mut queue = VecDeque::new();
189 let mut result = Vec::new();
190
191 for rule_path in self.rules.keys() {
192 in_degree.insert(rule_path.clone(), 0);
193 dependents.insert(rule_path.clone(), Vec::new());
194 }
195
196 for (rule_path, rule_node) in &self.rules {
197 for dependency in &rule_node.depends_on_rules {
198 if self.rules.contains_key(dependency) {
199 if let Some(degree) = in_degree.get_mut(rule_path) {
200 *degree += 1;
201 }
202 if let Some(deps) = dependents.get_mut(dependency) {
203 deps.push(rule_path.clone());
204 }
205 }
206 }
207 }
208
209 for (rule_path, degree) in &in_degree {
210 if *degree == 0 {
211 queue.push_back(rule_path.clone());
212 }
213 }
214
215 while let Some(rule_path) = queue.pop_front() {
216 result.push(rule_path.clone());
217
218 if let Some(dependent_rules) = dependents.get(&rule_path) {
219 for dependent in dependent_rules {
220 if let Some(degree) = in_degree.get_mut(dependent) {
221 *degree -= 1;
222 if *degree == 0 {
223 queue.push_back(dependent.clone());
224 }
225 }
226 }
227 }
228 }
229
230 if result.len() != self.rules.len() {
231 let missing: Vec<RulePath> = self
232 .rules
233 .keys()
234 .filter(|rule| !result.contains(rule))
235 .cloned()
236 .collect();
237 let cycle: Vec<Source> = missing
238 .iter()
239 .filter_map(|rule| self.rules.get(rule).map(|n| n.source.clone()))
240 .collect();
241
242 let Some(first_source) = cycle.first() else {
243 unreachable!(
244 "BUG: circular dependency detected but no sources could be collected ({} missing rules)",
245 missing.len()
246 );
247 };
248
249 return Err(vec![LemmaError::circular_dependency(
250 format!(
251 "Circular dependency detected. Rules involved: {}",
252 missing
253 .iter()
254 .map(|rule| rule.rule.clone())
255 .collect::<Vec<_>>()
256 .join(", ")
257 ),
258 first_source.span.clone(),
259 first_source.attribute.clone(),
260 self.source_text_for(first_source),
261 first_source.doc_name.clone(),
262 self.doc_start_line_for(first_source),
263 cycle,
264 None::<String>,
265 )]);
266 }
267
268 Ok(result)
269 }
270}
271
272#[derive(Debug)]
273pub(crate) struct RuleNode {
274 pub branches: Vec<(Option<Expression>, Expression)>,
277 pub source: Source,
278
279 pub depends_on_rules: HashSet<RulePath>,
280
281 pub rule_type: LemmaType,
284}
285
286struct GraphBuilder<'a> {
287 facts: IndexMap<FactPath, LemmaFact>,
288 rules: IndexMap<RulePath, RuleNode>,
289 sources: HashMap<String, String>,
290 all_docs: HashMap<String, &'a LemmaDoc>,
291 resolved_types: HashMap<String, ResolvedDocumentTypes>,
292 errors: Vec<LemmaError>,
293}
294
295impl Graph {
296 pub(crate) fn build(
297 main_doc: &LemmaDoc,
298 all_docs: &[LemmaDoc],
299 sources: HashMap<String, String>,
300 ) -> Result<Graph, Vec<LemmaError>> {
301 let mut type_registry = TypeRegistry::new();
303 for doc in all_docs {
304 for type_def in &doc.types {
305 if let Err(e) = type_registry.register_type(&doc.name, type_def.clone()) {
306 return Err(vec![e]);
307 }
308 }
309 }
310
311 let mut builder = GraphBuilder {
312 facts: IndexMap::new(),
313 rules: IndexMap::new(),
314 sources,
315 all_docs: all_docs.iter().map(|doc| (doc.name.clone(), doc)).collect(),
316 resolved_types: HashMap::new(),
317 errors: Vec::new(),
318 };
319
320 for doc in all_docs {
328 match type_registry.resolve_named_types(&doc.name) {
329 Ok(document_types) => {
330 for (type_name, lemma_type) in &document_types.named_types {
332 let source = Source::new(
333 "<type>",
334 Span {
335 start: 0,
336 end: 0,
337 line: doc.start_line,
338 col: 0,
339 },
340 doc.name.clone(),
341 );
342 let mut spec_errors = validate_type_specifications(
343 &lemma_type.specifications,
344 type_name,
345 &source,
346 );
347 builder.errors.append(&mut spec_errors);
348 }
349 builder
350 .resolved_types
351 .insert(doc.name.clone(), document_types);
352 }
353 Err(e) => builder.errors.push(e),
354 }
355 }
356 if !builder.errors.is_empty() {
357 return Err(builder.errors);
358 }
359
360 builder.build_document(main_doc, Vec::new(), &mut type_registry)?;
361
362 if !builder.errors.is_empty() {
363 return Err(builder.errors);
364 }
365
366 let mut graph = Graph {
367 facts: builder.facts,
368 rules: builder.rules,
369 sources: builder.sources,
370 execution_order: Vec::new(),
371 all_docs: all_docs
372 .iter()
373 .map(|doc| (doc.name.clone(), doc.clone()))
374 .collect(),
375 resolved_types: builder.resolved_types,
376 };
377
378 graph.validate(all_docs)?;
380
381 Ok(graph)
382 }
383
384 fn validate(&mut self, all_docs: &[LemmaDoc]) -> Result<(), Vec<LemmaError>> {
385 let mut errors = Vec::new();
386
387 validate_document_interfaces(self, all_docs, &mut errors);
388 validate_all_rule_references_exist(self, &mut errors);
389 validate_fact_override_paths_target_document_facts(self, &mut errors);
390 validate_fact_and_rule_name_collisions(self, &mut errors);
391
392 let execution_order = match self.topological_sort() {
393 Ok(order) => order,
394 Err(circular_errors) => {
395 errors.extend(circular_errors);
396 Vec::new()
397 }
398 };
399
400 if errors.is_empty() {
401 compute_all_rule_types(self, &execution_order, &mut errors);
402 }
403
404 if !errors.is_empty() {
405 return Err(errors);
406 }
407
408 self.execution_order = execution_order;
409 Ok(())
410 }
411}
412
413impl<'a> GraphBuilder<'a> {
414 fn source_text_for(&self, source: &Source) -> Arc<str> {
415 let source_text = self.sources.get(&source.attribute).unwrap_or_else(|| {
416 unreachable!(
417 "BUG: missing sources entry for attribute '{}' (doc '{}')",
418 source.attribute, source.doc_name
419 )
420 });
421 Arc::from(source_text.as_str())
422 }
423
424 fn doc_start_line_for(&self, source: &Source) -> usize {
425 self.all_docs
426 .get(&source.doc_name)
427 .map(|d| d.start_line)
428 .unwrap_or_else(|| {
429 unreachable!(
430 "BUG: missing document '{}' while computing error doc_start_line",
431 source.doc_name
432 )
433 })
434 }
435
436 fn engine_error(&self, message: impl Into<String>, source: &Source) -> LemmaError {
437 LemmaError::engine(
438 message.into(),
439 source.span.clone(),
440 source.attribute.clone(),
441 self.source_text_for(source),
442 source.doc_name.clone(),
443 self.doc_start_line_for(source),
444 None::<String>,
445 )
446 }
447
448 fn build_document(
449 &mut self,
450 doc: &'a LemmaDoc,
451 current_segments: Vec<PathSegment>,
452 type_registry: &mut TypeRegistry,
453 ) -> Result<(), Vec<LemmaError>> {
454 self.build_document_with_overrides(doc, current_segments, HashMap::new(), type_registry)
455 }
456
457 fn resolve_path_segments_with_overrides(
458 &mut self,
459 segments: &[String],
460 reference_source: &Source,
461 mut current_facts_map: HashMap<String, &'a LemmaFact>,
462 mut path_segments: Vec<PathSegment>,
463 effective_doc_refs: &HashMap<String, String>,
464 ) -> Option<Vec<PathSegment>> {
465 for (index, segment) in segments.iter().enumerate() {
466 let fact_ref =
467 match current_facts_map.get(segment) {
468 Some(f) => f,
469 None => {
470 self.errors.push(self.engine_error(
471 format!("Fact '{}' not found", segment),
472 reference_source,
473 ));
474 return None;
475 }
476 };
477
478 if let FactValue::DocumentReference(original_doc_name) = &fact_ref.value {
479 let doc_name = if index == 0 {
482 effective_doc_refs.get(segment).unwrap_or(original_doc_name)
483 } else {
484 original_doc_name
485 };
486
487 let next_doc = match self.all_docs.get(doc_name) {
488 Some(d) => d,
489 None => {
490 self.errors.push(self.engine_error(
491 format!("Document '{}' not found", doc_name),
492 reference_source,
493 ));
494 return None;
495 }
496 };
497 path_segments.push(PathSegment {
498 fact: segment.clone(),
499 doc: doc_name.clone(),
500 });
501 current_facts_map = next_doc
502 .facts
503 .iter()
504 .map(|f| (f.reference.fact.clone(), f))
505 .collect();
506 } else {
507 self.errors.push(self.engine_error(
508 format!("Fact '{}' is not a document reference", segment),
509 reference_source,
510 ));
511 return None;
512 }
513 }
514 Some(path_segments)
515 }
516
517 fn add_fact_with_overrides(
518 &mut self,
519 fact: &'a LemmaFact,
520 current_segments: &[PathSegment],
521 pending_overrides: &HashMap<String, Vec<(&'a LemmaFact, usize)>>,
522 current_doc: &'a LemmaDoc,
523 type_registry: &mut TypeRegistry,
524 ) {
525 if !fact.reference.segments.is_empty() {
529 return;
530 }
531
532 let fact_path = FactPath {
533 segments: current_segments.to_vec(),
534 fact: fact.reference.fact.clone(),
535 };
536
537 if self.facts.contains_key(&fact_path) {
539 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
540 unreachable!(
541 "BUG: fact '{}' missing source_location",
542 fact.reference.fact
543 )
544 });
545 self.errors.push(
546 self.engine_error(format!("Duplicate fact '{}'", fact_path.fact), fact_source),
547 );
548 return;
549 }
550
551 let current_depth = current_segments.len();
552
553 match &fact.value {
554 FactValue::Literal(_) => {
555 let effective_value = if let Some(overrides) =
557 pending_overrides.get(&fact.reference.fact)
558 {
559 if let Some((override_fact, _)) = overrides.iter().find(|(o, entry_depth)| {
562 *entry_depth + o.reference.segments.len() == current_depth
563 && o.reference.fact == fact.reference.fact
564 }) {
565 override_fact.value.clone()
566 } else {
567 fact.value.clone()
568 }
569 } else {
570 fact.value.clone()
571 };
572
573 let stored_fact = LemmaFact {
574 reference: fact.reference.clone(),
575 value: effective_value,
576 source_location: fact.source_location.clone(),
577 };
578 self.facts.insert(fact_path, stored_fact);
579 }
580 FactValue::TypeDeclaration {
581 base,
582 overrides: inline_overrides,
583 from,
584 } => {
585 let is_inline_type_definition = from.is_some() || inline_overrides.is_some();
588
589 if is_inline_type_definition && current_segments.is_empty() {
593 let source_location = fact.source_location.clone().unwrap_or_else(|| {
596 unreachable!(
597 "BUG: inline type definition fact '{}' missing source_location",
598 fact.reference.fact
599 )
600 });
601 let inline_type_def = TypeDef::Inline {
602 source_location,
603 parent: base.clone(),
604 overrides: inline_overrides.clone(),
605 fact_ref: fact.reference.clone(),
606 from: from.clone(),
607 };
608
609 let doc_name = current_doc.name.clone();
611
612 if let Err(e) = type_registry.register_type(&doc_name, inline_type_def) {
614 self.errors.push(e);
615 }
616 }
617
618 let effective_value = if let Some(overrides) =
620 pending_overrides.get(&fact.reference.fact)
621 {
622 if let Some((override_fact, _)) = overrides.iter().find(|(o, entry_depth)| {
625 *entry_depth + o.reference.segments.len() == current_depth
626 && o.reference.fact == fact.reference.fact
627 }) {
628 override_fact.value.clone()
629 } else {
630 fact.value.clone()
631 }
632 } else {
633 fact.value.clone()
634 };
635
636 let stored_fact = LemmaFact {
637 reference: fact.reference.clone(),
638 value: effective_value,
639 source_location: fact.source_location.clone(),
640 };
641 self.facts.insert(fact_path, stored_fact);
642 }
643 FactValue::DocumentReference(doc_name) => {
644 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
645 unreachable!(
646 "BUG: document reference fact '{}' missing source_location",
647 fact.reference.fact
648 )
649 });
650
651 let (effective_doc_name, effective_source) = if let Some(overrides) =
653 pending_overrides.get(&fact.reference.fact)
654 {
655 if let Some((override_fact, _)) = overrides.iter().find(|(o, entry_depth)| {
657 *entry_depth + o.reference.segments.len() == current_depth
658 && o.reference.fact == fact.reference.fact
659 }) {
660 if let FactValue::DocumentReference(override_doc) = &override_fact.value {
661 let override_source =
662 override_fact.source_location.as_ref().unwrap_or_else(|| {
663 unreachable!(
664 "BUG: override fact '{}' missing source_location",
665 override_fact.reference.fact
666 )
667 });
668 (override_doc.clone(), override_source)
669 } else {
670 (doc_name.clone(), fact_source)
671 }
672 } else {
673 (doc_name.clone(), fact_source)
674 }
675 } else {
676 (doc_name.clone(), fact_source)
677 };
678
679 let nested_doc = match self.all_docs.get(&effective_doc_name) {
680 Some(d) => d,
681 None => {
682 self.errors.push(self.engine_error(
683 format!("Document '{}' not found", effective_doc_name),
684 effective_source,
685 ));
686 return;
687 }
688 };
689
690 let stored_fact = LemmaFact {
692 reference: fact.reference.clone(),
693 value: FactValue::DocumentReference(effective_doc_name.clone()),
694 source_location: fact.source_location.clone(),
695 };
696 self.facts.insert(fact_path.clone(), stored_fact);
697
698 let nested_overrides: HashMap<String, Vec<(&LemmaFact, usize)>> = pending_overrides
702 .get(&fact.reference.fact)
703 .map(|overrides| {
704 let mut nested: HashMap<String, Vec<(&LemmaFact, usize)>> = HashMap::new();
705 for (o, entry_depth) in overrides {
706 let traversed = current_depth - entry_depth;
708 let next_index = traversed + 1;
709 let key = if o.reference.segments.len() > next_index {
710 o.reference.segments[next_index].clone()
711 } else {
712 o.reference.fact.clone()
713 };
714 nested.entry(key).or_default().push((*o, *entry_depth));
715 }
716 nested
717 })
718 .unwrap_or_default();
719
720 let mut nested_segments = current_segments.to_vec();
722 nested_segments.push(PathSegment {
723 fact: fact.reference.fact.clone(),
724 doc: effective_doc_name.clone(),
725 });
726
727 if let Err(errs) = self.build_document_with_overrides(
728 nested_doc,
729 nested_segments,
730 nested_overrides,
731 type_registry,
732 ) {
733 self.errors.extend(errs);
734 }
735 }
736 }
737 }
738
739 fn build_document_with_overrides(
740 &mut self,
741 doc: &'a LemmaDoc,
742 current_segments: Vec<PathSegment>,
743 override_map: HashMap<String, Vec<(&'a LemmaFact, usize)>>,
744 type_registry: &mut TypeRegistry,
745 ) -> Result<(), Vec<LemmaError>> {
746 let current_depth = current_segments.len();
749 let mut pending_overrides = override_map;
750 for fact in &doc.facts {
751 if !fact.reference.segments.is_empty() {
752 let first_segment = &fact.reference.segments[0];
753 pending_overrides
754 .entry(first_segment.clone())
755 .or_default()
756 .push((fact, current_depth));
757 }
758 }
759
760 let mut effective_doc_refs: HashMap<String, String> = HashMap::new();
763 for fact in doc.facts.iter() {
764 if fact.reference.segments.is_empty() {
765 if let FactValue::DocumentReference(doc_name) = &fact.value {
766 let effective_doc = if let Some(overrides) =
769 pending_overrides.get(&fact.reference.fact)
770 {
771 if let Some((override_fact, _)) =
772 overrides.iter().find(|(o, entry_depth)| {
773 *entry_depth + o.reference.segments.len() == current_depth
774 && o.reference.fact == fact.reference.fact
775 })
776 {
777 if let FactValue::DocumentReference(override_doc) = &override_fact.value
778 {
779 override_doc.clone()
780 } else {
781 doc_name.clone()
782 }
783 } else {
784 doc_name.clone()
785 }
786 } else {
787 doc_name.clone()
788 };
789 effective_doc_refs.insert(fact.reference.fact.clone(), effective_doc);
790 }
791 }
792 }
793
794 let facts_map: HashMap<String, &LemmaFact> = doc
796 .facts
797 .iter()
798 .map(|fact| (fact.reference.fact.clone(), fact))
799 .collect();
800
801 for fact in &doc.facts {
802 self.add_fact_with_overrides(
803 fact,
804 ¤t_segments,
805 &pending_overrides,
806 doc,
807 type_registry,
808 );
809 }
810
811 match type_registry.resolve_types(&doc.name) {
813 Ok(document_types) => {
814 for (fact_ref, lemma_type) in &document_types.inline_type_definitions {
816 let type_name = format!("{} (inline)", fact_ref.fact);
817 let fact = doc.facts.iter().find(|f| &f.reference == fact_ref).unwrap_or_else(|| {
818 unreachable!(
819 "BUG: inline type definition for '{}' has no corresponding fact in document '{}'",
820 fact_ref.fact,
821 doc.name
822 )
823 });
824 let source = fact.source_location.as_ref().unwrap_or_else(|| {
825 unreachable!(
826 "BUG: inline type definition fact '{}' missing source_location",
827 fact_ref.fact
828 )
829 });
830 let mut spec_errors = validate_type_specifications(
831 &lemma_type.specifications,
832 &type_name,
833 source,
834 );
835 self.errors.append(&mut spec_errors);
836 }
837 self.resolved_types.insert(doc.name.clone(), document_types);
839 }
840 Err(e) => {
841 self.errors.push(e);
842 return Err(self.errors.clone());
843 }
844 }
845
846 for rule in &doc.rules {
848 self.add_rule(
849 rule,
850 doc,
851 &facts_map,
852 ¤t_segments,
853 &effective_doc_refs,
854 );
855 }
856
857 Ok(())
858 }
859
860 fn add_rule(
861 &mut self,
862 rule: &LemmaRule,
863 current_doc: &'a LemmaDoc,
864 facts_map: &HashMap<String, &'a LemmaFact>,
865 current_segments: &[PathSegment],
866 effective_doc_refs: &HashMap<String, String>,
867 ) {
868 let rule_path = RulePath {
869 segments: current_segments.to_vec(),
870 rule: rule.name.clone(),
871 };
872
873 if self.rules.contains_key(&rule_path) {
874 let rule_source = rule.source_location.as_ref().unwrap_or_else(|| {
875 unreachable!("BUG: rule '{}' missing source_location", rule.name)
876 });
877 self.errors.push(
878 self.engine_error(format!("Duplicate rule '{}'", rule_path.rule), rule_source),
879 );
880 return;
881 }
882
883 let mut branches = Vec::new();
884 let mut depends_on_rules = HashSet::new();
885
886 let converted_expression = match self.convert_expression_and_extract_dependencies(
887 &rule.expression,
888 current_doc,
889 facts_map,
890 current_segments,
891 &mut depends_on_rules,
892 effective_doc_refs,
893 ) {
894 Some(expr) => expr,
895 None => return,
896 };
897 branches.push((None, converted_expression));
898
899 for unless_clause in &rule.unless_clauses {
900 let converted_condition = match self.convert_expression_and_extract_dependencies(
901 &unless_clause.condition,
902 current_doc,
903 facts_map,
904 current_segments,
905 &mut depends_on_rules,
906 effective_doc_refs,
907 ) {
908 Some(expr) => expr,
909 None => return,
910 };
911 let converted_result = match self.convert_expression_and_extract_dependencies(
912 &unless_clause.result,
913 current_doc,
914 facts_map,
915 current_segments,
916 &mut depends_on_rules,
917 effective_doc_refs,
918 ) {
919 Some(expr) => expr,
920 None => return,
921 };
922 branches.push((Some(converted_condition), converted_result));
923 }
924
925 let rule_node = RuleNode {
926 branches,
927 source: rule.source_location.clone().unwrap_or_else(|| {
928 unreachable!("BUG: rule '{}' missing source_location", rule.name)
929 }),
930 depends_on_rules,
931 rule_type: LemmaType::veto_type(), };
933
934 self.rules.insert(rule_path, rule_node);
935 }
936
937 #[allow(clippy::too_many_arguments)]
938 fn convert_binary_operands(
939 &mut self,
940 left: &Expression,
941 right: &Expression,
942 current_doc: &'a LemmaDoc,
943 facts_map: &HashMap<String, &'a LemmaFact>,
944 current_segments: &[PathSegment],
945 depends_on_rules: &mut HashSet<RulePath>,
946 effective_doc_refs: &HashMap<String, String>,
947 ) -> Option<(Expression, Expression)> {
948 let converted_left = self.convert_expression_and_extract_dependencies(
949 left,
950 current_doc,
951 facts_map,
952 current_segments,
953 depends_on_rules,
954 effective_doc_refs,
955 )?;
956 let converted_right = self.convert_expression_and_extract_dependencies(
957 right,
958 current_doc,
959 facts_map,
960 current_segments,
961 depends_on_rules,
962 effective_doc_refs,
963 )?;
964 Some((converted_left, converted_right))
965 }
966
967 fn convert_expression_and_extract_dependencies(
968 &mut self,
969 expr: &Expression,
970 current_doc: &'a LemmaDoc,
971 facts_map: &HashMap<String, &'a LemmaFact>,
972 current_segments: &[PathSegment],
973 depends_on_rules: &mut HashSet<RulePath>,
974 effective_doc_refs: &HashMap<String, String>,
975 ) -> Option<Expression> {
976 match &expr.kind {
977 ExpressionKind::Reference(r) => {
978 let fact_ref_expr = Expression {
980 kind: ExpressionKind::FactReference(r.to_fact_reference()),
981 source_location: expr.source_location.clone(),
982 };
983 self.convert_expression_and_extract_dependencies(
984 &fact_ref_expr,
985 current_doc,
986 facts_map,
987 current_segments,
988 depends_on_rules,
989 effective_doc_refs,
990 )
991 }
992 ExpressionKind::UnresolvedUnitLiteral(number, unit_name) => {
993 let expr_source = expr.source_location.as_ref().unwrap_or_else(|| {
994 unreachable!(
995 "BUG: UnresolvedUnitLiteral expression missing source_location for unit '{}'",
996 unit_name
997 )
998 });
999
1000 let document_types = self.resolved_types.get(¤t_doc.name).unwrap_or_else(|| {
1004 unreachable!(
1005 "Internal error: resolved types not found for document '{}' - types should have been resolved before processing rules (even empty documents have resolved types with empty maps)",
1006 current_doc.name
1007 )
1008 });
1009
1010 let lemma_type = match document_types.unit_index.get(unit_name) {
1012 Some(lemma_type) => lemma_type.clone(),
1013 None => {
1014 self.errors.push(self.engine_error(
1015 format!(
1016 "Unknown unit '{}' in document '{}'",
1017 unit_name, current_doc.name
1018 ),
1019 expr_source,
1020 ));
1021 return None;
1022 }
1023 };
1024
1025 match &lemma_type.specifications {
1026 TypeSpecification::Scale { units, .. } => {
1027 if units
1028 .iter()
1029 .all(|unit| !unit.name.eq_ignore_ascii_case(unit_name))
1030 {
1031 unreachable!(
1032 "Internal error: unit_index returned type '{}' that doesn't have unit '{}'",
1033 lemma_type.name.as_ref().unwrap_or(&"<inline>".to_string()),
1034 unit_name
1035 );
1036 }
1037
1038 let literal_value = LiteralValue::scale_with_type(
1039 *number,
1040 Some(unit_name.clone()), lemma_type.clone(),
1042 );
1043 Some(Expression {
1044 kind: ExpressionKind::Literal(literal_value),
1045 source_location: expr.source_location.clone(),
1046 })
1047 }
1048 TypeSpecification::Ratio { units, .. } => {
1049 if units
1050 .iter()
1051 .all(|unit| !unit.name.eq_ignore_ascii_case(unit_name))
1052 {
1053 unreachable!(
1054 "Internal error: unit_index returned type '{}' that doesn't have unit '{}'",
1055 lemma_type.name.as_ref().unwrap_or(&"<inline>".to_string()),
1056 unit_name
1057 );
1058 }
1059
1060 let literal_value = LiteralValue::ratio_with_type(
1061 *number,
1062 Some(unit_name.clone()), lemma_type.clone(),
1064 );
1065 Some(Expression {
1066 kind: ExpressionKind::Literal(literal_value),
1067 source_location: expr.source_location.clone(),
1068 })
1069 }
1070 _ => {
1071 unreachable!(
1072 "Internal error: unit_index returned non-Number/Ratio type '{}' for unit '{}'",
1073 lemma_type.name.as_ref().unwrap_or(&"<inline>".to_string()),
1074 unit_name
1075 );
1076 }
1077 }
1078 }
1079 ExpressionKind::FactReference(fact_ref) => {
1080 let expr_source = expr.source_location.as_ref().unwrap_or_else(|| {
1081 unreachable!(
1082 "BUG: FactReference expression missing source_location for '{}'",
1083 fact_ref.fact
1084 )
1085 });
1086 let segments = self.resolve_path_segments_with_overrides(
1087 &fact_ref.segments,
1088 expr_source,
1089 facts_map.clone(),
1090 current_segments.to_vec(),
1091 effective_doc_refs,
1092 )?;
1093
1094 if fact_ref.segments.is_empty() && !facts_map.contains_key(&fact_ref.fact) {
1098 let is_rule = current_doc.rules.iter().any(|r| r.name == fact_ref.fact);
1100 if is_rule {
1101 self.errors.push(self.engine_error(
1102 format!(
1103 "'{}' is a rule, not a fact. Use '{}?' to reference rules",
1104 fact_ref.fact, fact_ref.fact
1105 ),
1106 expr_source,
1107 ));
1108 } else {
1109 self.errors.push(self.engine_error(
1110 format!("Fact '{}' not found", fact_ref.fact),
1111 expr_source,
1112 ));
1113 }
1114 return None;
1115 }
1116
1117 let fact_path = FactPath {
1118 segments,
1119 fact: fact_ref.fact.clone(),
1120 };
1121
1122 Some(Expression {
1123 kind: ExpressionKind::FactPath(fact_path),
1124 source_location: expr.source_location.clone(),
1125 })
1126 }
1127
1128 ExpressionKind::RuleReference(rule_ref) => {
1129 let expr_source = expr.source_location.as_ref().unwrap_or_else(|| {
1130 unreachable!(
1131 "BUG: RuleReference expression missing source_location for '{}?'",
1132 rule_ref.rule
1133 )
1134 });
1135 let segments = self.resolve_path_segments_with_overrides(
1136 &rule_ref.segments,
1137 expr_source,
1138 facts_map.clone(),
1139 current_segments.to_vec(),
1140 effective_doc_refs,
1141 )?;
1142
1143 let rule_path = RulePath {
1144 segments,
1145 rule: rule_ref.rule.clone(),
1146 };
1147
1148 depends_on_rules.insert(rule_path.clone());
1149
1150 Some(Expression {
1151 kind: ExpressionKind::RulePath(rule_path),
1152 source_location: expr.source_location.clone(),
1153 })
1154 }
1155
1156 ExpressionKind::LogicalAnd(left, right) => {
1157 let (l, r) = self.convert_binary_operands(
1158 left,
1159 right,
1160 current_doc,
1161 facts_map,
1162 current_segments,
1163 depends_on_rules,
1164 effective_doc_refs,
1165 )?;
1166 Some(Expression {
1167 kind: ExpressionKind::LogicalAnd(Arc::new(l), Arc::new(r)),
1168 source_location: expr.source_location.clone(),
1169 })
1170 }
1171
1172 ExpressionKind::LogicalOr(left, right) => {
1173 let (l, r) = self.convert_binary_operands(
1174 left,
1175 right,
1176 current_doc,
1177 facts_map,
1178 current_segments,
1179 depends_on_rules,
1180 effective_doc_refs,
1181 )?;
1182 Some(Expression {
1183 kind: ExpressionKind::LogicalOr(Arc::new(l), Arc::new(r)),
1184 source_location: expr.source_location.clone(),
1185 })
1186 }
1187
1188 ExpressionKind::Arithmetic(left, op, right) => {
1189 let (l, r) = self.convert_binary_operands(
1190 left,
1191 right,
1192 current_doc,
1193 facts_map,
1194 current_segments,
1195 depends_on_rules,
1196 effective_doc_refs,
1197 )?;
1198 Some(Expression {
1199 kind: ExpressionKind::Arithmetic(Arc::new(l), op.clone(), Arc::new(r)),
1200 source_location: expr.source_location.clone(),
1201 })
1202 }
1203
1204 ExpressionKind::Comparison(left, op, right) => {
1205 let (l, r) = self.convert_binary_operands(
1206 left,
1207 right,
1208 current_doc,
1209 facts_map,
1210 current_segments,
1211 depends_on_rules,
1212 effective_doc_refs,
1213 )?;
1214 Some(Expression {
1215 kind: ExpressionKind::Comparison(Arc::new(l), op.clone(), Arc::new(r)),
1216 source_location: expr.source_location.clone(),
1217 })
1218 }
1219
1220 ExpressionKind::UnitConversion(value, target) => {
1221 let converted_value = self.convert_expression_and_extract_dependencies(
1222 value,
1223 current_doc,
1224 facts_map,
1225 current_segments,
1226 depends_on_rules,
1227 effective_doc_refs,
1228 )?;
1229
1230 Some(Expression {
1231 kind: ExpressionKind::UnitConversion(Arc::new(converted_value), target.clone()),
1232 source_location: expr.source_location.clone(),
1233 })
1234 }
1235
1236 ExpressionKind::LogicalNegation(operand, neg_type) => {
1237 let converted_operand = self.convert_expression_and_extract_dependencies(
1238 operand,
1239 current_doc,
1240 facts_map,
1241 current_segments,
1242 depends_on_rules,
1243 effective_doc_refs,
1244 )?;
1245 Some(Expression {
1246 kind: ExpressionKind::LogicalNegation(
1247 Arc::new(converted_operand),
1248 neg_type.clone(),
1249 ),
1250 source_location: expr.source_location.clone(),
1251 })
1252 }
1253
1254 ExpressionKind::MathematicalComputation(op, operand) => {
1255 let converted_operand = self.convert_expression_and_extract_dependencies(
1256 operand,
1257 current_doc,
1258 facts_map,
1259 current_segments,
1260 depends_on_rules,
1261 effective_doc_refs,
1262 )?;
1263 Some(Expression {
1264 kind: ExpressionKind::MathematicalComputation(
1265 op.clone(),
1266 Arc::new(converted_operand),
1267 ),
1268 source_location: expr.source_location.clone(),
1269 })
1270 }
1271
1272 ExpressionKind::FactPath(_) => Some(expr.clone()),
1273 ExpressionKind::RulePath(rule_path) => {
1274 depends_on_rules.insert(rule_path.clone());
1275 Some(expr.clone())
1276 }
1277
1278 ExpressionKind::Literal(_) => Some(expr.clone()),
1279
1280 ExpressionKind::Veto(_) => Some(expr.clone()),
1281 }
1282 }
1283}
1284
1285fn compute_all_rule_types(
1286 graph: &mut Graph,
1287 execution_order: &[RulePath],
1288 errors: &mut Vec<LemmaError>,
1289) {
1290 let mut computed_types: HashMap<RulePath, LemmaType> = HashMap::new();
1291
1292 for rule_path in execution_order {
1293 let branches = {
1294 let rule_node = match graph.rules().get(rule_path) {
1295 Some(node) => node,
1296 None => continue,
1297 };
1298 rule_node.branches.clone()
1299 };
1300
1301 if branches.is_empty() {
1302 continue;
1303 }
1304
1305 let (_, default_result) = &branches[0];
1306 let default_type = compute_expression_type(default_result, graph, &computed_types, errors);
1307
1308 let mut non_veto_type: Option<LemmaType> = None;
1312 if !default_type.is_veto() {
1313 non_veto_type = Some(default_type.clone());
1314 }
1315
1316 for (branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
1317 if let Some(condition_expression) = condition {
1318 let condition_type =
1319 compute_expression_type(condition_expression, graph, &computed_types, errors);
1320 if !condition_type.is_boolean() {
1321 let condition_source = condition_expression
1322 .source_location
1323 .as_ref()
1324 .unwrap_or_else(|| {
1325 unreachable!(
1326 "BUG: unless clause condition in rule '{}' missing source_location",
1327 rule_path.rule
1328 )
1329 });
1330 errors.push(LemmaError::engine(
1331 format!(
1332 "Unless clause condition in rule '{}' must be boolean, got {:?}",
1333 rule_path.rule, condition_type
1334 ),
1335 condition_source.span.clone(),
1336 condition_source.attribute.clone(),
1337 graph.source_text_for(condition_source),
1338 condition_source.doc_name.clone(),
1339 graph.doc_start_line_for(condition_source),
1340 None::<String>,
1341 ));
1342 }
1343 }
1344
1345 let result_type = compute_expression_type(result, graph, &computed_types, errors);
1346 if !result_type.is_veto() {
1347 if non_veto_type.is_none() {
1350 non_veto_type = Some(result_type.clone());
1351 } else if let Some(ref existing_type) = non_veto_type {
1352 if !existing_type.has_same_base_type(&result_type) {
1354 let Some(rule_node) = graph.rules().get(rule_path) else {
1355 unreachable!(
1356 "BUG: rule type validation referenced missing rule '{}'",
1357 rule_path.rule
1358 );
1359 };
1360 let rule_source = &rule_node.source;
1361 let default_expr = &branches[0].1;
1362
1363 let mut location_parts = vec![format!(
1364 "{}:{}:{}",
1365 rule_source.attribute, rule_source.span.line, rule_source.span.col
1366 )];
1367
1368 if let Some(loc) = &default_expr.source_location {
1369 location_parts.push(format!(
1370 "default branch at {}:{}:{}",
1371 loc.attribute, loc.span.line, loc.span.col
1372 ));
1373 }
1374 if let Some(loc) = &result.source_location {
1375 location_parts.push(format!(
1376 "unless clause {} at {}:{}:{}",
1377 branch_index, loc.attribute, loc.span.line, loc.span.col
1378 ));
1379 }
1380
1381 errors.push(LemmaError::semantic(
1382 format!("Type mismatch in rule '{}' in document '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same standard type.",
1383 rule_path.rule,
1384 rule_source.doc_name,
1385 location_parts.join(", "),
1386 existing_type.name(),
1387 branch_index,
1388 result_type.name()),
1389 rule_source.span.clone(),
1390 rule_source.attribute.clone(),
1391 graph.source_text_for(rule_source),
1392 rule_source.doc_name.clone(),
1393 graph.doc_start_line_for(rule_source),
1394 None::<String>,
1395 ));
1396 }
1397 }
1398 }
1399
1400 if !default_type.has_same_base_type(&result_type)
1401 && !default_type.is_veto()
1402 && !result_type.is_veto()
1403 {
1404 let Some(rule_node) = graph.rules().get(rule_path) else {
1405 unreachable!(
1406 "BUG: rule type validation referenced missing rule '{}'",
1407 rule_path.rule
1408 );
1409 };
1410 let rule_source = &rule_node.source;
1411 let default_expr = &branches[0].1;
1412
1413 let mut location_parts = vec![format!(
1414 "{}:{}:{}",
1415 rule_source.attribute, rule_source.span.line, rule_source.span.col
1416 )];
1417
1418 if let Some(loc) = &default_expr.source_location {
1419 location_parts.push(format!(
1420 "default branch at {}:{}:{}",
1421 loc.attribute, loc.span.line, loc.span.col
1422 ));
1423 }
1424 if let Some(loc) = &result.source_location {
1425 location_parts.push(format!(
1426 "unless clause {} at {}:{}:{}",
1427 branch_index, loc.attribute, loc.span.line, loc.span.col
1428 ));
1429 }
1430
1431 errors.push(LemmaError::semantic(
1432 format!("Type mismatch in rule '{}' in document '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same standard type.",
1433 rule_path.rule,
1434 rule_source.doc_name,
1435 location_parts.join(", "),
1436 default_type.name(),
1437 branch_index,
1438 result_type.name()),
1439 rule_source.span.clone(),
1440 rule_source.attribute.clone(),
1441 graph.source_text_for(rule_source),
1442 rule_source.doc_name.clone(),
1443 graph.doc_start_line_for(rule_source),
1444 None::<String>,
1445 ));
1446 }
1447 }
1448
1449 let rule_type = non_veto_type.unwrap_or_else(LemmaType::veto_type);
1454 computed_types.insert(rule_path.clone(), rule_type);
1455 }
1456
1457 for (rule_path, rule_type) in computed_types {
1458 if let Some(rule_node) = graph.rules_mut().get_mut(&rule_path) {
1459 rule_node.rule_type = rule_type;
1460 }
1461 }
1462}
1463
1464fn compute_expression_type(
1465 expression: &Expression,
1466 graph: &Graph,
1467 computed_rule_types: &HashMap<RulePath, LemmaType>,
1468 errors: &mut Vec<LemmaError>,
1469) -> LemmaType {
1470 match &expression.kind {
1471 ExpressionKind::Literal(literal_value) => literal_value.get_type().clone(),
1472 ExpressionKind::FactPath(fact_path) => {
1473 let expr_source = expression.source_location.as_ref().unwrap_or_else(|| {
1474 unreachable!("BUG: fact path expression missing source_location")
1475 });
1476 compute_fact_type(fact_path, graph, expr_source, errors)
1477 }
1478 ExpressionKind::RulePath(rule_path) => computed_rule_types
1479 .get(rule_path)
1480 .cloned()
1481 .unwrap_or_else(|| {
1482 unreachable!(
1483 "BUG: Rule '{}' referenced before its type was computed (topological ordering)",
1484 rule_path.rule
1485 )
1486 }),
1487 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
1488 let expr_source = expression
1489 .source_location
1490 .as_ref()
1491 .unwrap_or_else(|| unreachable!("BUG: logical expression missing source_location"));
1492 let left_type = compute_expression_type(left, graph, computed_rule_types, errors);
1493 let right_type = compute_expression_type(right, graph, computed_rule_types, errors);
1494 validate_logical_operands(&left_type, &right_type, graph, expr_source, errors);
1495 standard_boolean().clone()
1496 }
1497 ExpressionKind::LogicalNegation(operand, _) => {
1498 let expr_source = expression.source_location.as_ref().unwrap_or_else(|| {
1499 unreachable!("BUG: logical negation expression missing source_location")
1500 });
1501 let operand_type = compute_expression_type(operand, graph, computed_rule_types, errors);
1502 validate_logical_operand(&operand_type, graph, expr_source, errors);
1503 standard_boolean().clone()
1504 }
1505 ExpressionKind::Comparison(left, op, right) => {
1506 let expr_source = expression.source_location.as_ref().unwrap_or_else(|| {
1507 unreachable!("BUG: comparison expression missing source_location")
1508 });
1509 let left_type = compute_expression_type(left, graph, computed_rule_types, errors);
1510 let right_type = compute_expression_type(right, graph, computed_rule_types, errors);
1511 validate_comparison_types(&left_type, op, &right_type, graph, expr_source, errors);
1512 standard_boolean().clone()
1513 }
1514 ExpressionKind::Arithmetic(left, operator, right) => {
1515 let expr_source = expression.source_location.as_ref().unwrap_or_else(|| {
1516 unreachable!("BUG: arithmetic expression missing source_location")
1517 });
1518 let left_type = compute_expression_type(left, graph, computed_rule_types, errors);
1519 let right_type = compute_expression_type(right, graph, computed_rule_types, errors);
1520 validate_arithmetic_types(
1521 &left_type,
1522 &right_type,
1523 operator,
1524 graph,
1525 expr_source,
1526 errors,
1527 );
1528 compute_arithmetic_result_type(left_type, right_type, operator)
1529 }
1530 ExpressionKind::UnitConversion(source_expression, target) => {
1531 let expr_source = expression.source_location.as_ref().unwrap_or_else(|| {
1532 unreachable!("BUG: unit conversion expression missing source_location")
1533 });
1534 let source_type =
1535 compute_expression_type(source_expression, graph, computed_rule_types, errors);
1536 validate_unit_conversion_types(&source_type, target, graph, expr_source, errors);
1537 match target {
1538 ConversionTarget::Duration(_) => standard_duration().clone(),
1539 ConversionTarget::Percentage => standard_ratio().clone(),
1540 ConversionTarget::ScaleUnit(_) => source_type,
1541 }
1542 }
1543 ExpressionKind::MathematicalComputation(_, operand) => {
1544 let expr_source = expression.source_location.as_ref().unwrap_or_else(|| {
1545 unreachable!("BUG: mathematical computation expression missing source_location")
1546 });
1547 let operand_type = compute_expression_type(operand, graph, computed_rule_types, errors);
1548 validate_mathematical_operand(&operand_type, graph, expr_source, errors);
1549 standard_number().clone()
1550 }
1551 ExpressionKind::Veto(_) => LemmaType::veto_type(),
1552 ExpressionKind::Reference(_)
1553 | ExpressionKind::FactReference(_)
1554 | ExpressionKind::RuleReference(_) => {
1555 unreachable!("Internal error: Reference/FactReference/RuleReference should be converted during graph building");
1556 }
1557 ExpressionKind::UnresolvedUnitLiteral(_, _) => {
1558 unreachable!(
1559 "UnresolvedUnitLiteral found during type computation - this is a bug: unresolved units should be resolved during graph building in convert_expression_and_extract_dependencies"
1560 );
1561 }
1562 }
1563}
1564
1565fn push_engine_error_at(
1566 errors: &mut Vec<LemmaError>,
1567 graph: &Graph,
1568 source: &Source,
1569 message: impl Into<String>,
1570) {
1571 errors.push(LemmaError::engine(
1572 message.into(),
1573 source.span.clone(),
1574 source.attribute.clone(),
1575 graph.source_text_for(source),
1576 source.doc_name.clone(),
1577 graph.doc_start_line_for(source),
1578 None::<String>,
1579 ));
1580}
1581
1582fn validate_logical_operands(
1583 left_type: &LemmaType,
1584 right_type: &LemmaType,
1585 graph: &Graph,
1586 source: &Source,
1587 errors: &mut Vec<LemmaError>,
1588) {
1589 if !left_type.is_boolean() {
1590 push_engine_error_at(
1591 errors,
1592 graph,
1593 source,
1594 format!(
1595 "Logical operation requires boolean operands, got {:?} for left operand",
1596 left_type
1597 ),
1598 );
1599 }
1600 if !right_type.is_boolean() {
1601 push_engine_error_at(
1602 errors,
1603 graph,
1604 source,
1605 format!(
1606 "Logical operation requires boolean operands, got {:?} for right operand",
1607 right_type
1608 ),
1609 );
1610 }
1611}
1612
1613fn validate_logical_operand(
1614 operand_type: &LemmaType,
1615 graph: &Graph,
1616 source: &Source,
1617 errors: &mut Vec<LemmaError>,
1618) {
1619 if !operand_type.is_boolean() {
1620 push_engine_error_at(
1621 errors,
1622 graph,
1623 source,
1624 format!(
1625 "Logical negation requires boolean operand, got {:?}",
1626 operand_type
1627 ),
1628 );
1629 }
1630}
1631
1632fn validate_comparison_types(
1633 left_type: &LemmaType,
1634 op: &crate::ComparisonComputation,
1635 right_type: &LemmaType,
1636 graph: &Graph,
1637 source: &Source,
1638 errors: &mut Vec<LemmaError>,
1639) {
1640 let is_equality_only = matches!(
1641 op,
1642 crate::ComparisonComputation::Equal
1643 | crate::ComparisonComputation::NotEqual
1644 | crate::ComparisonComputation::Is
1645 | crate::ComparisonComputation::IsNot
1646 );
1647
1648 if left_type.is_boolean() && right_type.is_boolean() {
1650 if !is_equality_only {
1651 push_engine_error_at(
1652 errors,
1653 graph,
1654 source,
1655 format!("Can only use == and != with booleans (got {})", op),
1656 );
1657 }
1658 return;
1659 }
1660
1661 if left_type.is_text() && right_type.is_text() {
1663 if !is_equality_only {
1664 push_engine_error_at(
1665 errors,
1666 graph,
1667 source,
1668 format!("Can only use == and != with text (got {})", op),
1669 );
1670 }
1671 return;
1672 }
1673
1674 if left_type.is_number() && right_type.is_number() {
1676 return;
1677 }
1678
1679 if left_type.is_ratio() && right_type.is_ratio() {
1681 return;
1682 }
1683
1684 if left_type.is_date() && right_type.is_date() {
1686 return;
1687 }
1688
1689 if left_type.is_time() && right_type.is_time() {
1691 return;
1692 }
1693
1694 if left_type.is_scale() && right_type.is_scale() {
1696 if left_type.name != right_type.name {
1697 push_engine_error_at(
1698 errors,
1699 graph,
1700 source,
1701 format!(
1702 "Cannot compare different scale types: {} and {}",
1703 left_type.name(),
1704 right_type.name()
1705 ),
1706 );
1707 }
1708 return;
1709 }
1710
1711 if left_type.is_duration() && right_type.is_duration() {
1713 return;
1714 }
1715 if left_type.is_duration() && right_type.is_number() {
1716 return;
1717 }
1718 if left_type.is_number() && right_type.is_duration() {
1719 return;
1720 }
1721
1722 push_engine_error_at(
1723 errors,
1724 graph,
1725 source,
1726 format!("Cannot compare {:?} with {:?}", left_type, right_type,),
1727 );
1728}
1729
1730fn validate_arithmetic_types(
1731 left_type: &LemmaType,
1732 right_type: &LemmaType,
1733 operator: &ArithmeticComputation,
1734 graph: &Graph,
1735 source: &Source,
1736 errors: &mut Vec<LemmaError>,
1737) {
1738 if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
1740 let result = compute_temporal_arithmetic_result_type(left_type, right_type, operator);
1744 if result.is_duration()
1746 && !matches!(
1747 operator,
1748 ArithmeticComputation::Subtract | ArithmeticComputation::Add
1749 )
1750 {
1751 push_engine_error_at(
1752 errors,
1753 graph,
1754 source,
1755 format!(
1756 "Invalid date/time arithmetic: {:?} {:?} {:?}",
1757 left_type, operator, right_type
1758 ),
1759 );
1760 }
1761 return;
1762 }
1763
1764 if left_type.is_scale() && right_type.is_scale() && left_type.name != right_type.name {
1766 push_engine_error_at(
1767 errors,
1768 graph,
1769 source,
1770 format!("Cannot {} different scale types: {} and {}. Operations between different scale types produce ambiguous result units.",
1771 match operator {
1772 ArithmeticComputation::Add => "add",
1773 ArithmeticComputation::Subtract => "subtract",
1774 ArithmeticComputation::Multiply => "multiply",
1775 ArithmeticComputation::Divide => "divide",
1776 ArithmeticComputation::Modulo => "modulo",
1777 ArithmeticComputation::Power => "power",
1778 },
1779 left_type.name(),
1780 right_type.name()
1781 ),
1782 );
1783 return;
1784 }
1785
1786 let left_valid = left_type.is_scale()
1790 || left_type.is_number()
1791 || left_type.is_duration()
1792 || left_type.is_ratio();
1793 let right_valid = right_type.is_scale()
1794 || right_type.is_number()
1795 || right_type.is_duration()
1796 || right_type.is_ratio();
1797
1798 if !left_valid {
1799 push_engine_error_at(
1800 errors,
1801 graph,
1802 source,
1803 format!(
1804 "Arithmetic operation requires numeric operands, got {:?} for left operand",
1805 left_type
1806 ),
1807 );
1808 return;
1809 }
1810 if !right_valid {
1811 push_engine_error_at(
1812 errors,
1813 graph,
1814 source,
1815 format!(
1816 "Arithmetic operation requires numeric operands, got {:?} for right operand",
1817 right_type
1818 ),
1819 );
1820 return;
1821 }
1822
1823 validate_arithmetic_operator_constraints(
1824 left_type, right_type, operator, graph, source, errors,
1825 );
1826}
1827
1828fn validate_arithmetic_operator_constraints(
1829 left_type: &LemmaType,
1830 right_type: &LemmaType,
1831 operator: &ArithmeticComputation,
1832 graph: &Graph,
1833 source: &Source,
1834 errors: &mut Vec<LemmaError>,
1835) {
1836 match operator {
1837 ArithmeticComputation::Modulo => {
1838 if left_type.is_duration() || right_type.is_duration() {
1839 push_engine_error_at(
1840 errors,
1841 graph,
1842 source,
1843 format!(
1844 "Modulo operation not supported for duration types: {:?} % {:?}",
1845 left_type, right_type
1846 ),
1847 );
1848 } else if !right_type.is_number() {
1849 push_engine_error_at(
1857 errors,
1858 graph,
1859 source,
1860 format!(
1861 "Modulo divisor must be a dimensionless number (not a scale type), got {}",
1862 right_type.name()
1863 ),
1864 );
1865 }
1866 }
1868 ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
1869 if !left_type.has_same_base_type(right_type) {
1882 let is_scale_number = (left_type.is_scale() && right_type.is_number())
1884 || (left_type.is_number() && right_type.is_scale());
1885
1886 let is_scale_ratio = (left_type.is_scale() && right_type.is_ratio())
1888 || (left_type.is_ratio() && right_type.is_scale());
1889
1890 let is_number_ratio = (left_type.is_number() && right_type.is_ratio())
1892 || (left_type.is_ratio() && right_type.is_number());
1893
1894 let is_duration_number = (left_type.is_duration() && right_type.is_number())
1896 || (left_type.is_number() && right_type.is_duration());
1897
1898 if is_duration_number {
1899 if matches!(operator, ArithmeticComputation::Divide)
1903 && left_type.is_number()
1904 && right_type.is_duration()
1905 {
1906 push_engine_error_at(
1907 errors,
1908 graph,
1909 source,
1910 "Cannot divide number by duration. Duration can only be multiplied by number or divided by number.".to_string(),
1911 );
1912 }
1913 } else if !is_scale_number && !is_scale_ratio && !is_number_ratio {
1915 push_engine_error_at(
1917 errors,
1918 graph,
1919 source,
1920 format!(
1921 "Cannot apply '{}' to values with different types: {} and {}. '*'/'/' require the same standard type, scale * number (or number * scale), scale * ratio (or ratio * scale), number * ratio (or ratio * number), or duration * number (or number * duration) for multiply, or duration / number for divide.",
1922 operator,
1923 left_type.name(),
1924 right_type.name()
1925 ),
1926 );
1927 }
1928 } else {
1929 }
1931 }
1932 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
1933 if !left_type.has_same_base_type(right_type) {
1942 let is_scale_number = (left_type.is_scale() && right_type.is_number())
1944 || (left_type.is_number() && right_type.is_scale());
1945
1946 let is_scale_ratio = (left_type.is_scale() && right_type.is_ratio())
1948 || (left_type.is_ratio() && right_type.is_scale());
1949
1950 let is_number_ratio = (left_type.is_number() && right_type.is_ratio())
1952 || (left_type.is_ratio() && right_type.is_number());
1953
1954 if !is_scale_number && !is_scale_ratio && !is_number_ratio {
1955 push_engine_error_at(
1957 errors,
1958 graph,
1959 source,
1960 format!(
1961 "Cannot apply '{}' to values with different types: {} and {}. '+'/'-' require the same standard type, scale + number (or number + scale), scale + ratio (or ratio + scale), or number + ratio (or ratio + number).",
1962 operator,
1963 left_type.name(),
1964 right_type.name()
1965 ),
1966 );
1967 }
1968 } else {
1969 }
1971 }
1972 ArithmeticComputation::Power => {
1973 if !right_type.is_number() && !right_type.is_ratio() {
1981 push_engine_error_at(
1982 errors,
1983 graph,
1984 source,
1985 format!(
1986 "Power exponent must be a dimensionless number (not a scale type), got {}",
1987 right_type.name()
1988 ),
1989 );
1990 }
1991 }
1993 }
1994}
1995
1996fn validate_unit_conversion_types(
1997 source_type: &LemmaType,
1998 target: &ConversionTarget,
1999 graph: &Graph,
2000 source: &Source,
2001 errors: &mut Vec<LemmaError>,
2002) {
2003 match target {
2004 ConversionTarget::ScaleUnit(unit_name) => {
2005 if !source_type.is_scale() {
2006 push_engine_error_at(
2007 errors,
2008 graph,
2009 source,
2010 format!(
2011 "Cannot convert {} to scale unit '{}': source is not a scale type",
2012 source_type.name(),
2013 unit_name
2014 ),
2015 );
2016 return;
2017 }
2018
2019 let units = match &source_type.specifications {
2020 crate::semantic::TypeSpecification::Scale { units, .. } => units,
2021 _ => unreachable!("BUG: is_scale() but not TypeSpecification::Scale"),
2022 };
2023
2024 if units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name)) {
2025 return;
2026 }
2027
2028 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2029 push_engine_error_at(
2030 errors,
2031 graph,
2032 source,
2033 format!(
2034 "Unknown unit '{}' for scale type {}. Valid units: {}",
2035 unit_name,
2036 source_type.name(),
2037 valid.join(", ")
2038 ),
2039 );
2040 }
2041 ConversionTarget::Duration(_) | ConversionTarget::Percentage => {
2042 let target_type = match target {
2043 ConversionTarget::Duration(_) => standard_duration().clone(),
2044 ConversionTarget::Percentage => standard_ratio().clone(),
2045 ConversionTarget::ScaleUnit(_) => unreachable!("handled above"),
2046 };
2047
2048 if source_type.specifications != target_type.specifications
2051 && !source_type.is_scale()
2052 && !source_type.is_number()
2053 {
2054 push_engine_error_at(
2055 errors,
2056 graph,
2057 source,
2058 format!("Cannot convert {:?} to {:?}", source_type, target_type),
2059 );
2060 }
2061 }
2062 }
2063}
2064
2065fn validate_mathematical_operand(
2066 operand_type: &LemmaType,
2067 graph: &Graph,
2068 source: &Source,
2069 errors: &mut Vec<LemmaError>,
2070) {
2071 if !operand_type.is_scale() && !operand_type.is_number() {
2074 push_engine_error_at(
2075 errors,
2076 graph,
2077 source,
2078 format!(
2079 "Mathematical function requires numeric operand (scale or number), got {:?}",
2080 operand_type
2081 ),
2082 );
2083 }
2084}
2085
2086fn compute_fact_type(
2087 fact_path: &FactPath,
2088 graph: &Graph,
2089 fact_source: &Source,
2090 errors: &mut Vec<LemmaError>,
2091) -> LemmaType {
2092 let fact = match graph.facts().get(fact_path) {
2093 Some(fact) => fact,
2094 None => {
2095 let maybe_rule_path = RulePath {
2098 segments: fact_path.segments.clone(),
2099 rule: fact_path.fact.clone(),
2100 };
2101
2102 if graph.rules().contains_key(&maybe_rule_path) {
2103 errors.push(LemmaError::semantic(
2104 format!(
2105 "Rule reference '{}' must use '?' (did you mean '{}?')",
2106 fact_path, fact_path
2107 ),
2108 fact_source.span.clone(),
2109 fact_source.attribute.clone(),
2110 graph.source_text_for(fact_source),
2111 fact_source.doc_name.clone(),
2112 graph.doc_start_line_for(fact_source),
2113 None::<String>,
2114 ));
2115 } else {
2116 errors.push(LemmaError::semantic(
2119 format!("Unknown fact reference '{}'", fact_path),
2120 fact_source.span.clone(),
2121 fact_source.attribute.clone(),
2122 graph.source_text_for(fact_source),
2123 fact_source.doc_name.clone(),
2124 graph.doc_start_line_for(fact_source),
2125 None::<String>,
2126 ));
2127 }
2128
2129 return crate::semantic::standard_text().clone();
2131 }
2132 };
2133 match &fact.value {
2134 FactValue::Literal(literal_value) => literal_value.get_type().clone(),
2135 FactValue::TypeDeclaration { .. } => {
2136 let fact_ref = FactReference {
2138 segments: fact_path.segments.iter().map(|s| s.fact.clone()).collect(),
2139 fact: fact_path.fact.clone(),
2140 };
2141
2142 for (_doc_name, document_types) in graph.resolved_types.iter() {
2145 if let Some(resolved_type) = document_types.inline_type_definitions.get(&fact_ref) {
2146 return resolved_type.clone();
2148 }
2149 }
2150
2151 let context_doc: &str = if let Some(first_segment) = fact_path.segments.first() {
2155 &first_segment.doc
2157 } else {
2158 let fact_ref_segments: Vec<String> = vec![];
2160 let mut found_doc: Option<&str> = None;
2161 for (doc_name, doc) in graph.all_docs() {
2162 for orig_fact in &doc.facts {
2163 if orig_fact.reference.segments == fact_ref_segments
2164 && orig_fact.reference.fact == fact_path.fact
2165 {
2166 found_doc = Some(doc_name);
2167 break;
2168 }
2169 }
2170 if found_doc.is_some() {
2171 break;
2172 }
2173 }
2174 if let Some(doc) = found_doc.or_else(|| {
2177 fact.source_location
2178 .as_ref()
2179 .map(|src| src.doc_name.as_str())
2180 }) {
2181 doc
2182 } else {
2183 unreachable!(
2184 "BUG: cannot determine document context for fact '{}' during planning",
2185 fact_path
2186 );
2187 }
2188 };
2189
2190 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
2193 unreachable!(
2194 "BUG: type declaration fact '{}' missing source_location",
2195 fact.reference.fact
2196 )
2197 });
2198 match graph.resolve_type_declaration(&fact.value, fact_source, context_doc) {
2199 Ok(lemma_type) => {
2200 lemma_type
2202 }
2203 Err(e) => {
2204 errors.push(e);
2205 LemmaType::veto_type()
2206 }
2207 }
2208 }
2209 FactValue::DocumentReference(_) => {
2210 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
2211 unreachable!(
2212 "BUG: document reference fact '{}' missing source_location",
2213 fact.reference.fact
2214 )
2215 });
2216 push_engine_error_at(
2217 errors,
2218 graph,
2219 fact_source,
2220 format!(
2221 "Cannot compute type for document reference fact '{}'",
2222 fact_path
2223 ),
2224 );
2225 LemmaType::veto_type()
2226 }
2227 }
2228}
2229
2230fn compute_arithmetic_result_type(
2231 left_type: LemmaType,
2232 right_type: LemmaType,
2233 operator: &ArithmeticComputation,
2234) -> LemmaType {
2235 let left = &left_type;
2236 let right = &right_type;
2237
2238 if left.is_date() || left.is_time() || right.is_date() || right.is_time() {
2239 return compute_temporal_arithmetic_result_type(left, right, operator);
2240 }
2241 if left == right {
2242 return left_type;
2243 }
2244
2245 if left.is_scale() && right.is_number() {
2247 return left_type; }
2249 if left.is_number() && right.is_scale() {
2250 return right_type; }
2252
2253 if left.is_ratio() && right.is_number() {
2256 return standard_number().clone(); }
2258 if left.is_number() && right.is_ratio() {
2259 return standard_number().clone(); }
2261 if left.is_ratio() && right.is_ratio() {
2263 return left_type; }
2265 if left.is_ratio() && right.is_scale() {
2267 return right_type; }
2269 if left.is_scale() && right.is_ratio() {
2270 return left_type; }
2272
2273 let one_is_standard_one_is_custom = left_type.name.is_none() != right_type.name.is_none();
2278
2279 if one_is_standard_one_is_custom {
2280 if left_type.name.is_some() {
2283 return left_type;
2284 } else {
2285 return right_type;
2286 }
2287 }
2288
2289 if left.name.is_some() && right.name.is_some() {
2292 return left_type;
2297 }
2298
2299 if left.is_scale() && right.is_scale() {
2305 return left_type;
2307 }
2308 if left.is_number() && right.is_number() {
2309 return standard_number().clone();
2311 }
2312
2313 standard_number().clone()
2315}
2316
2317fn compute_temporal_arithmetic_result_type(
2318 left: &LemmaType,
2319 right: &LemmaType,
2320 operator: &ArithmeticComputation,
2321) -> LemmaType {
2322 match operator {
2323 ArithmeticComputation::Subtract => {
2324 if left.is_date() && right.is_date() {
2326 return standard_duration().clone();
2327 }
2328 if left.is_time() && right.is_time() {
2330 return standard_duration().clone();
2331 }
2332 if left.is_date() && right.is_time() {
2334 return standard_duration().clone();
2335 }
2336 if left.is_time() && right.is_date() {
2338 return standard_duration().clone();
2339 }
2340 if left.is_date() && right.is_duration() {
2342 return left.clone();
2343 }
2344 if left.is_time() && right.is_duration() {
2346 return left.clone();
2347 }
2348 }
2349 ArithmeticComputation::Add => {
2350 if left.is_date() && right.is_duration() {
2352 return left.clone();
2353 }
2354 if left.is_time() && right.is_duration() {
2356 return left.clone();
2357 }
2358 if left.is_duration() && right.is_date() {
2360 return right.clone();
2361 }
2362 if left.is_duration() && right.is_time() {
2364 return right.clone();
2365 }
2366 }
2367 _ => {}
2368 }
2369 standard_duration().clone()
2372}
2373
2374fn validate_all_rule_references_exist(graph: &Graph, errors: &mut Vec<LemmaError>) {
2375 let existing_rules: HashSet<&RulePath> = graph.rules().keys().collect();
2376 for (rule_path, rule_node) in graph.rules() {
2377 for dependency in &rule_node.depends_on_rules {
2378 if !existing_rules.contains(dependency) {
2379 push_engine_error_at(
2380 errors,
2381 graph,
2382 &rule_node.source,
2383 format!(
2384 "Rule '{}' references non-existent rule '{}'",
2385 rule_path.rule, dependency.rule
2386 ),
2387 );
2388 }
2389 }
2390 }
2391}
2392
2393fn validate_fact_override_paths_target_document_facts(graph: &Graph, errors: &mut Vec<LemmaError>) {
2394 for (fact_path, fact) in graph.facts() {
2396 if fact_path.segments.is_empty() {
2397 continue;
2398 }
2399
2400 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
2401 unreachable!(
2402 "BUG: fact '{}' missing source_location while validating override paths",
2403 fact.reference.fact
2404 )
2405 });
2406
2407 for i in 0..fact_path.segments.len() {
2408 let seg = &fact_path.segments[i];
2409 let prefix_segments: Vec<PathSegment> = fact_path.segments[..i].to_vec();
2410 let seg_fact_path = FactPath::new(prefix_segments, seg.fact.clone());
2411
2412 match graph.facts().get(&seg_fact_path) {
2413 Some(seg_fact) => match &seg_fact.value {
2414 FactValue::DocumentReference(_) => {}
2415 _ => push_engine_error_at(
2416 errors,
2417 graph,
2418 fact_source,
2419 format!(
2420 "Invalid fact override path '{}': '{}' is not a document reference",
2421 fact_path, seg_fact_path
2422 ),
2423 ),
2424 },
2425 None => push_engine_error_at(
2426 errors,
2427 graph,
2428 fact_source,
2429 format!(
2430 "Invalid fact override path '{}': missing document reference '{}'",
2431 fact_path, seg_fact_path
2432 ),
2433 ),
2434 }
2435 }
2436 }
2437
2438 for doc in graph.all_docs.values() {
2446 for fact in &doc.facts {
2447 if fact.reference.segments.is_empty() {
2448 continue;
2449 }
2450
2451 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
2452 unreachable!(
2453 "BUG: override fact '{}.{}' missing source_location",
2454 fact.reference.segments.join("."),
2455 fact.reference.fact
2456 )
2457 });
2458
2459 let mut current_doc_name = doc.name.clone();
2460 let mut prefix: Vec<String> = Vec::new();
2461 let mut path_valid = true;
2462
2463 for seg in &fact.reference.segments {
2464 prefix.push(seg.clone());
2465
2466 let current_doc = match graph.all_docs.get(¤t_doc_name) {
2467 Some(d) => d,
2468 None => {
2469 push_engine_error_at(
2470 errors,
2471 graph,
2472 fact_source,
2473 format!(
2474 "Invalid fact override path '{}.{}': document '{}' not found",
2475 prefix.join("."),
2476 fact.reference.fact,
2477 current_doc_name
2478 ),
2479 );
2480 path_valid = false;
2481 break;
2482 }
2483 };
2484
2485 let Some(seg_fact) = current_doc
2486 .facts
2487 .iter()
2488 .find(|f| f.reference.segments.is_empty() && f.reference.fact == *seg)
2489 else {
2490 push_engine_error_at(
2491 errors,
2492 graph,
2493 fact_source,
2494 format!(
2495 "Invalid fact override path '{}.{}': missing document reference '{}'",
2496 prefix.join("."),
2497 fact.reference.fact,
2498 prefix.join(".")
2499 ),
2500 );
2501 path_valid = false;
2502 break;
2503 };
2504
2505 match &seg_fact.value {
2506 FactValue::DocumentReference(next_doc) => {
2507 current_doc_name = next_doc.clone();
2508 }
2509 _ => {
2510 push_engine_error_at(
2511 errors,
2512 graph,
2513 fact_source,
2514 format!(
2515 "Invalid fact override path '{}.{}': '{}' is not a document reference",
2516 prefix.join("."),
2517 fact.reference.fact,
2518 prefix.join(".")
2519 ),
2520 );
2521 path_valid = false;
2522 break;
2523 }
2524 }
2525 }
2526
2527 if path_valid {
2529 if let Some(target_doc) = graph.all_docs.get(¤t_doc_name) {
2530 if let Some(original_fact) = target_doc.facts.iter().find(|f| {
2531 f.reference.segments.is_empty() && f.reference.fact == fact.reference.fact
2532 }) {
2533 if matches!(&original_fact.value, FactValue::TypeDeclaration { .. })
2535 && matches!(&fact.value, FactValue::TypeDeclaration { .. })
2536 {
2537 let override_path = if fact.reference.segments.is_empty() {
2538 fact.reference.fact.clone()
2539 } else {
2540 format!(
2541 "{}.{}",
2542 fact.reference.segments.join("."),
2543 fact.reference.fact
2544 )
2545 };
2546 push_engine_error_at(
2547 errors,
2548 graph,
2549 fact_source,
2550 format!(
2551 "Cannot override typed fact '{}' with type definition. Use a concrete value instead.",
2552 override_path
2553 ),
2554 );
2555 }
2556 }
2557 }
2558 }
2559 }
2560 }
2561}
2562
2563fn validate_fact_and_rule_name_collisions(graph: &Graph, errors: &mut Vec<LemmaError>) {
2564 for rule_path in graph.rules().keys() {
2566 let fact_path = FactPath::new(rule_path.segments.clone(), rule_path.rule.clone());
2567 if graph.facts().contains_key(&fact_path) {
2568 let rule_node = graph.rules().get(rule_path).unwrap_or_else(|| {
2569 unreachable!(
2570 "BUG: rule '{}' missing from graph while validating name collisions",
2571 rule_path.rule
2572 )
2573 });
2574 push_engine_error_at(
2575 errors,
2576 graph,
2577 &rule_node.source,
2578 format!(
2579 "Name collision: '{}' is defined as both a fact and a rule",
2580 fact_path
2581 ),
2582 );
2583 }
2584 }
2585}
2586
2587fn validate_document_interfaces(
2588 graph: &Graph,
2589 all_docs: &[LemmaDoc],
2590 errors: &mut Vec<LemmaError>,
2591) {
2592 let mut referenced_rules: HashMap<Vec<String>, HashSet<String>> = HashMap::new();
2593 for rule_node in graph.rules().values() {
2594 for rule_dependency in &rule_node.depends_on_rules {
2595 if !rule_dependency.segments.is_empty() {
2596 let path: Vec<String> = rule_dependency
2597 .segments
2598 .iter()
2599 .map(|segment| segment.fact.clone())
2600 .collect();
2601 referenced_rules
2602 .entry(path)
2603 .or_default()
2604 .insert(rule_dependency.rule.clone());
2605 }
2606 }
2607 }
2608 for (fact_path, fact) in graph.facts() {
2609 if let FactValue::DocumentReference(doc_name) = &fact.value {
2610 let mut full_path: Vec<String> = fact_path
2611 .segments
2612 .iter()
2613 .map(|segment| segment.fact.clone())
2614 .collect();
2615 full_path.push(fact_path.fact.clone());
2616 if let Some(required_rules) = referenced_rules.get(&full_path) {
2617 let doc = match all_docs.iter().find(|document| document.name == *doc_name) {
2618 Some(document) => document,
2619 None => continue,
2620 };
2621 let doc_rule_names: HashSet<String> =
2622 doc.rules.iter().map(|rule| rule.name.clone()).collect();
2623 for required_rule in required_rules {
2624 if !doc_rule_names.contains(required_rule) {
2625 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
2626 unreachable!(
2627 "BUG: document reference fact '{}' missing source_location",
2628 fact.reference.fact
2629 )
2630 });
2631 push_engine_error_at(
2632 errors,
2633 graph,
2634 fact_source,
2635 format!(
2636 "Document '{}' referenced by '{}' is missing required rule '{}'",
2637 doc_name, fact_path, required_rule
2638 ),
2639 );
2640 }
2641 }
2642 }
2643 }
2644 }
2645}
2646
2647#[cfg(test)]
2648mod tests {
2649 use super::*;
2650
2651 use crate::semantic::{FactReference, LiteralValue, RuleReference};
2652
2653 fn test_source() -> Option<Source> {
2654 Some(Source::new(
2655 "test.lemma",
2656 Span {
2657 start: 0,
2658 end: 0,
2659 line: 1,
2660 col: 0,
2661 },
2662 "test",
2663 ))
2664 }
2665
2666 fn test_sources() -> HashMap<String, String> {
2667 let mut sources = HashMap::new();
2668 sources.insert("test.lemma".to_string(), "doc test\n".to_string());
2669 sources
2670 }
2671
2672 fn create_test_doc(name: &str) -> LemmaDoc {
2673 LemmaDoc::new(name.to_string())
2674 }
2675
2676 fn create_literal_fact(name: &str, value: LiteralValue) -> LemmaFact {
2677 LemmaFact {
2678 reference: FactReference {
2679 segments: Vec::new(),
2680 fact: name.to_string(),
2681 },
2682 value: FactValue::Literal(value),
2683 source_location: test_source(),
2684 }
2685 }
2686
2687 fn create_literal_expr(value: LiteralValue) -> Expression {
2688 Expression {
2689 kind: ExpressionKind::Literal(value),
2690 source_location: test_source(),
2691 }
2692 }
2693
2694 #[test]
2695 fn test_build_simple_graph() {
2696 let mut doc = create_test_doc("test");
2697 doc = doc.add_fact(create_literal_fact(
2698 "age",
2699 LiteralValue::number(rust_decimal::Decimal::from(25)),
2700 ));
2701 doc = doc.add_fact(create_literal_fact(
2702 "name",
2703 LiteralValue::text("John".to_string()),
2704 ));
2705
2706 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2707 assert!(result.is_ok(), "Should build graph successfully");
2708
2709 let graph = result.unwrap();
2710 assert_eq!(graph.facts().len(), 2);
2711 assert_eq!(graph.rules().len(), 0);
2712 }
2713
2714 #[test]
2715 fn test_build_graph_with_rule() {
2716 let mut doc = create_test_doc("test");
2717 doc = doc.add_fact(create_literal_fact(
2718 "age",
2719 LiteralValue::number(rust_decimal::Decimal::from(25)),
2720 ));
2721
2722 let age_expr = Expression {
2723 kind: ExpressionKind::FactReference(FactReference {
2724 segments: Vec::new(),
2725 fact: "age".to_string(),
2726 }),
2727 source_location: test_source(),
2728 };
2729
2730 let rule = LemmaRule {
2731 name: "is_adult".to_string(),
2732 expression: age_expr,
2733 unless_clauses: Vec::new(),
2734 source_location: test_source(),
2735 };
2736 doc = doc.add_rule(rule);
2737
2738 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2739 assert!(result.is_ok(), "Should build graph successfully");
2740
2741 let graph = result.unwrap();
2742 assert_eq!(graph.facts().len(), 1);
2743 assert_eq!(graph.rules().len(), 1);
2744 }
2745
2746 #[test]
2747 fn should_reject_fact_override_into_non_document_fact() {
2748 let mut doc = create_test_doc("test");
2753 doc = doc.add_fact(create_literal_fact("x", LiteralValue::number(1)));
2754
2755 doc = doc.add_fact(LemmaFact {
2757 reference: FactReference::from_path(vec!["x".to_string(), "y".to_string()]),
2758 value: FactValue::Literal(LiteralValue::number(2)),
2759 source_location: test_source(),
2760 });
2761
2762 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2763 assert!(
2764 result.is_err(),
2765 "Overriding x.y must fail when x is not a document reference"
2766 );
2767 }
2768
2769 #[test]
2770 fn should_reject_fact_and_rule_name_collision() {
2771 let mut doc = create_test_doc("test");
2776 doc = doc.add_fact(create_literal_fact("x", LiteralValue::number(1)));
2777 doc = doc.add_rule(LemmaRule {
2778 name: "x".to_string(),
2779 expression: create_literal_expr(LiteralValue::number(2)),
2780 unless_clauses: Vec::new(),
2781 source_location: test_source(),
2782 });
2783
2784 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2785 assert!(
2786 result.is_err(),
2787 "Fact and rule name collisions should be rejected"
2788 );
2789 }
2790
2791 #[test]
2792 fn test_duplicate_fact() {
2793 let mut doc = create_test_doc("test");
2794 doc = doc.add_fact(create_literal_fact(
2795 "age",
2796 LiteralValue::number(rust_decimal::Decimal::from(25)),
2797 ));
2798 doc = doc.add_fact(create_literal_fact(
2799 "age",
2800 LiteralValue::number(rust_decimal::Decimal::from(30)),
2801 ));
2802
2803 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2804 assert!(result.is_err(), "Should detect duplicate fact");
2805
2806 let errors = result.unwrap_err();
2807 assert!(errors
2808 .iter()
2809 .any(|e| e.to_string().contains("Duplicate fact") && e.to_string().contains("age")));
2810 }
2811
2812 #[test]
2813 fn test_duplicate_rule() {
2814 let mut doc = create_test_doc("test");
2815
2816 let rule1 = LemmaRule {
2817 name: "test_rule".to_string(),
2818 expression: create_literal_expr(LiteralValue::boolean(true.into())),
2819 unless_clauses: Vec::new(),
2820 source_location: test_source(),
2821 };
2822 let rule2 = LemmaRule {
2823 name: "test_rule".to_string(),
2824 expression: create_literal_expr(LiteralValue::boolean(false.into())),
2825 unless_clauses: Vec::new(),
2826 source_location: test_source(),
2827 };
2828
2829 doc = doc.add_rule(rule1);
2830 doc = doc.add_rule(rule2);
2831
2832 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2833 assert!(result.is_err(), "Should detect duplicate rule");
2834
2835 let errors = result.unwrap_err();
2836 assert!(errors.iter().any(
2837 |e| e.to_string().contains("Duplicate rule") && e.to_string().contains("test_rule")
2838 ));
2839 }
2840
2841 #[test]
2842 fn test_missing_fact_reference() {
2843 let mut doc = create_test_doc("test");
2844
2845 let missing_fact_expr = Expression {
2846 kind: ExpressionKind::FactReference(FactReference {
2847 segments: Vec::new(),
2848 fact: "nonexistent".to_string(),
2849 }),
2850 source_location: test_source(),
2851 };
2852
2853 let rule = LemmaRule {
2854 name: "test_rule".to_string(),
2855 expression: missing_fact_expr,
2856 unless_clauses: Vec::new(),
2857 source_location: test_source(),
2858 };
2859 doc = doc.add_rule(rule);
2860
2861 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2862 assert!(result.is_err(), "Should detect missing fact");
2863
2864 let errors = result.unwrap_err();
2865 assert!(errors
2866 .iter()
2867 .any(|e| e.to_string().contains("Fact 'nonexistent' not found")));
2868 }
2869
2870 #[test]
2871 fn test_missing_document_reference() {
2872 let mut doc = create_test_doc("test");
2873
2874 let fact = LemmaFact {
2875 reference: FactReference {
2876 segments: Vec::new(),
2877 fact: "contract".to_string(),
2878 },
2879 value: FactValue::DocumentReference("nonexistent".to_string()),
2880 source_location: test_source(),
2881 };
2882 doc = doc.add_fact(fact);
2883
2884 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2885 assert!(result.is_err(), "Should detect missing document");
2886
2887 let errors = result.unwrap_err();
2888 assert!(errors
2889 .iter()
2890 .any(|e| e.to_string().contains("Document 'nonexistent' not found")));
2891 }
2892
2893 #[test]
2894 fn test_fact_reference_conversion() {
2895 let mut doc = create_test_doc("test");
2896 doc = doc.add_fact(create_literal_fact(
2897 "age",
2898 LiteralValue::number(rust_decimal::Decimal::from(25)),
2899 ));
2900
2901 let age_expr = Expression {
2902 kind: ExpressionKind::FactReference(FactReference {
2903 segments: Vec::new(),
2904 fact: "age".to_string(),
2905 }),
2906 source_location: test_source(),
2907 };
2908
2909 let rule = LemmaRule {
2910 name: "test_rule".to_string(),
2911 expression: age_expr,
2912 unless_clauses: Vec::new(),
2913 source_location: test_source(),
2914 };
2915 doc = doc.add_rule(rule);
2916
2917 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2918 assert!(result.is_ok(), "Should build graph successfully");
2919
2920 let graph = result.unwrap();
2921 let rule_node = graph.rules().values().next().unwrap();
2922
2923 assert!(matches!(
2924 rule_node.branches[0].1.kind,
2925 ExpressionKind::FactPath(_)
2926 ));
2927 }
2928
2929 #[test]
2930 fn test_rule_reference_conversion() {
2931 let mut doc = create_test_doc("test");
2932
2933 let rule1_expr = Expression {
2934 kind: ExpressionKind::FactReference(FactReference {
2935 segments: Vec::new(),
2936 fact: "age".to_string(),
2937 }),
2938 source_location: test_source(),
2939 };
2940
2941 let rule1 = LemmaRule {
2942 name: "rule1".to_string(),
2943 expression: rule1_expr,
2944 unless_clauses: Vec::new(),
2945 source_location: test_source(),
2946 };
2947 doc = doc.add_rule(rule1);
2948
2949 let rule2_expr = Expression {
2950 kind: ExpressionKind::RuleReference(RuleReference {
2951 segments: Vec::new(),
2952 rule: "rule1".to_string(),
2953 }),
2954 source_location: test_source(),
2955 };
2956
2957 let rule2 = LemmaRule {
2958 name: "rule2".to_string(),
2959 expression: rule2_expr,
2960 unless_clauses: Vec::new(),
2961 source_location: test_source(),
2962 };
2963 doc = doc.add_rule(rule2);
2964
2965 doc = doc.add_fact(create_literal_fact(
2966 "age",
2967 LiteralValue::number(rust_decimal::Decimal::from(25)),
2968 ));
2969
2970 let result = Graph::build(&doc, &[doc.clone()], test_sources());
2971 assert!(result.is_ok(), "Should build graph successfully");
2972
2973 let graph = result.unwrap();
2974 let rule2_node = graph
2975 .rules()
2976 .get(&RulePath {
2977 segments: Vec::new(),
2978 rule: "rule2".to_string(),
2979 })
2980 .unwrap();
2981
2982 assert_eq!(rule2_node.depends_on_rules.len(), 1);
2983 assert!(matches!(
2984 rule2_node.branches[0].1.kind,
2985 ExpressionKind::RulePath(_)
2986 ));
2987 }
2988
2989 #[test]
2990 fn test_collect_multiple_errors() {
2991 let mut doc = create_test_doc("test");
2992 doc = doc.add_fact(create_literal_fact(
2993 "age",
2994 LiteralValue::number(rust_decimal::Decimal::from(25)),
2995 ));
2996 doc = doc.add_fact(create_literal_fact(
2997 "age",
2998 LiteralValue::number(rust_decimal::Decimal::from(30)),
2999 ));
3000
3001 let missing_fact_expr = Expression {
3002 kind: ExpressionKind::FactReference(FactReference {
3003 segments: Vec::new(),
3004 fact: "nonexistent".to_string(),
3005 }),
3006 source_location: test_source(),
3007 };
3008
3009 let rule = LemmaRule {
3010 name: "test_rule".to_string(),
3011 expression: missing_fact_expr,
3012 unless_clauses: Vec::new(),
3013 source_location: test_source(),
3014 };
3015 doc = doc.add_rule(rule);
3016
3017 let result = Graph::build(&doc, &[doc.clone()], test_sources());
3018 assert!(result.is_err(), "Should collect multiple errors");
3019
3020 let errors = result.unwrap_err();
3021 assert!(errors.len() >= 2, "Should have at least 2 errors");
3022 assert!(errors
3023 .iter()
3024 .any(|e| e.to_string().contains("Duplicate fact")));
3025 assert!(errors
3026 .iter()
3027 .any(|e| e.to_string().contains("Fact 'nonexistent' not found")));
3028 }
3029}