1use crate::parsing::ast::{self as ast, LemmaDoc, LemmaFact, LemmaRule, TypeDef, Value};
2use crate::parsing::source::Source;
3use crate::planning::semantics::{
4 conversion_target_to_semantic, parse_number_unit, primitive_boolean, primitive_date,
5 primitive_duration, primitive_number, primitive_ratio, primitive_scale, primitive_text,
6 primitive_time, value_to_semantic, ArithmeticComputation, Expression, ExpressionKind, FactData,
7 FactPath, LemmaType, LiteralValue, PathSegment, RulePath, SemanticConversionTarget,
8 TypeExtends, TypeSpecification, ValueKind,
9};
10use crate::planning::types::{ResolvedDocumentTypes, TypeRegistry};
11use crate::planning::validation::{
12 validate_document_interfaces, validate_type_specifications, RuleEntryForBindingCheck,
13};
14use crate::LemmaError;
15use ast::FactValue as ParsedFactValue;
16use indexmap::IndexMap;
17use std::collections::{HashMap, HashSet, VecDeque};
18use std::sync::Arc;
19
20type FactBindings = HashMap<Vec<String>, (ParsedFactValue, Source)>;
29
30#[derive(Debug)]
31pub(crate) struct Graph {
32 facts: IndexMap<FactPath, FactData>,
33 rules: IndexMap<RulePath, RuleNode>,
34 sources: HashMap<String, String>,
35 execution_order: Vec<RulePath>,
36 pub(crate) resolved_types: HashMap<String, ResolvedDocumentTypes>,
39}
40
41impl Graph {
42 pub(crate) fn facts(&self) -> &IndexMap<FactPath, FactData> {
43 &self.facts
44 }
45
46 pub(crate) fn rules(&self) -> &IndexMap<RulePath, RuleNode> {
47 &self.rules
48 }
49
50 pub(crate) fn rules_mut(&mut self) -> &mut IndexMap<RulePath, RuleNode> {
51 &mut self.rules
52 }
53
54 pub(crate) fn sources(&self) -> &HashMap<String, String> {
55 &self.sources
56 }
57
58 pub(crate) fn execution_order(&self) -> &[RulePath] {
59 &self.execution_order
60 }
61
62 pub(crate) fn build_facts(&self) -> HashMap<FactPath, FactData> {
64 let mut schema: HashMap<FactPath, LemmaType> = HashMap::new();
65 let mut values: HashMap<FactPath, LiteralValue> = HashMap::new();
66 let mut doc_refs: HashMap<FactPath, String> = HashMap::new();
67 let mut sources: HashMap<FactPath, Source> = HashMap::new();
68
69 for (path, rfv) in self.facts.iter() {
70 sources.insert(path.clone(), rfv.source().clone());
71 match rfv {
72 FactData::Value { value, .. } => {
73 values.insert(path.clone(), value.clone());
74 schema.insert(path.clone(), value.lemma_type.clone());
75 }
76 FactData::TypeDeclaration { resolved_type, .. } => {
77 schema.insert(path.clone(), resolved_type.clone());
78 }
79 FactData::DocumentRef { doc_name, .. } => {
80 doc_refs.insert(path.clone(), doc_name.clone());
81 }
82 }
83 }
84
85 for (path, schema_type) in &schema {
86 if values.contains_key(path) {
87 continue;
88 }
89 if let Some(default_value) = schema_type.create_default_value() {
90 values.insert(path.clone(), default_value);
91 }
92 }
93
94 for (path, value) in values.iter_mut() {
95 let Some(schema_type) = schema.get(path).cloned() else {
96 continue;
97 };
98 match Self::coerce_literal_to_schema_type(value, &schema_type) {
99 Ok(coerced) => *value = coerced,
100 Err(msg) => unreachable!("Fact {} incompatible: {}", path, msg),
101 }
102 }
103
104 let mut facts = HashMap::new();
105 for (path, source) in sources {
106 if let Some(doc_name) = doc_refs.get(&path) {
107 facts.insert(
108 path,
109 FactData::DocumentRef {
110 doc_name: doc_name.clone(),
111 source,
112 },
113 );
114 } else if let Some(value) = values.remove(&path) {
115 facts.insert(path, FactData::Value { value, source });
116 } else {
117 let resolved_type = schema
118 .get(&path)
119 .cloned()
120 .expect("non-doc-ref fact has schema (value or type-only)");
121 facts.insert(
122 path,
123 FactData::TypeDeclaration {
124 resolved_type,
125 source,
126 },
127 );
128 }
129 }
130 facts
131 }
132
133 fn coerce_literal_to_schema_type(
134 lit: &LiteralValue,
135 schema_type: &LemmaType,
136 ) -> Result<LiteralValue, String> {
137 if lit.lemma_type.specifications == schema_type.specifications {
138 let mut out = lit.clone();
139 out.lemma_type = schema_type.clone();
140 return Ok(out);
141 }
142 match (&schema_type.specifications, &lit.value) {
143 (TypeSpecification::Number { .. }, ValueKind::Number(_))
144 | (TypeSpecification::Text { .. }, ValueKind::Text(_))
145 | (TypeSpecification::Boolean { .. }, ValueKind::Boolean(_))
146 | (TypeSpecification::Date { .. }, ValueKind::Date(_))
147 | (TypeSpecification::Time { .. }, ValueKind::Time(_))
148 | (TypeSpecification::Duration { .. }, ValueKind::Duration(_, _))
149 | (TypeSpecification::Ratio { .. }, ValueKind::Ratio(_, _))
150 | (TypeSpecification::Scale { .. }, ValueKind::Scale(_, _)) => {
151 let mut out = lit.clone();
152 out.lemma_type = schema_type.clone();
153 Ok(out)
154 }
155 (TypeSpecification::Ratio { .. }, ValueKind::Number(n)) => {
156 Ok(LiteralValue::ratio_with_type(*n, None, schema_type.clone()))
157 }
158 _ => Err(format!(
159 "value {} cannot be used as type {}",
160 lit,
161 schema_type.name()
162 )),
163 }
164 }
165
166 fn resolve_primitive_type(name: &str) -> Option<TypeSpecification> {
168 match name {
169 "boolean" => Some(TypeSpecification::boolean()),
170 "scale" => Some(TypeSpecification::scale()),
171 "number" => Some(TypeSpecification::number()),
172 "ratio" => Some(TypeSpecification::ratio()),
173 "text" => Some(TypeSpecification::text()),
174 "date" => Some(TypeSpecification::date()),
175 "time" => Some(TypeSpecification::time()),
176 "duration" => Some(TypeSpecification::duration()),
177 "percent" => Some(TypeSpecification::ratio()),
178 _ => None,
179 }
180 }
181
182 fn topological_sort(&self) -> Result<Vec<RulePath>, Vec<LemmaError>> {
183 let mut in_degree: HashMap<RulePath, usize> = HashMap::new();
184 let mut dependents: HashMap<RulePath, Vec<RulePath>> = HashMap::new();
185 let mut queue = VecDeque::new();
186 let mut result = Vec::new();
187
188 for rule_path in self.rules.keys() {
189 in_degree.insert(rule_path.clone(), 0);
190 dependents.insert(rule_path.clone(), Vec::new());
191 }
192
193 for (rule_path, rule_node) in &self.rules {
194 for dependency in &rule_node.depends_on_rules {
195 if self.rules.contains_key(dependency) {
196 if let Some(degree) = in_degree.get_mut(rule_path) {
197 *degree += 1;
198 }
199 if let Some(deps) = dependents.get_mut(dependency) {
200 deps.push(rule_path.clone());
201 }
202 }
203 }
204 }
205
206 for (rule_path, degree) in &in_degree {
207 if *degree == 0 {
208 queue.push_back(rule_path.clone());
209 }
210 }
211
212 while let Some(rule_path) = queue.pop_front() {
213 result.push(rule_path.clone());
214
215 if let Some(dependent_rules) = dependents.get(&rule_path) {
216 for dependent in dependent_rules {
217 if let Some(degree) = in_degree.get_mut(dependent) {
218 *degree -= 1;
219 if *degree == 0 {
220 queue.push_back(dependent.clone());
221 }
222 }
223 }
224 }
225 }
226
227 if result.len() != self.rules.len() {
228 let missing: Vec<RulePath> = self
229 .rules
230 .keys()
231 .filter(|rule| !result.contains(rule))
232 .cloned()
233 .collect();
234 let cycle: Vec<Source> = missing
235 .iter()
236 .filter_map(|rule| self.rules.get(rule).map(|n| n.source.clone()))
237 .collect();
238
239 let Some(first_source) = cycle.first() else {
240 unreachable!(
241 "BUG: circular dependency detected but no sources could be collected ({} missing rules)",
242 missing.len()
243 );
244 };
245
246 return Err(vec![LemmaError::circular_dependency(
247 format!(
248 "Circular dependency detected. Rules involved: {}",
249 missing
250 .iter()
251 .map(|rule| rule.rule.clone())
252 .collect::<Vec<_>>()
253 .join(", ")
254 ),
255 Some(first_source.clone()),
256 cycle,
257 None::<String>,
258 )]);
259 }
260
261 Ok(result)
262 }
263}
264
265#[derive(Debug)]
266pub(crate) struct RuleNode {
267 pub branches: Vec<(Option<Expression>, Expression)>,
270 pub source: Source,
271
272 pub depends_on_rules: HashSet<RulePath>,
273
274 pub rule_type: LemmaType,
277}
278
279struct GraphBuilder<'a> {
280 facts: IndexMap<FactPath, FactData>,
281 rules: IndexMap<RulePath, RuleNode>,
282 sources: HashMap<String, String>,
283 all_docs: HashMap<String, &'a LemmaDoc>,
284 resolved_types: HashMap<String, ResolvedDocumentTypes>,
285 errors: Vec<LemmaError>,
286}
287
288#[derive(Clone)]
293pub(crate) struct PreparedTypes {
294 pub type_registry: TypeRegistry,
295 pub resolved_types: HashMap<String, ResolvedDocumentTypes>,
296}
297
298impl Graph {
299 pub(crate) fn prepare_types(
307 all_docs: &[LemmaDoc],
308 sources: &HashMap<String, String>,
309 ) -> (PreparedTypes, Vec<LemmaError>) {
310 let mut type_registry = TypeRegistry::new(sources.clone());
311 let mut errors: Vec<LemmaError> = Vec::new();
312 let mut resolved_types: HashMap<String, ResolvedDocumentTypes> = HashMap::new();
313
314 for doc in all_docs {
316 for type_def in &doc.types {
317 if let Err(e) = type_registry.register_type(&doc.name, type_def.clone()) {
318 errors.push(e);
319 }
320 }
321 }
322
323 for doc in all_docs {
331 match type_registry.resolve_named_types(&doc.name) {
332 Ok(document_types) => {
333 for (type_name, lemma_type) in &document_types.named_types {
335 let source = doc
337 .types
338 .iter()
339 .find(|td| match td {
340 ast::TypeDef::Regular { name, .. }
341 | ast::TypeDef::Import { name, .. } => name == type_name,
342 ast::TypeDef::Inline { .. } => false,
343 })
344 .map(|td| td.source_location().clone())
345 .unwrap_or_else(|| {
346 unreachable!(
347 "BUG: resolved named type '{}' has no corresponding TypeDef in document '{}'",
348 type_name, doc.name
349 )
350 });
351 let mut spec_errors = validate_type_specifications(
352 &lemma_type.specifications,
353 type_name,
354 &source,
355 );
356 errors.append(&mut spec_errors);
357 }
358 resolved_types.insert(doc.name.clone(), document_types);
359 }
360 Err(e) => errors.push(e),
361 }
362 }
363
364 let prepared = PreparedTypes {
365 type_registry,
366 resolved_types,
367 };
368 (prepared, errors)
369 }
370
371 pub(crate) fn build(
380 main_doc: &LemmaDoc,
381 all_docs: &[LemmaDoc],
382 sources: HashMap<String, String>,
383 prepared: &PreparedTypes,
384 ) -> Result<Graph, Vec<LemmaError>> {
385 let mut type_registry = prepared.type_registry.clone();
386
387 let mut builder = GraphBuilder {
388 facts: IndexMap::new(),
389 rules: IndexMap::new(),
390 sources,
391 all_docs: all_docs.iter().map(|doc| (doc.name.clone(), doc)).collect(),
392 resolved_types: prepared.resolved_types.clone(),
393 errors: Vec::new(),
394 };
395
396 builder.build_document(main_doc, Vec::new(), HashMap::new(), &mut type_registry)?;
405
406 let graph_errors = builder.errors;
407
408 let mut graph = Graph {
409 facts: builder.facts,
410 rules: builder.rules,
411 sources: builder.sources,
412 execution_order: Vec::new(),
413 resolved_types: builder.resolved_types,
414 };
415
416 let validation_errors = match graph.validate(all_docs) {
421 Ok(()) => Vec::new(),
422 Err(errors) => errors,
423 };
424
425 let mut all_errors = graph_errors;
426 all_errors.extend(validation_errors);
427
428 if all_errors.is_empty() {
429 Ok(graph)
430 } else {
431 Err(all_errors)
432 }
433 }
434
435 fn validate(&mut self, all_docs: &[LemmaDoc]) -> Result<(), Vec<LemmaError>> {
436 let mut errors = Vec::new();
437
438 if let Err(structural_errors) = check_all_rule_references_exist(self) {
440 errors.extend(structural_errors);
441 }
442 if let Err(collision_errors) = check_fact_and_rule_name_collisions(self) {
443 errors.extend(collision_errors);
444 }
445
446 let execution_order = match self.topological_sort() {
447 Ok(order) => order,
448 Err(circular_errors) => {
449 errors.extend(circular_errors);
450 return Err(errors);
451 }
452 };
453
454 let inferred_types = infer_rule_types(self, &execution_order);
461
462 if let Err(type_errors) = check_rule_types(self, &execution_order, &inferred_types) {
464 errors.extend(type_errors);
465 }
466
467 let referenced_rules = compute_referenced_rules_by_path(self);
469 let doc_ref_facts: Vec<(FactPath, String, Source)> = self
470 .facts()
471 .iter()
472 .filter_map(|(path, fact)| {
473 fact.doc_ref()
474 .map(|doc_name| (path.clone(), doc_name.to_string(), fact.source().clone()))
475 })
476 .collect();
477 let rule_entries: IndexMap<RulePath, RuleEntryForBindingCheck> = self
478 .rules()
479 .iter()
480 .map(|(path, node)| {
481 let rule_type = inferred_types
482 .get(path)
483 .cloned()
484 .unwrap_or_else(|| node.rule_type.clone());
485 (
486 path.clone(),
487 RuleEntryForBindingCheck {
488 rule_type,
489 depends_on_rules: node.depends_on_rules.clone(),
490 branches: node.branches.clone(),
491 },
492 )
493 })
494 .collect();
495 if let Err(interface_errors) = validate_document_interfaces(
496 &referenced_rules,
497 &doc_ref_facts,
498 &rule_entries,
499 all_docs,
500 self.sources(),
501 ) {
502 errors.extend(interface_errors);
503 }
504
505 if !errors.is_empty() {
506 return Err(errors);
507 }
508
509 apply_inferred_types(self, inferred_types);
511 self.execution_order = execution_order;
512 Ok(())
513 }
514}
515
516impl<'a> GraphBuilder<'a> {
517 fn engine_error(&self, message: impl Into<String>, source: &Source) -> LemmaError {
518 LemmaError::engine(message.into(), Some(source.clone()), None::<String>)
519 }
520
521 fn resolve_type_declaration(
523 &self,
524 type_decl: &ParsedFactValue,
525 decl_source: &Source,
526 context_doc: &str,
527 ) -> Result<LemmaType, Vec<LemmaError>> {
528 let ParsedFactValue::TypeDeclaration {
529 base,
530 constraints,
531 from,
532 } = type_decl
533 else {
534 unreachable!(
535 "BUG: resolve_type_declaration called with non-TypeDeclaration ParsedFactValue"
536 );
537 };
538
539 if base.is_empty() {
540 return Err(vec![
541 self.engine_error("TypeDeclaration base cannot be empty", decl_source)
542 ]);
543 }
544
545 let source_doc = from
549 .as_ref()
550 .map(|r| r.name.as_str())
551 .unwrap_or(context_doc);
552
553 let (base_lemma_type, extends) = if let Some(specs) = Graph::resolve_primitive_type(base) {
555 (LemmaType::primitive(specs), TypeExtends::Primitive)
557 } else {
558 let document_types = self.resolved_types.get(source_doc).ok_or_else(|| {
560 vec![self.engine_error(
561 format!("Resolved types not found for document '{}'", source_doc),
562 decl_source,
563 )]
564 })?;
565
566 let base_type = document_types
567 .named_types
568 .get(base)
569 .ok_or_else(|| {
570 vec![self.engine_error(
571 format!("Unknown type: '{}'. Type must be defined before use.", base),
572 decl_source,
573 )]
574 })?
575 .clone();
576 let family = base_type
577 .scale_family_name()
578 .map(String::from)
579 .unwrap_or_else(|| base.clone());
580 let extends = TypeExtends::Custom {
581 parent: base.to_string(),
582 family,
583 };
584 (base_type, extends)
585 };
586
587 let mut errors = Vec::new();
589 let mut specs = base_lemma_type.specifications;
590 if let Some(ref constraints_vec) = constraints {
591 for (command, args) in constraints_vec {
592 match specs.clone().apply_constraint(command, args) {
593 Ok(updated) => specs = updated,
594 Err(e) => errors.push(self.engine_error(
595 format!("Invalid command '{}' for type '{}': {}", command, base, e),
596 decl_source,
597 )),
598 }
599 }
600 errors.extend(validate_type_specifications(&specs, base, decl_source));
601 }
602
603 if !errors.is_empty() {
604 return Err(errors);
605 }
606
607 Ok(LemmaType::new(base.clone(), specs, extends))
608 }
609
610 fn resolve_fact_binding(
616 &self,
617 fact: &LemmaFact,
618 current_segment_names: &[String],
619 effective_doc_refs: &HashMap<String, String>,
620 ) -> Result<(Vec<String>, ParsedFactValue, Source), Vec<LemmaError>> {
621 let fact_source = &fact.source_location;
622 let binding_path_display = format!(
623 "{}.{}",
624 fact.reference.segments.join("."),
625 fact.reference.fact
626 );
627
628 let mut current_doc_name: Option<String> = None;
629
630 for (index, segment) in fact.reference.segments.iter().enumerate() {
631 let doc_name = if index == 0 {
632 match effective_doc_refs.get(segment) {
633 Some(name) => name.clone(),
634 None => {
635 return Err(vec![self.engine_error(
636 format!(
637 "Invalid fact binding path '{}': '{}' is not a document reference",
638 binding_path_display, segment
639 ),
640 fact_source,
641 )]);
642 }
643 }
644 } else {
645 let prev_doc_name = current_doc_name.as_ref().unwrap_or_else(|| {
646 unreachable!(
647 "BUG: current_doc_name should be set after first segment in resolve_fact_binding"
648 )
649 });
650 let prev_doc = match self.all_docs.get(prev_doc_name.as_str()) {
651 Some(d) => d,
652 None => {
653 return Err(vec![self.engine_error(
654 format!(
655 "Invalid fact binding path '{}': document '{}' not found",
656 binding_path_display, prev_doc_name
657 ),
658 fact_source,
659 )]);
660 }
661 };
662
663 let seg_fact = prev_doc
664 .facts
665 .iter()
666 .find(|f| f.reference.segments.is_empty() && f.reference.fact == *segment);
667
668 match seg_fact {
669 Some(f) => match &f.value {
670 ParsedFactValue::DocumentReference(doc_ref) => doc_ref.name.clone(),
671 _ => {
672 return Err(vec![self.engine_error(
673 format!(
674 "Invalid fact binding path '{}': '{}' in document '{}' is not a document reference",
675 binding_path_display, segment, prev_doc_name
676 ),
677 fact_source,
678 )]);
679 }
680 },
681 None => {
682 return Err(vec![self.engine_error(
683 format!(
684 "Invalid fact binding path '{}': fact '{}' not found in document '{}'",
685 binding_path_display, segment, prev_doc_name
686 ),
687 fact_source,
688 )]);
689 }
690 }
691 };
692
693 current_doc_name = Some(doc_name);
694 }
695
696 let mut binding_key: Vec<String> = current_segment_names.to_vec();
698 binding_key.extend(fact.reference.segments.iter().cloned());
699 binding_key.push(fact.reference.fact.clone());
700
701 Ok((
702 binding_key,
703 fact.value.clone(),
704 fact.source_location.clone(),
705 ))
706 }
707
708 fn build_fact_bindings(
714 &self,
715 doc: &LemmaDoc,
716 current_segment_names: &[String],
717 effective_doc_refs: &HashMap<String, String>,
718 ) -> Result<FactBindings, Vec<LemmaError>> {
719 let mut bindings: FactBindings = HashMap::new();
720 let mut errors: Vec<LemmaError> = Vec::new();
721
722 for fact in &doc.facts {
723 if fact.reference.segments.is_empty() {
724 continue; }
726
727 if matches!(&fact.value, ParsedFactValue::TypeDeclaration { .. }) {
729 let binding_path_display = format!(
730 "{}.{}",
731 fact.reference.segments.join("."),
732 fact.reference.fact
733 );
734 errors.push(self.engine_error(
735 format!(
736 "Fact binding '{}' must provide a literal value or document reference, not a type declaration",
737 binding_path_display
738 ),
739 &fact.source_location,
740 ));
741 continue;
742 }
743
744 match self.resolve_fact_binding(fact, current_segment_names, effective_doc_refs) {
745 Ok((binding_key, fact_value, source)) => {
746 if let Some((_, existing_source)) = bindings.get(&binding_key) {
747 errors.push(self.engine_error(
748 format!(
749 "Duplicate fact binding for '{}' (previously bound at {}:{})",
750 binding_key.join("."),
751 existing_source.attribute,
752 existing_source.span.line
753 ),
754 &fact.source_location,
755 ));
756 } else {
757 bindings.insert(binding_key, (fact_value, source));
758 }
759 }
760 Err(mut resolve_errors) => {
761 errors.append(&mut resolve_errors);
762 }
763 }
764 }
765
766 if !errors.is_empty() {
767 return Err(errors);
768 }
769
770 Ok(bindings)
771 }
772
773 #[allow(clippy::too_many_arguments)]
779 fn add_fact(
780 &mut self,
781 fact: &'a LemmaFact,
782 current_segments: &[PathSegment],
783 fact_bindings: &FactBindings,
784 effective_doc_refs: &HashMap<String, String>,
785 current_doc: &'a LemmaDoc,
786 used_binding_keys: &mut HashSet<Vec<String>>,
787 ) {
788 let fact_path = FactPath {
789 segments: current_segments.to_vec(),
790 fact: fact.reference.fact.clone(),
791 };
792
793 if self.facts.contains_key(&fact_path) {
795 self.errors.push(self.engine_error(
796 format!("Duplicate fact '{}'", fact_path.fact),
797 &fact.source_location,
798 ));
799 return;
800 }
801
802 let binding_key: Vec<String> = current_segments
804 .iter()
805 .map(|s| s.fact.clone())
806 .chain(std::iter::once(fact.reference.fact.clone()))
807 .collect();
808
809 let (effective_value, effective_source) = match fact_bindings.get(&binding_key) {
811 Some((bound_value, bound_source)) => {
812 used_binding_keys.insert(binding_key);
813 (bound_value.clone(), bound_source.clone())
814 }
815 None => (fact.value.clone(), fact.source_location.clone()),
816 };
817
818 let original_schema_type = if matches!(&fact.value, ParsedFactValue::TypeDeclaration { .. })
822 {
823 match self.resolve_type_declaration(
824 &fact.value,
825 &fact.source_location,
826 current_doc.name.as_str(),
827 ) {
828 Ok(t) => Some(t),
829 Err(errs) => {
830 self.errors.extend(errs);
831 return;
832 }
833 }
834 } else {
835 None
836 };
837
838 match &effective_value {
839 ParsedFactValue::Literal(value) => {
840 let semantic_value = match value_to_semantic(value) {
841 Ok(s) => s,
842 Err(e) => {
843 self.errors.push(self.engine_error(e, &effective_source));
844 return;
845 }
846 };
847 let inferred_type = match value {
848 Value::Text(_) => primitive_text().clone(),
849 Value::Number(_) => primitive_number().clone(),
850 Value::Scale(_, unit) => self
851 .resolved_types
852 .get(¤t_doc.name)
853 .and_then(|dt| dt.unit_index.get(unit))
854 .cloned()
855 .unwrap_or_else(|| primitive_scale().clone()),
856 Value::Boolean(_) => primitive_boolean().clone(),
857 Value::Date(_) => primitive_date().clone(),
858 Value::Time(_) => primitive_time().clone(),
859 Value::Duration(_, _) => primitive_duration().clone(),
860 Value::Ratio(_, _) => primitive_ratio().clone(),
861 };
862 let schema_type = original_schema_type.unwrap_or(inferred_type);
865 let literal_value = LiteralValue {
866 value: semantic_value,
867 lemma_type: schema_type,
868 };
869 self.facts.insert(
870 fact_path,
871 FactData::Value {
872 value: literal_value,
873 source: effective_source,
874 },
875 );
876 }
877 ParsedFactValue::TypeDeclaration { .. } => {
878 let resolved_type = original_schema_type.unwrap_or_else(|| {
880 match self.resolve_type_declaration(
881 &effective_value,
882 &effective_source,
883 current_doc.name.as_str(),
884 ) {
885 Ok(t) => t,
886 Err(_) => {
887 unreachable!(
892 "BUG: TypeDeclaration effective value without original_schema_type"
893 )
894 }
895 }
896 });
897
898 self.facts.insert(
899 fact_path,
900 FactData::TypeDeclaration {
901 resolved_type,
902 source: effective_source,
903 },
904 );
905 }
906 ParsedFactValue::DocumentReference(_) => {
907 let effective_doc_name = effective_doc_refs
910 .get(&fact.reference.fact)
911 .cloned()
912 .unwrap_or_else(|| {
913 if let ParsedFactValue::DocumentReference(doc_ref) = &effective_value {
914 doc_ref.name.clone()
915 } else {
916 unreachable!(
917 "BUG: effective_value is DocumentReference but pattern didn't match"
918 )
919 }
920 });
921
922 if !self.all_docs.contains_key(effective_doc_name.as_str()) {
923 self.errors.push(self.engine_error(
924 format!("Document '{}' not found", effective_doc_name),
925 &effective_source,
926 ));
927 return;
928 }
929
930 self.facts.insert(
931 fact_path,
932 FactData::DocumentRef {
933 doc_name: effective_doc_name,
934 source: effective_source,
935 },
936 );
937 }
938 }
939 }
940
941 fn resolve_path_segments(
942 &mut self,
943 segments: &[String],
944 reference_source: &Source,
945 mut current_facts_map: HashMap<String, &'a LemmaFact>,
946 mut path_segments: Vec<PathSegment>,
947 effective_doc_refs: &HashMap<String, String>,
948 ) -> Option<Vec<PathSegment>> {
949 for (index, segment) in segments.iter().enumerate() {
950 let fact_ref =
951 match current_facts_map.get(segment) {
952 Some(f) => f,
953 None => {
954 self.errors.push(self.engine_error(
955 format!("Fact '{}' not found", segment),
956 reference_source,
957 ));
958 return None;
959 }
960 };
961
962 if let ParsedFactValue::DocumentReference(original_doc_ref) = &fact_ref.value {
963 let doc_name = if index == 0 {
967 effective_doc_refs
968 .get(segment)
969 .map(|s| s.as_str())
970 .unwrap_or(&original_doc_ref.name)
971 } else {
972 &original_doc_ref.name
973 };
974
975 let next_doc = match self.all_docs.get(doc_name) {
976 Some(d) => d,
977 None => {
978 self.errors.push(self.engine_error(
979 format!("Document '{}' not found", doc_name),
980 reference_source,
981 ));
982 return None;
983 }
984 };
985 path_segments.push(PathSegment {
986 fact: segment.clone(),
987 doc: doc_name.to_string(),
988 });
989 current_facts_map = next_doc
990 .facts
991 .iter()
992 .map(|f| (f.reference.fact.clone(), f))
993 .collect();
994 } else {
995 self.errors.push(self.engine_error(
996 format!("Fact '{}' is not a document reference", segment),
997 reference_source,
998 ));
999 return None;
1000 }
1001 }
1002 Some(path_segments)
1003 }
1004
1005 fn build_document(
1006 &mut self,
1007 doc: &'a LemmaDoc,
1008 current_segments: Vec<PathSegment>,
1009 fact_bindings: FactBindings,
1010 type_registry: &mut TypeRegistry,
1011 ) -> Result<(), Vec<LemmaError>> {
1012 let mut effective_doc_refs: HashMap<String, String> = HashMap::new();
1015 for fact in doc.facts.iter() {
1016 if fact.reference.segments.is_empty() {
1017 if let ParsedFactValue::DocumentReference(doc_ref) = &fact.value {
1018 effective_doc_refs.insert(fact.reference.fact.clone(), doc_ref.name.clone());
1019 }
1020 }
1021 }
1022
1023 let current_segment_names: Vec<String> =
1026 current_segments.iter().map(|s| s.fact.clone()).collect();
1027 for fact in doc.facts.iter() {
1028 if fact.reference.segments.is_empty() {
1029 if let ParsedFactValue::DocumentReference(_) = &fact.value {
1030 let mut binding_key = current_segment_names.clone();
1031 binding_key.push(fact.reference.fact.clone());
1032 if let Some((ParsedFactValue::DocumentReference(bound_doc_ref), _)) =
1033 fact_bindings.get(&binding_key)
1034 {
1035 effective_doc_refs
1036 .insert(fact.reference.fact.clone(), bound_doc_ref.name.clone());
1037 }
1038 }
1039 }
1040 }
1041
1042 let this_doc_bindings =
1044 match self.build_fact_bindings(doc, ¤t_segment_names, &effective_doc_refs) {
1045 Ok(bindings) => bindings,
1046 Err(errors) => {
1047 self.errors.extend(errors);
1048 HashMap::new() }
1050 };
1051
1052 let facts_map: HashMap<String, &LemmaFact> = doc
1054 .facts
1055 .iter()
1056 .map(|fact| (fact.reference.fact.clone(), fact))
1057 .collect();
1058
1059 if current_segments.is_empty() {
1062 for fact in &doc.facts {
1063 if !fact.reference.segments.is_empty() {
1064 continue;
1065 }
1066 if let ParsedFactValue::TypeDeclaration {
1067 base,
1068 constraints: inline_constraints,
1069 from,
1070 } = &fact.value
1071 {
1072 if base.is_empty() {
1073 self.errors.push(self.engine_error(
1074 "TypeDeclaration base cannot be empty",
1075 &fact.source_location,
1076 ));
1077 continue;
1078 }
1079 let is_inline_type_definition = from.is_some() || inline_constraints.is_some();
1080 if is_inline_type_definition {
1081 let source_location = fact.source_location.clone();
1082 let inline_type_def = TypeDef::Inline {
1083 source_location,
1084 parent: base.clone(),
1085 constraints: inline_constraints.clone(),
1086 fact_ref: fact.reference.clone(),
1087 from: from.clone(),
1088 };
1089 if let Err(e) = type_registry.register_type(&doc.name, inline_type_def) {
1090 self.errors.push(e);
1091 }
1092 }
1093 }
1094 }
1095 }
1096
1097 if let Some(existing_types) = self.resolved_types.remove(&doc.name) {
1104 match type_registry.resolve_inline_types(&doc.name, existing_types) {
1105 Ok(document_types) => {
1106 for (fact_ref, lemma_type) in &document_types.inline_type_definitions {
1107 let type_name = format!("{} (inline)", fact_ref.fact);
1108 let fact = doc
1109 .facts
1110 .iter()
1111 .find(|f| &f.reference == fact_ref)
1112 .unwrap_or_else(|| {
1113 unreachable!(
1114 "BUG: inline type definition for '{}' has no corresponding fact in document '{}'",
1115 fact_ref.fact, doc.name
1116 )
1117 });
1118 let source = &fact.source_location;
1119 let mut spec_errors = validate_type_specifications(
1120 &lemma_type.specifications,
1121 &type_name,
1122 source,
1123 );
1124 self.errors.append(&mut spec_errors);
1125 }
1126 self.resolved_types.insert(doc.name.clone(), document_types);
1127 }
1128 Err(e) => {
1129 self.errors.push(e);
1130 }
1131 }
1132 }
1133
1134 let mut used_binding_keys: HashSet<Vec<String>> = HashSet::new();
1136 for fact in &doc.facts {
1137 if !fact.reference.segments.is_empty() {
1138 continue; }
1140 self.add_fact(
1141 fact,
1142 ¤t_segments,
1143 &fact_bindings,
1144 &effective_doc_refs,
1145 doc,
1146 &mut used_binding_keys,
1147 );
1148 }
1149
1150 for (path, rfv) in &self.facts {
1152 if path.segments.len() != current_segments.len() {
1153 continue;
1154 }
1155 if !path
1156 .segments
1157 .iter()
1158 .zip(current_segments.iter())
1159 .all(|(a, b)| a.fact == b.fact && a.doc == b.doc)
1160 {
1161 continue;
1162 }
1163 if let FactData::DocumentRef { doc_name, .. } = rfv {
1164 effective_doc_refs.insert(path.fact.clone(), doc_name.clone());
1165 }
1166 }
1167
1168 for fact in &doc.facts {
1170 if !fact.reference.segments.is_empty() {
1171 continue;
1172 }
1173 if let ParsedFactValue::DocumentReference(_) = &fact.value {
1174 let doc_name = match effective_doc_refs.get(&fact.reference.fact) {
1175 Some(name) => name.clone(),
1176 None => continue, };
1178 let nested_doc = match self.all_docs.get(doc_name.as_str()) {
1179 Some(d) => *d,
1180 None => continue, };
1182 let mut nested_segments = current_segments.clone();
1183 nested_segments.push(PathSegment {
1184 fact: fact.reference.fact.clone(),
1185 doc: doc_name.clone(),
1186 });
1187
1188 let nested_segment_names: Vec<String> =
1191 nested_segments.iter().map(|s| s.fact.clone()).collect();
1192 let mut combined_bindings = this_doc_bindings.clone();
1193 for (key, value_and_source) in &fact_bindings {
1194 if key.len() > nested_segment_names.len()
1195 && key[..nested_segment_names.len()] == nested_segment_names[..]
1196 && !combined_bindings.contains_key(key)
1197 {
1198 combined_bindings.insert(key.clone(), value_and_source.clone());
1199 }
1200 }
1201
1202 if let Err(errs) = self.build_document(
1203 nested_doc,
1204 nested_segments,
1205 combined_bindings,
1206 type_registry,
1207 ) {
1208 self.errors.extend(errs);
1209 }
1210 }
1211 }
1212
1213 let expected_key_len = current_segments.len() + 1;
1216 for (binding_key, (_, binding_source)) in &fact_bindings {
1217 if binding_key.len() == expected_key_len
1218 && binding_key[..current_segments.len()]
1219 .iter()
1220 .zip(current_segments.iter())
1221 .all(|(a, b)| a == &b.fact)
1222 && !used_binding_keys.contains(binding_key)
1223 {
1224 self.errors.push(self.engine_error(
1225 format!(
1226 "Fact binding targets a fact that does not exist in the referenced document: '{}'",
1227 binding_key.join(".")
1228 ),
1229 binding_source,
1230 ));
1231 }
1232 }
1233
1234 for rule in &doc.rules {
1236 self.add_rule(
1237 rule,
1238 doc,
1239 &facts_map,
1240 ¤t_segments,
1241 &effective_doc_refs,
1242 );
1243 }
1244
1245 Ok(())
1246 }
1247
1248 fn add_rule(
1249 &mut self,
1250 rule: &LemmaRule,
1251 current_doc: &'a LemmaDoc,
1252 facts_map: &HashMap<String, &'a LemmaFact>,
1253 current_segments: &[PathSegment],
1254 effective_doc_refs: &HashMap<String, String>,
1255 ) {
1256 let rule_path = RulePath {
1257 segments: current_segments.to_vec(),
1258 rule: rule.name.clone(),
1259 };
1260
1261 if self.rules.contains_key(&rule_path) {
1262 let rule_source = &rule.source_location;
1263 self.errors.push(
1264 self.engine_error(format!("Duplicate rule '{}'", rule_path.rule), rule_source),
1265 );
1266 return;
1267 }
1268
1269 let mut branches = Vec::new();
1270 let mut depends_on_rules = HashSet::new();
1271
1272 let converted_expression = match self.convert_expression_and_extract_dependencies(
1273 &rule.expression,
1274 current_doc,
1275 facts_map,
1276 current_segments,
1277 &mut depends_on_rules,
1278 effective_doc_refs,
1279 ) {
1280 Some(expr) => expr,
1281 None => return,
1282 };
1283 branches.push((None, converted_expression));
1284
1285 for unless_clause in &rule.unless_clauses {
1286 let converted_condition = match self.convert_expression_and_extract_dependencies(
1287 &unless_clause.condition,
1288 current_doc,
1289 facts_map,
1290 current_segments,
1291 &mut depends_on_rules,
1292 effective_doc_refs,
1293 ) {
1294 Some(expr) => expr,
1295 None => return,
1296 };
1297 let converted_result = match self.convert_expression_and_extract_dependencies(
1298 &unless_clause.result,
1299 current_doc,
1300 facts_map,
1301 current_segments,
1302 &mut depends_on_rules,
1303 effective_doc_refs,
1304 ) {
1305 Some(expr) => expr,
1306 None => return,
1307 };
1308 branches.push((Some(converted_condition), converted_result));
1309 }
1310
1311 let rule_node = RuleNode {
1312 branches,
1313 source: rule.source_location.clone(),
1314 depends_on_rules,
1315 rule_type: LemmaType::veto_type(), };
1317
1318 self.rules.insert(rule_path, rule_node);
1319 }
1320
1321 #[allow(clippy::too_many_arguments)]
1324 fn convert_binary_operands(
1325 &mut self,
1326 left: &ast::Expression,
1327 right: &ast::Expression,
1328 current_doc: &'a LemmaDoc,
1329 facts_map: &HashMap<String, &'a LemmaFact>,
1330 current_segments: &[PathSegment],
1331 depends_on_rules: &mut HashSet<RulePath>,
1332 effective_doc_refs: &HashMap<String, String>,
1333 ) -> Option<(Expression, Expression)> {
1334 let converted_left = self.convert_expression_and_extract_dependencies(
1335 left,
1336 current_doc,
1337 facts_map,
1338 current_segments,
1339 depends_on_rules,
1340 effective_doc_refs,
1341 )?;
1342 let converted_right = self.convert_expression_and_extract_dependencies(
1343 right,
1344 current_doc,
1345 facts_map,
1346 current_segments,
1347 depends_on_rules,
1348 effective_doc_refs,
1349 )?;
1350 Some((converted_left, converted_right))
1351 }
1352
1353 fn convert_expression_and_extract_dependencies(
1361 &mut self,
1362 expr: &ast::Expression,
1363 current_doc: &'a LemmaDoc,
1364 facts_map: &HashMap<String, &'a LemmaFact>,
1365 current_segments: &[PathSegment],
1366 depends_on_rules: &mut HashSet<RulePath>,
1367 effective_doc_refs: &HashMap<String, String>,
1368 ) -> Option<Expression> {
1369 let expr_src = expr
1370 .source_location
1371 .as_ref()
1372 .expect("BUG: AST expression missing source location");
1373 match &expr.kind {
1374 ast::ExpressionKind::FactReference(r) => {
1375 let expr_source = expr_src;
1376 let segments = self.resolve_path_segments(
1377 &r.segments,
1378 expr_source,
1379 facts_map.clone(),
1380 current_segments.to_vec(),
1381 effective_doc_refs,
1382 )?;
1383
1384 if r.segments.is_empty() && !facts_map.contains_key(&r.fact) {
1385 let is_rule = current_doc.rules.iter().any(|rule| rule.name == r.fact);
1386 if is_rule {
1387 self.errors.push(self.engine_error(
1388 format!(
1389 "'{}' is a rule, not a fact. Use '{}?' to reference rules",
1390 r.fact, r.fact
1391 ),
1392 expr_source,
1393 ));
1394 } else {
1395 self.errors.push(
1396 self.engine_error(format!("Fact '{}' not found", r.fact), expr_source),
1397 );
1398 }
1399 return None;
1400 }
1401
1402 let fact_path = FactPath {
1403 segments,
1404 fact: r.fact.clone(),
1405 };
1406
1407 Some(Expression {
1408 kind: ExpressionKind::FactPath(fact_path),
1409 source_location: expr.source_location.clone(),
1410 })
1411 }
1412 ast::ExpressionKind::UnresolvedUnitLiteral(_number, unit_name) => {
1413 let expr_source = expr_src;
1414
1415 let Some(document_types) = self.resolved_types.get(¤t_doc.name) else {
1416 self.errors.push(self.engine_error(
1417 format!(
1418 "Cannot resolve unit '{}': types were not resolved for document '{}'",
1419 unit_name, current_doc.name
1420 ),
1421 expr_source,
1422 ));
1423 return None;
1424 };
1425
1426 let lemma_type = match document_types.unit_index.get(unit_name) {
1427 Some(lemma_type) => lemma_type.clone(),
1428 None => {
1429 self.errors.push(self.engine_error(
1430 format!(
1431 "Unknown unit '{}' in document '{}'",
1432 unit_name, current_doc.name
1433 ),
1434 expr_source,
1435 ));
1436 return None;
1437 }
1438 };
1439
1440 let source_text = self.sources.get(&expr_src.attribute).unwrap_or_else(|| {
1441 unreachable!(
1442 "BUG: missing sources entry for attribute '{}' (doc '{}')",
1443 expr_src.attribute, current_doc.name
1444 )
1445 });
1446 let literal_str = match expr_src.extract_text(source_text) {
1447 Some(s) => s,
1448 None => {
1449 self.errors.push(self.engine_error(
1450 "Could not extract source text for literal".to_string(),
1451 expr_source,
1452 ));
1453 return None;
1454 }
1455 };
1456
1457 let value = match parse_number_unit(&literal_str, &lemma_type.specifications) {
1458 Ok(v) => v,
1459 Err(e) => {
1460 self.errors.push(self.engine_error(e, expr_source));
1461 return None;
1462 }
1463 };
1464
1465 let literal_value = match value {
1466 Value::Scale(n, u) => LiteralValue::scale_with_type(n, u, lemma_type),
1467 Value::Ratio(r, u) => LiteralValue::ratio_with_type(r, u, lemma_type),
1468 _ => unreachable!(
1469 "parse_number_unit only returns Scale or Ratio for unit_index types"
1470 ),
1471 };
1472 Some(Expression {
1473 kind: ExpressionKind::Literal(Box::new(literal_value)),
1474 source_location: expr.source_location.clone(),
1475 })
1476 }
1477 ast::ExpressionKind::RuleReference(rule_ref) => {
1478 let expr_source = expr_src;
1479 let segments = self.resolve_path_segments(
1480 &rule_ref.segments,
1481 expr_source,
1482 facts_map.clone(),
1483 current_segments.to_vec(),
1484 effective_doc_refs,
1485 )?;
1486
1487 let rule_path = RulePath {
1488 segments,
1489 rule: rule_ref.rule.clone(),
1490 };
1491
1492 depends_on_rules.insert(rule_path.clone());
1493
1494 Some(Expression {
1495 kind: ExpressionKind::RulePath(rule_path),
1496 source_location: expr.source_location.clone(),
1497 })
1498 }
1499
1500 ast::ExpressionKind::LogicalAnd(left, right) => {
1501 let (l, r) = self.convert_binary_operands(
1502 left,
1503 right,
1504 current_doc,
1505 facts_map,
1506 current_segments,
1507 depends_on_rules,
1508 effective_doc_refs,
1509 )?;
1510 Some(Expression {
1511 kind: ExpressionKind::LogicalAnd(Arc::new(l), Arc::new(r)),
1512 source_location: expr.source_location.clone(),
1513 })
1514 }
1515
1516 ast::ExpressionKind::LogicalOr(left, right) => {
1517 let (l, r) = self.convert_binary_operands(
1518 left,
1519 right,
1520 current_doc,
1521 facts_map,
1522 current_segments,
1523 depends_on_rules,
1524 effective_doc_refs,
1525 )?;
1526 Some(Expression {
1527 kind: ExpressionKind::LogicalOr(Arc::new(l), Arc::new(r)),
1528 source_location: expr.source_location.clone(),
1529 })
1530 }
1531
1532 ast::ExpressionKind::Arithmetic(left, op, right) => {
1533 let (l, r) = self.convert_binary_operands(
1534 left,
1535 right,
1536 current_doc,
1537 facts_map,
1538 current_segments,
1539 depends_on_rules,
1540 effective_doc_refs,
1541 )?;
1542 Some(Expression {
1543 kind: ExpressionKind::Arithmetic(Arc::new(l), op.clone(), Arc::new(r)),
1544 source_location: expr.source_location.clone(),
1545 })
1546 }
1547
1548 ast::ExpressionKind::Comparison(left, op, right) => {
1549 let (l, r) = self.convert_binary_operands(
1550 left,
1551 right,
1552 current_doc,
1553 facts_map,
1554 current_segments,
1555 depends_on_rules,
1556 effective_doc_refs,
1557 )?;
1558 Some(Expression {
1559 kind: ExpressionKind::Comparison(Arc::new(l), op.clone(), Arc::new(r)),
1560 source_location: expr.source_location.clone(),
1561 })
1562 }
1563
1564 ast::ExpressionKind::UnitConversion(value, target) => {
1565 let converted_value = self.convert_expression_and_extract_dependencies(
1566 value,
1567 current_doc,
1568 facts_map,
1569 current_segments,
1570 depends_on_rules,
1571 effective_doc_refs,
1572 )?;
1573
1574 let unit_index = self
1575 .resolved_types
1576 .get(¤t_doc.name)
1577 .map(|dt| &dt.unit_index);
1578 let semantic_target = match conversion_target_to_semantic(target, unit_index) {
1579 Ok(t) => t,
1580 Err(msg) => {
1581 let full_msg = unit_index
1582 .map(|idx| {
1583 let valid: Vec<&str> = idx.keys().map(String::as_str).collect();
1584 format!("{} Valid units: {}", msg, valid.join(", "))
1585 })
1586 .unwrap_or(msg);
1587 self.errors.push(LemmaError::semantic(
1588 full_msg,
1589 expr.source_location.clone(),
1590 None::<String>,
1591 ));
1592 return None;
1593 }
1594 };
1595
1596 Some(Expression {
1597 kind: ExpressionKind::UnitConversion(
1598 Arc::new(converted_value),
1599 semantic_target,
1600 ),
1601 source_location: expr.source_location.clone(),
1602 })
1603 }
1604
1605 ast::ExpressionKind::LogicalNegation(operand, neg_type) => {
1606 let converted_operand = self.convert_expression_and_extract_dependencies(
1607 operand,
1608 current_doc,
1609 facts_map,
1610 current_segments,
1611 depends_on_rules,
1612 effective_doc_refs,
1613 )?;
1614 Some(Expression {
1615 kind: ExpressionKind::LogicalNegation(
1616 Arc::new(converted_operand),
1617 neg_type.clone(),
1618 ),
1619 source_location: expr.source_location.clone(),
1620 })
1621 }
1622
1623 ast::ExpressionKind::MathematicalComputation(op, operand) => {
1624 let converted_operand = self.convert_expression_and_extract_dependencies(
1625 operand,
1626 current_doc,
1627 facts_map,
1628 current_segments,
1629 depends_on_rules,
1630 effective_doc_refs,
1631 )?;
1632 Some(Expression {
1633 kind: ExpressionKind::MathematicalComputation(
1634 op.clone(),
1635 Arc::new(converted_operand),
1636 ),
1637 source_location: expr.source_location.clone(),
1638 })
1639 }
1640
1641 ast::ExpressionKind::Literal(value) => {
1642 let semantic_value = match value_to_semantic(value) {
1644 Ok(v) => v,
1645 Err(e) => {
1646 self.errors.push(self.engine_error(e, expr_src));
1647 return None;
1648 }
1649 };
1650 let lemma_type = match value {
1652 Value::Text(_) => primitive_text().clone(),
1653 Value::Number(_) => primitive_number().clone(),
1654 Value::Scale(_, unit) => self
1655 .resolved_types
1656 .get(¤t_doc.name)
1657 .and_then(|dt| dt.unit_index.get(unit))
1658 .cloned()
1659 .unwrap_or_else(|| primitive_scale().clone()),
1660 Value::Boolean(_) => primitive_boolean().clone(),
1661 Value::Date(_) => primitive_date().clone(),
1662 Value::Time(_) => primitive_time().clone(),
1663 Value::Duration(_, _) => primitive_duration().clone(),
1664 Value::Ratio(_, _) => primitive_ratio().clone(),
1665 };
1666 let literal_value = LiteralValue {
1667 value: semantic_value,
1668 lemma_type,
1669 };
1670 Some(Expression {
1671 kind: ExpressionKind::Literal(Box::new(literal_value)),
1672 source_location: expr.source_location.clone(),
1673 })
1674 }
1675
1676 ast::ExpressionKind::Veto(veto_expression) => Some(Expression {
1677 kind: ExpressionKind::Veto(veto_expression.clone()),
1678 source_location: expr.source_location.clone(),
1679 }),
1680 }
1681 }
1682}
1683
1684fn compute_arithmetic_result_type(left_type: LemmaType, right_type: LemmaType) -> LemmaType {
1685 compute_arithmetic_result_type_recursive(left_type, right_type, false)
1686}
1687
1688fn compute_arithmetic_result_type_recursive(
1689 left_type: LemmaType,
1690 right_type: LemmaType,
1691 swapped: bool,
1692) -> LemmaType {
1693 match (&left_type.specifications, &right_type.specifications) {
1694 (TypeSpecification::Error, _) => LemmaType::error_type(),
1695
1696 (TypeSpecification::Date { .. }, TypeSpecification::Date { .. }) => {
1697 primitive_duration().clone()
1698 }
1699 (TypeSpecification::Date { .. }, TypeSpecification::Time { .. }) => {
1700 primitive_duration().clone()
1701 }
1702 (TypeSpecification::Time { .. }, TypeSpecification::Time { .. }) => {
1703 primitive_duration().clone()
1704 }
1705
1706 _ if left_type == right_type => left_type,
1707
1708 (TypeSpecification::Date { .. }, TypeSpecification::Duration { .. }) => left_type,
1709 (TypeSpecification::Time { .. }, TypeSpecification::Duration { .. }) => left_type,
1710
1711 (TypeSpecification::Scale { .. }, TypeSpecification::Ratio { .. }) => left_type,
1712 (TypeSpecification::Scale { .. }, TypeSpecification::Number { .. }) => left_type,
1713 (TypeSpecification::Scale { .. }, TypeSpecification::Duration { .. }) => {
1714 primitive_number().clone()
1715 }
1716 (TypeSpecification::Scale { .. }, TypeSpecification::Scale { .. }) => left_type,
1717
1718 (TypeSpecification::Duration { .. }, TypeSpecification::Number { .. }) => left_type,
1719 (TypeSpecification::Duration { .. }, TypeSpecification::Ratio { .. }) => left_type,
1720 (TypeSpecification::Duration { .. }, TypeSpecification::Duration { .. }) => {
1721 primitive_duration().clone()
1722 }
1723
1724 (TypeSpecification::Number { .. }, TypeSpecification::Ratio { .. }) => {
1725 primitive_number().clone()
1726 }
1727 (TypeSpecification::Number { .. }, TypeSpecification::Number { .. }) => {
1728 primitive_number().clone()
1729 }
1730
1731 (TypeSpecification::Ratio { .. }, TypeSpecification::Ratio { .. }) => left_type,
1732
1733 _ => {
1734 if swapped {
1735 LemmaType::error_type()
1736 } else {
1737 compute_arithmetic_result_type_recursive(right_type, left_type, true)
1738 }
1739 }
1740 }
1741}
1742
1743fn infer_expression_type(
1751 expression: &Expression,
1752 graph: &Graph,
1753 computed_rule_types: &HashMap<RulePath, LemmaType>,
1754) -> LemmaType {
1755 match &expression.kind {
1756 ExpressionKind::Literal(literal_value) => literal_value.as_ref().get_type().clone(),
1757
1758 ExpressionKind::FactPath(fact_path) => infer_fact_type(fact_path, graph),
1759
1760 ExpressionKind::RulePath(rule_path) => computed_rule_types
1761 .get(rule_path)
1762 .cloned()
1763 .unwrap_or_else(LemmaType::error_type),
1764
1765 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
1766 let left_type = infer_expression_type(left, graph, computed_rule_types);
1767 let right_type = infer_expression_type(right, graph, computed_rule_types);
1768 if left_type.is_error() || right_type.is_error() {
1769 return LemmaType::error_type();
1770 }
1771 primitive_boolean().clone()
1772 }
1773
1774 ExpressionKind::LogicalNegation(operand, _) => {
1775 let operand_type = infer_expression_type(operand, graph, computed_rule_types);
1776 if operand_type.is_error() {
1777 return LemmaType::error_type();
1778 }
1779 primitive_boolean().clone()
1780 }
1781
1782 ExpressionKind::Comparison(left, _op, right) => {
1783 let left_type = infer_expression_type(left, graph, computed_rule_types);
1784 let right_type = infer_expression_type(right, graph, computed_rule_types);
1785 if left_type.is_error() || right_type.is_error() {
1786 return LemmaType::error_type();
1787 }
1788 primitive_boolean().clone()
1789 }
1790
1791 ExpressionKind::Arithmetic(left, _operator, right) => {
1792 let left_type = infer_expression_type(left, graph, computed_rule_types);
1793 let right_type = infer_expression_type(right, graph, computed_rule_types);
1794 compute_arithmetic_result_type(left_type, right_type)
1795 }
1796
1797 ExpressionKind::UnitConversion(source_expression, target) => {
1798 let expr_source = expression
1799 .source_location
1800 .as_ref()
1801 .expect("BUG: expression missing source in infer_expression_type");
1802 let source_type = infer_expression_type(source_expression, graph, computed_rule_types);
1803 if source_type.is_error() {
1804 return LemmaType::error_type();
1805 }
1806 match target {
1807 SemanticConversionTarget::Duration(_) => primitive_duration().clone(),
1808 SemanticConversionTarget::ScaleUnit(unit_name) => {
1809 if source_type.is_number() {
1810 let doc_name = &expr_source.doc_name;
1811 graph
1812 .resolved_types
1813 .get(doc_name)
1814 .and_then(|dt| dt.unit_index.get(unit_name).cloned())
1815 .unwrap_or_else(LemmaType::error_type)
1816 } else {
1817 source_type
1818 }
1819 }
1820 SemanticConversionTarget::RatioUnit(unit_name) => {
1821 if source_type.is_number() {
1822 let doc_name = &expr_source.doc_name;
1823 graph
1824 .resolved_types
1825 .get(doc_name)
1826 .and_then(|dt| dt.unit_index.get(unit_name).cloned())
1827 .unwrap_or_else(LemmaType::error_type)
1828 } else {
1829 source_type
1830 }
1831 }
1832 }
1833 }
1834
1835 ExpressionKind::MathematicalComputation(_, operand) => {
1836 let operand_type = infer_expression_type(operand, graph, computed_rule_types);
1837 if operand_type.is_error() {
1838 return LemmaType::error_type();
1839 }
1840 primitive_number().clone()
1841 }
1842
1843 ExpressionKind::Veto(_) => LemmaType::veto_type(),
1844 }
1845}
1846
1847fn infer_fact_type(fact_path: &FactPath, graph: &Graph) -> LemmaType {
1850 let entry = match graph.facts().get(fact_path) {
1851 Some(e) => e,
1852 None => return LemmaType::error_type(),
1853 };
1854 match entry {
1855 FactData::Value { value, .. } => value.lemma_type.clone(),
1856 FactData::TypeDeclaration { resolved_type, .. } => resolved_type.clone(),
1857 FactData::DocumentRef { .. } => LemmaType::error_type(),
1858 }
1859}
1860
1861fn engine_error_at(_graph: &Graph, source: &Source, message: impl Into<String>) -> LemmaError {
1867 LemmaError::engine(message.into(), Some(source.clone()), None::<String>)
1868}
1869
1870fn semantic_error_at(_graph: &Graph, source: &Source, message: impl Into<String>) -> LemmaError {
1872 LemmaError::semantic(message.into(), Some(source.clone()), None::<String>)
1873}
1874
1875fn check_logical_operands(
1877 left_type: &LemmaType,
1878 right_type: &LemmaType,
1879 graph: &Graph,
1880 source: &Source,
1881) -> Result<(), Vec<LemmaError>> {
1882 let mut errors = Vec::new();
1883 if !left_type.is_boolean() {
1884 errors.push(engine_error_at(
1885 graph,
1886 source,
1887 format!(
1888 "Logical operation requires boolean operands, got {:?} for left operand",
1889 left_type
1890 ),
1891 ));
1892 }
1893 if !right_type.is_boolean() {
1894 errors.push(engine_error_at(
1895 graph,
1896 source,
1897 format!(
1898 "Logical operation requires boolean operands, got {:?} for right operand",
1899 right_type
1900 ),
1901 ));
1902 }
1903 if errors.is_empty() {
1904 Ok(())
1905 } else {
1906 Err(errors)
1907 }
1908}
1909
1910fn check_logical_operand(
1912 operand_type: &LemmaType,
1913 graph: &Graph,
1914 source: &Source,
1915) -> Result<(), Vec<LemmaError>> {
1916 if !operand_type.is_boolean() {
1917 Err(vec![engine_error_at(
1918 graph,
1919 source,
1920 format!(
1921 "Logical negation requires boolean operand, got {:?}",
1922 operand_type
1923 ),
1924 )])
1925 } else {
1926 Ok(())
1927 }
1928}
1929
1930fn check_comparison_types(
1932 left_type: &LemmaType,
1933 op: &crate::ComparisonComputation,
1934 right_type: &LemmaType,
1935 graph: &Graph,
1936 source: &Source,
1937) -> Result<(), Vec<LemmaError>> {
1938 let is_equality_only = matches!(
1939 op,
1940 crate::ComparisonComputation::Equal
1941 | crate::ComparisonComputation::NotEqual
1942 | crate::ComparisonComputation::Is
1943 | crate::ComparisonComputation::IsNot
1944 );
1945
1946 if left_type.is_boolean() && right_type.is_boolean() {
1947 if !is_equality_only {
1948 return Err(vec![engine_error_at(
1949 graph,
1950 source,
1951 format!("Can only use == and != with booleans (got {})", op),
1952 )]);
1953 }
1954 return Ok(());
1955 }
1956
1957 if left_type.is_text() && right_type.is_text() {
1958 if !is_equality_only {
1959 return Err(vec![engine_error_at(
1960 graph,
1961 source,
1962 format!("Can only use == and != with text (got {})", op),
1963 )]);
1964 }
1965 return Ok(());
1966 }
1967
1968 if left_type.is_number() && right_type.is_number() {
1969 return Ok(());
1970 }
1971
1972 if left_type.is_ratio() && right_type.is_ratio() {
1973 return Ok(());
1974 }
1975
1976 if left_type.is_date() && right_type.is_date() {
1977 return Ok(());
1978 }
1979
1980 if left_type.is_time() && right_type.is_time() {
1981 return Ok(());
1982 }
1983
1984 if left_type.is_scale() && right_type.is_scale() {
1985 if !left_type.same_scale_family(right_type) {
1986 return Err(vec![engine_error_at(
1987 graph,
1988 source,
1989 format!(
1990 "Cannot compare different scale types: {} and {}",
1991 left_type.name(),
1992 right_type.name()
1993 ),
1994 )]);
1995 }
1996 return Ok(());
1997 }
1998
1999 if left_type.is_duration() && right_type.is_duration() {
2000 return Ok(());
2001 }
2002 if left_type.is_duration() && right_type.is_number() {
2003 return Ok(());
2004 }
2005 if left_type.is_number() && right_type.is_duration() {
2006 return Ok(());
2007 }
2008
2009 Err(vec![engine_error_at(
2010 graph,
2011 source,
2012 format!("Cannot compare {:?} with {:?}", left_type, right_type),
2013 )])
2014}
2015
2016fn check_arithmetic_types(
2019 left_type: &LemmaType,
2020 right_type: &LemmaType,
2021 operator: &ArithmeticComputation,
2022 graph: &Graph,
2023 source: &Source,
2024) -> Result<(), Vec<LemmaError>> {
2025 if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
2027 let both_temporal = (left_type.is_date() || left_type.is_time())
2028 && (right_type.is_date() || right_type.is_time());
2029 let one_is_duration = left_type.is_duration() || right_type.is_duration();
2030 let valid = matches!(
2031 operator,
2032 ArithmeticComputation::Add | ArithmeticComputation::Subtract
2033 ) && (both_temporal || one_is_duration);
2034 if !valid {
2035 return Err(vec![engine_error_at(
2036 graph,
2037 source,
2038 format!(
2039 "Cannot apply '{}' to {} and {}.",
2040 operator,
2041 left_type.name(),
2042 right_type.name()
2043 ),
2044 )]);
2045 }
2046 return Ok(());
2047 }
2048
2049 if left_type.is_scale() && right_type.is_scale() && !left_type.same_scale_family(right_type) {
2051 return Err(vec![engine_error_at(
2052 graph,
2053 source,
2054 format!(
2055 "Cannot {} different scale types: {} and {}. Operations between different scale types produce ambiguous result units.",
2056 match operator {
2057 ArithmeticComputation::Add => "add",
2058 ArithmeticComputation::Subtract => "subtract",
2059 ArithmeticComputation::Multiply => "multiply",
2060 ArithmeticComputation::Divide => "divide",
2061 ArithmeticComputation::Modulo => "modulo",
2062 ArithmeticComputation::Power => "power",
2063 },
2064 left_type.name(),
2065 right_type.name()
2066 ),
2067 )]);
2068 }
2069
2070 let left_valid = left_type.is_scale()
2072 || left_type.is_number()
2073 || left_type.is_duration()
2074 || left_type.is_ratio();
2075 let right_valid = right_type.is_scale()
2076 || right_type.is_number()
2077 || right_type.is_duration()
2078 || right_type.is_ratio();
2079
2080 if !left_valid || !right_valid {
2081 return Err(vec![engine_error_at(
2082 graph,
2083 source,
2084 format!(
2085 "Cannot apply '{}' to {} and {}.",
2086 operator,
2087 left_type.name(),
2088 right_type.name()
2089 ),
2090 )]);
2091 }
2092
2093 if left_type.has_same_base_type(right_type) {
2095 return Ok(());
2096 }
2097
2098 let pair = |a: fn(&LemmaType) -> bool, b: fn(&LemmaType) -> bool| {
2099 (a(left_type) && b(right_type)) || (b(left_type) && a(right_type))
2100 };
2101
2102 let allowed = match operator {
2103 ArithmeticComputation::Multiply => {
2104 pair(LemmaType::is_scale, LemmaType::is_number)
2105 || pair(LemmaType::is_scale, LemmaType::is_ratio)
2106 || pair(LemmaType::is_scale, LemmaType::is_duration)
2107 || pair(LemmaType::is_duration, LemmaType::is_number)
2108 || pair(LemmaType::is_duration, LemmaType::is_ratio)
2109 || pair(LemmaType::is_number, LemmaType::is_ratio)
2110 }
2111 ArithmeticComputation::Divide => {
2112 pair(LemmaType::is_scale, LemmaType::is_number)
2113 || pair(LemmaType::is_scale, LemmaType::is_ratio)
2114 || pair(LemmaType::is_scale, LemmaType::is_duration)
2115 || (left_type.is_duration() && right_type.is_number())
2116 || (left_type.is_duration() && right_type.is_ratio())
2117 || pair(LemmaType::is_number, LemmaType::is_ratio)
2118 }
2119 ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
2120 pair(LemmaType::is_scale, LemmaType::is_number)
2121 || pair(LemmaType::is_scale, LemmaType::is_ratio)
2122 || pair(LemmaType::is_duration, LemmaType::is_number)
2123 || pair(LemmaType::is_duration, LemmaType::is_ratio)
2124 || pair(LemmaType::is_number, LemmaType::is_ratio)
2125 }
2126 ArithmeticComputation::Power => {
2127 (left_type.is_number()
2128 || left_type.is_scale()
2129 || left_type.is_ratio()
2130 || left_type.is_duration())
2131 && (right_type.is_number() || right_type.is_ratio())
2132 }
2133 ArithmeticComputation::Modulo => right_type.is_number() || right_type.is_ratio(),
2134 };
2135
2136 if !allowed {
2137 return Err(vec![engine_error_at(
2138 graph,
2139 source,
2140 format!(
2141 "Cannot apply '{}' to {} and {}.",
2142 operator,
2143 left_type.name(),
2144 right_type.name(),
2145 ),
2146 )]);
2147 }
2148
2149 Ok(())
2150}
2151
2152fn check_unit_conversion_types(
2154 source_type: &LemmaType,
2155 target: &SemanticConversionTarget,
2156 graph: &Graph,
2157 source: &Source,
2158) -> Result<(), Vec<LemmaError>> {
2159 match target {
2160 SemanticConversionTarget::ScaleUnit(unit_name)
2161 | SemanticConversionTarget::RatioUnit(unit_name) => {
2162 let unit_check: Option<(bool, Vec<&str>)> = match (&source_type.specifications, target)
2163 {
2164 (
2165 TypeSpecification::Scale { units, .. },
2166 SemanticConversionTarget::ScaleUnit(_),
2167 ) => {
2168 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2169 let found = units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name));
2170 Some((found, valid))
2171 }
2172 (
2173 TypeSpecification::Ratio { units, .. },
2174 SemanticConversionTarget::RatioUnit(_),
2175 ) => {
2176 let valid: Vec<&str> = units.iter().map(|u| u.name.as_str()).collect();
2177 let found = units.iter().any(|u| u.name.eq_ignore_ascii_case(unit_name));
2178 Some((found, valid))
2179 }
2180 _ => None,
2181 };
2182
2183 match unit_check {
2184 Some((true, _)) => Ok(()),
2185 Some((false, valid)) => Err(vec![engine_error_at(
2186 graph,
2187 source,
2188 format!(
2189 "Unknown unit '{}' for type {}. Valid units: {}",
2190 unit_name,
2191 source_type.name(),
2192 valid.join(", ")
2193 ),
2194 )]),
2195 None if source_type.is_number() => {
2196 if graph
2197 .resolved_types
2198 .get(&source.doc_name)
2199 .and_then(|dt| dt.unit_index.get(unit_name))
2200 .is_none()
2201 {
2202 Err(vec![engine_error_at(
2203 graph,
2204 source,
2205 format!(
2206 "Unknown unit '{}' in document '{}'.",
2207 unit_name, source.doc_name
2208 ),
2209 )])
2210 } else {
2211 Ok(())
2212 }
2213 }
2214 None => Err(vec![engine_error_at(
2215 graph,
2216 source,
2217 format!(
2218 "Cannot convert {} to unit '{}'.",
2219 source_type.name(),
2220 unit_name
2221 ),
2222 )]),
2223 }
2224 }
2225 SemanticConversionTarget::Duration(_) => {
2226 if !source_type.is_duration() && !source_type.is_numeric() {
2227 Err(vec![engine_error_at(
2228 graph,
2229 source,
2230 format!("Cannot convert {} to duration.", source_type.name()),
2231 )])
2232 } else {
2233 Ok(())
2234 }
2235 }
2236 }
2237}
2238
2239fn check_mathematical_operand(
2241 operand_type: &LemmaType,
2242 graph: &Graph,
2243 source: &Source,
2244) -> Result<(), Vec<LemmaError>> {
2245 if !operand_type.is_scale() && !operand_type.is_number() {
2246 Err(vec![engine_error_at(
2247 graph,
2248 source,
2249 format!(
2250 "Mathematical function requires numeric operand (scale or number), got {:?}",
2251 operand_type
2252 ),
2253 )])
2254 } else {
2255 Ok(())
2256 }
2257}
2258
2259fn check_all_rule_references_exist(graph: &Graph) -> Result<(), Vec<LemmaError>> {
2261 let mut errors = Vec::new();
2262 let existing_rules: HashSet<&RulePath> = graph.rules().keys().collect();
2263 for (rule_path, rule_node) in graph.rules() {
2264 for dependency in &rule_node.depends_on_rules {
2265 if !existing_rules.contains(dependency) {
2266 errors.push(engine_error_at(
2267 graph,
2268 &rule_node.source,
2269 format!(
2270 "Rule '{}' references non-existent rule '{}'",
2271 rule_path.rule, dependency.rule
2272 ),
2273 ));
2274 }
2275 }
2276 }
2277 if errors.is_empty() {
2278 Ok(())
2279 } else {
2280 Err(errors)
2281 }
2282}
2283
2284fn check_fact_and_rule_name_collisions(graph: &Graph) -> Result<(), Vec<LemmaError>> {
2286 let mut errors = Vec::new();
2287 for rule_path in graph.rules().keys() {
2288 let fact_path = FactPath::new(rule_path.segments.clone(), rule_path.rule.clone());
2289 if graph.facts().contains_key(&fact_path) {
2290 let rule_node = graph.rules().get(rule_path).unwrap_or_else(|| {
2291 unreachable!(
2292 "BUG: rule '{}' missing from graph while validating name collisions",
2293 rule_path.rule
2294 )
2295 });
2296 errors.push(engine_error_at(
2297 graph,
2298 &rule_node.source,
2299 format!(
2300 "Name collision: '{}' is defined as both a fact and a rule",
2301 fact_path
2302 ),
2303 ));
2304 }
2305 }
2306 if errors.is_empty() {
2307 Ok(())
2308 } else {
2309 Err(errors)
2310 }
2311}
2312
2313fn check_fact_reference(
2316 fact_path: &FactPath,
2317 graph: &Graph,
2318 fact_source: &Source,
2319) -> Result<(), Vec<LemmaError>> {
2320 let entry = match graph.facts().get(fact_path) {
2321 Some(e) => e,
2322 None => {
2323 let maybe_rule_path = RulePath {
2324 segments: fact_path.segments.clone(),
2325 rule: fact_path.fact.clone(),
2326 };
2327
2328 if graph.rules().contains_key(&maybe_rule_path) {
2329 return Err(vec![semantic_error_at(
2330 graph,
2331 fact_source,
2332 format!(
2333 "Rule reference '{}' must use '?' (did you mean '{}?')",
2334 fact_path, fact_path
2335 ),
2336 )]);
2337 } else {
2338 return Err(vec![semantic_error_at(
2339 graph,
2340 fact_source,
2341 format!("Unknown fact reference '{}'", fact_path),
2342 )]);
2343 }
2344 }
2345 };
2346 match entry {
2347 FactData::Value { .. } | FactData::TypeDeclaration { .. } => Ok(()),
2348 FactData::DocumentRef { .. } => Err(vec![engine_error_at(
2349 graph,
2350 entry.source(),
2351 format!(
2352 "Cannot compute type for document reference fact '{}'",
2353 fact_path
2354 ),
2355 )]),
2356 }
2357}
2358
2359fn check_expression(
2363 expression: &Expression,
2364 graph: &Graph,
2365 inferred_types: &HashMap<RulePath, LemmaType>,
2366) -> Result<(), Vec<LemmaError>> {
2367 let mut errors = Vec::new();
2368
2369 let collect = |result: Result<(), Vec<LemmaError>>, errors: &mut Vec<LemmaError>| {
2370 if let Err(errs) = result {
2371 errors.extend(errs);
2372 }
2373 };
2374
2375 match &expression.kind {
2376 ExpressionKind::Literal(_) => {}
2377
2378 ExpressionKind::FactPath(fact_path) => {
2379 let fact_source = expression
2380 .source_location
2381 .as_ref()
2382 .expect("BUG: expression missing source in check_expression");
2383 collect(
2384 check_fact_reference(fact_path, graph, fact_source),
2385 &mut errors,
2386 );
2387 }
2388
2389 ExpressionKind::RulePath(_) => {}
2390
2391 ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
2392 collect(check_expression(left, graph, inferred_types), &mut errors);
2393 collect(check_expression(right, graph, inferred_types), &mut errors);
2394
2395 let left_type = infer_expression_type(left, graph, inferred_types);
2396 let right_type = infer_expression_type(right, graph, inferred_types);
2397 if !left_type.is_error() && !right_type.is_error() {
2398 let expr_source = expression
2399 .source_location
2400 .as_ref()
2401 .expect("BUG: expression missing source in check_expression");
2402 collect(
2403 check_logical_operands(&left_type, &right_type, graph, expr_source),
2404 &mut errors,
2405 );
2406 }
2407 }
2408
2409 ExpressionKind::LogicalNegation(operand, _) => {
2410 collect(
2411 check_expression(operand, graph, inferred_types),
2412 &mut errors,
2413 );
2414
2415 let operand_type = infer_expression_type(operand, graph, inferred_types);
2416 if !operand_type.is_error() {
2417 let expr_source = expression
2418 .source_location
2419 .as_ref()
2420 .expect("BUG: expression missing source in check_expression");
2421 collect(
2422 check_logical_operand(&operand_type, graph, expr_source),
2423 &mut errors,
2424 );
2425 }
2426 }
2427
2428 ExpressionKind::Comparison(left, op, right) => {
2429 collect(check_expression(left, graph, inferred_types), &mut errors);
2430 collect(check_expression(right, graph, inferred_types), &mut errors);
2431
2432 let left_type = infer_expression_type(left, graph, inferred_types);
2433 let right_type = infer_expression_type(right, graph, inferred_types);
2434 if !left_type.is_error() && !right_type.is_error() {
2435 let expr_source = expression
2436 .source_location
2437 .as_ref()
2438 .expect("BUG: expression missing source in check_expression");
2439 collect(
2440 check_comparison_types(&left_type, op, &right_type, graph, expr_source),
2441 &mut errors,
2442 );
2443 }
2444 }
2445
2446 ExpressionKind::Arithmetic(left, operator, right) => {
2447 collect(check_expression(left, graph, inferred_types), &mut errors);
2448 collect(check_expression(right, graph, inferred_types), &mut errors);
2449
2450 let left_type = infer_expression_type(left, graph, inferred_types);
2451 let right_type = infer_expression_type(right, graph, inferred_types);
2452 if !left_type.is_error() && !right_type.is_error() {
2453 let expr_source = expression
2454 .source_location
2455 .as_ref()
2456 .expect("BUG: expression missing source in check_expression");
2457 collect(
2458 check_arithmetic_types(&left_type, &right_type, operator, graph, expr_source),
2459 &mut errors,
2460 );
2461 }
2462 }
2463
2464 ExpressionKind::UnitConversion(source_expression, target) => {
2465 collect(
2466 check_expression(source_expression, graph, inferred_types),
2467 &mut errors,
2468 );
2469
2470 let source_type = infer_expression_type(source_expression, graph, inferred_types);
2471 if !source_type.is_error() {
2472 let expr_source = expression
2473 .source_location
2474 .as_ref()
2475 .expect("BUG: expression missing source in check_expression");
2476 collect(
2477 check_unit_conversion_types(&source_type, target, graph, expr_source),
2478 &mut errors,
2479 );
2480
2481 if source_type.is_number() {
2483 match target {
2484 SemanticConversionTarget::ScaleUnit(unit_name)
2485 | SemanticConversionTarget::RatioUnit(unit_name) => {
2486 if graph
2487 .resolved_types
2488 .get(&expr_source.doc_name)
2489 .and_then(|dt| dt.unit_index.get(unit_name))
2490 .is_none()
2491 {
2492 errors.push(engine_error_at(
2493 graph,
2494 expr_source,
2495 format!(
2496 "Cannot resolve unit '{}' for document '{}' (types may not have been resolved)",
2497 unit_name,
2498 expr_source.doc_name
2499 ),
2500 ));
2501 }
2502 }
2503 SemanticConversionTarget::Duration(_) => {}
2504 }
2505 }
2506 }
2507 }
2508
2509 ExpressionKind::MathematicalComputation(_, operand) => {
2510 collect(
2511 check_expression(operand, graph, inferred_types),
2512 &mut errors,
2513 );
2514
2515 let operand_type = infer_expression_type(operand, graph, inferred_types);
2516 if !operand_type.is_error() {
2517 let expr_source = expression
2518 .source_location
2519 .as_ref()
2520 .expect("BUG: expression missing source in check_expression");
2521 collect(
2522 check_mathematical_operand(&operand_type, graph, expr_source),
2523 &mut errors,
2524 );
2525 }
2526 }
2527
2528 ExpressionKind::Veto(_) => {}
2529 }
2530
2531 if errors.is_empty() {
2532 Ok(())
2533 } else {
2534 Err(errors)
2535 }
2536}
2537
2538fn check_rule_types(
2544 graph: &Graph,
2545 execution_order: &[RulePath],
2546 inferred_types: &HashMap<RulePath, LemmaType>,
2547) -> Result<(), Vec<LemmaError>> {
2548 let mut errors = Vec::new();
2549
2550 let collect = |result: Result<(), Vec<LemmaError>>, errors: &mut Vec<LemmaError>| {
2551 if let Err(errs) = result {
2552 errors.extend(errs);
2553 }
2554 };
2555
2556 for rule_path in execution_order {
2557 let branches = {
2558 let rule_node = match graph.rules().get(rule_path) {
2559 Some(node) => node,
2560 None => continue,
2561 };
2562 rule_node.branches.clone()
2563 };
2564
2565 if branches.is_empty() {
2566 continue;
2567 }
2568
2569 let (_, default_result) = &branches[0];
2570 collect(
2571 check_expression(default_result, graph, inferred_types),
2572 &mut errors,
2573 );
2574 let default_type = infer_expression_type(default_result, graph, inferred_types);
2575
2576 let mut non_veto_type: Option<LemmaType> = None;
2577 if !default_type.is_veto() && !default_type.is_error() {
2578 non_veto_type = Some(default_type.clone());
2579 }
2580
2581 for (branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
2582 if let Some(condition_expression) = condition {
2583 collect(
2584 check_expression(condition_expression, graph, inferred_types),
2585 &mut errors,
2586 );
2587 let condition_type =
2588 infer_expression_type(condition_expression, graph, inferred_types);
2589 if !condition_type.is_boolean() && !condition_type.is_error() {
2590 let condition_source = condition_expression
2591 .source_location
2592 .as_ref()
2593 .expect("BUG: condition expression missing source in check_rule_types");
2594 errors.push(engine_error_at(
2595 graph,
2596 condition_source,
2597 format!(
2598 "Unless clause condition in rule '{}' must be boolean, got {:?}",
2599 rule_path.rule, condition_type
2600 ),
2601 ));
2602 }
2603 }
2604
2605 collect(check_expression(result, graph, inferred_types), &mut errors);
2606 let result_type = infer_expression_type(result, graph, inferred_types);
2607
2608 if !result_type.is_veto() && !result_type.is_error() {
2609 if non_veto_type.is_none() {
2610 non_veto_type = Some(result_type.clone());
2611 } else if let Some(ref existing_type) = non_veto_type {
2612 if !existing_type.has_same_base_type(&result_type) {
2613 let Some(rule_node) = graph.rules().get(rule_path) else {
2614 unreachable!(
2615 "BUG: rule type validation referenced missing rule '{}'",
2616 rule_path.rule
2617 );
2618 };
2619 let rule_source = &rule_node.source;
2620 let default_expr = &branches[0].1;
2621
2622 let mut location_parts = vec![format!(
2623 "{}:{}:{}",
2624 rule_source.attribute, rule_source.span.line, rule_source.span.col
2625 )];
2626
2627 if let Some(loc) = &default_expr.source_location {
2628 location_parts.push(format!(
2629 "default branch at {}:{}:{}",
2630 loc.attribute, loc.span.line, loc.span.col
2631 ));
2632 }
2633 if let Some(loc) = &result.source_location {
2634 location_parts.push(format!(
2635 "unless clause {} at {}:{}:{}",
2636 branch_index, loc.attribute, loc.span.line, loc.span.col
2637 ));
2638 }
2639
2640 errors.push(LemmaError::semantic(
2641 format!("Type mismatch in rule '{}' in document '{}' ({}): default branch returns {}, but unless clause {} returns {}. All branches must return the same primitive type.",
2642 rule_path.rule,
2643 rule_source.doc_name,
2644 location_parts.join(", "),
2645 existing_type.name(),
2646 branch_index,
2647 result_type.name()),
2648 Some(rule_source.clone()),
2649 None::<String>,
2650 ));
2651 }
2652 }
2653 }
2654 }
2655 }
2656
2657 if errors.is_empty() {
2658 Ok(())
2659 } else {
2660 Err(errors)
2661 }
2662}
2663
2664fn apply_inferred_types(graph: &mut Graph, inferred_types: HashMap<RulePath, LemmaType>) {
2672 for (rule_path, rule_type) in inferred_types {
2673 if let Some(rule_node) = graph.rules_mut().get_mut(&rule_path) {
2674 rule_node.rule_type = rule_type;
2675 }
2676 }
2677}
2678
2679fn infer_rule_types(graph: &Graph, execution_order: &[RulePath]) -> HashMap<RulePath, LemmaType> {
2683 let mut computed_types: HashMap<RulePath, LemmaType> = HashMap::new();
2684
2685 for rule_path in execution_order {
2686 let branches = {
2687 let rule_node = match graph.rules().get(rule_path) {
2688 Some(node) => node,
2689 None => continue,
2690 };
2691 rule_node.branches.clone()
2692 };
2693
2694 if branches.is_empty() {
2695 continue;
2696 }
2697
2698 let (_, default_result) = &branches[0];
2699 let default_type = infer_expression_type(default_result, graph, &computed_types);
2700
2701 let mut non_veto_type: Option<LemmaType> = None;
2702 if !default_type.is_veto() && !default_type.is_error() {
2703 non_veto_type = Some(default_type.clone());
2704 }
2705
2706 for (_branch_index, (condition, result)) in branches.iter().enumerate().skip(1) {
2707 if let Some(condition_expression) = condition {
2708 let _condition_type =
2709 infer_expression_type(condition_expression, graph, &computed_types);
2710 }
2711
2712 let result_type = infer_expression_type(result, graph, &computed_types);
2713 if !result_type.is_veto() && !result_type.is_error() && non_veto_type.is_none() {
2714 non_veto_type = Some(result_type.clone());
2715 }
2716 }
2717
2718 let rule_type = non_veto_type.unwrap_or_else(LemmaType::veto_type);
2719 computed_types.insert(rule_path.clone(), rule_type);
2720 }
2721
2722 computed_types
2723}
2724
2725fn compute_referenced_rules_by_path(graph: &Graph) -> HashMap<Vec<String>, HashSet<String>> {
2726 let mut referenced_rules: HashMap<Vec<String>, HashSet<String>> = HashMap::new();
2727 for rule_node in graph.rules().values() {
2728 for rule_dependency in &rule_node.depends_on_rules {
2729 if !rule_dependency.segments.is_empty() {
2730 let path: Vec<String> = rule_dependency
2731 .segments
2732 .iter()
2733 .map(|segment| segment.fact.clone())
2734 .collect();
2735 referenced_rules
2736 .entry(path)
2737 .or_default()
2738 .insert(rule_dependency.rule.clone());
2739 }
2740 }
2741 }
2742 referenced_rules
2743}
2744
2745#[cfg(test)]
2746mod tests {
2747 use super::*;
2748
2749 use crate::parsing::ast::{BooleanValue, FactReference, RuleReference, Span, Value};
2750
2751 fn test_source() -> Source {
2752 Source::new(
2753 "test.lemma",
2754 Span {
2755 start: 0,
2756 end: 0,
2757 line: 1,
2758 col: 0,
2759 },
2760 "test",
2761 Arc::from("doc test\nfact x = 1\nrule result = x"),
2762 )
2763 }
2764
2765 fn test_sources() -> HashMap<String, String> {
2766 let mut sources = HashMap::new();
2767 sources.insert("test.lemma".to_string(), "doc test\n".to_string());
2768 sources
2769 }
2770
2771 fn build_graph(
2773 main_doc: &LemmaDoc,
2774 all_docs: &[LemmaDoc],
2775 sources: HashMap<String, String>,
2776 ) -> Result<Graph, Vec<LemmaError>> {
2777 let (prepared, type_errors) = Graph::prepare_types(all_docs, &sources);
2778 match Graph::build(main_doc, all_docs, sources, &prepared) {
2779 Ok(graph) => {
2780 if type_errors.is_empty() {
2781 Ok(graph)
2782 } else {
2783 Err(type_errors)
2784 }
2785 }
2786 Err(mut doc_errors) => {
2787 let mut all_errors = type_errors;
2788 all_errors.append(&mut doc_errors);
2789 Err(all_errors)
2790 }
2791 }
2792 }
2793
2794 fn create_test_doc(name: &str) -> LemmaDoc {
2795 LemmaDoc::new(name.to_string())
2796 }
2797
2798 fn create_literal_fact(name: &str, value: Value) -> LemmaFact {
2799 LemmaFact {
2800 reference: FactReference {
2801 segments: Vec::new(),
2802 fact: name.to_string(),
2803 },
2804 value: ParsedFactValue::Literal(value),
2805 source_location: test_source(),
2806 }
2807 }
2808
2809 fn create_literal_expr(value: Value) -> ast::Expression {
2810 ast::Expression {
2811 kind: ast::ExpressionKind::Literal(value),
2812 source_location: Some(test_source()),
2813 }
2814 }
2815
2816 #[test]
2817 fn test_build_simple_graph() {
2818 let mut doc = create_test_doc("test");
2819 doc = doc.add_fact(create_literal_fact(
2820 "age",
2821 Value::Number(rust_decimal::Decimal::from(25)),
2822 ));
2823 doc = doc.add_fact(create_literal_fact("name", Value::Text("John".to_string())));
2824
2825 let result = build_graph(&doc, &[doc.clone()], test_sources());
2826 assert!(result.is_ok(), "Should build graph successfully");
2827
2828 let graph = result.unwrap();
2829 assert_eq!(graph.facts().len(), 2);
2830 assert_eq!(graph.rules().len(), 0);
2831 }
2832
2833 #[test]
2834 fn test_build_graph_with_rule() {
2835 let mut doc = create_test_doc("test");
2836 doc = doc.add_fact(create_literal_fact(
2837 "age",
2838 Value::Number(rust_decimal::Decimal::from(25)),
2839 ));
2840
2841 let age_expr = ast::Expression {
2842 kind: ast::ExpressionKind::FactReference(FactReference {
2843 segments: Vec::new(),
2844 fact: "age".to_string(),
2845 }),
2846 source_location: Some(test_source()),
2847 };
2848
2849 let rule = LemmaRule {
2850 name: "is_adult".to_string(),
2851 expression: age_expr,
2852 unless_clauses: Vec::new(),
2853 source_location: test_source(),
2854 };
2855 doc = doc.add_rule(rule);
2856
2857 let result = build_graph(&doc, &[doc.clone()], test_sources());
2858 assert!(result.is_ok(), "Should build graph successfully");
2859
2860 let graph = result.unwrap();
2861 assert_eq!(graph.facts().len(), 1);
2862 assert_eq!(graph.rules().len(), 1);
2863 }
2864
2865 #[test]
2866 fn should_reject_fact_binding_into_non_document_fact() {
2867 let mut doc = create_test_doc("test");
2872 doc = doc.add_fact(create_literal_fact("x", Value::Number(1.into())));
2873
2874 doc = doc.add_fact(LemmaFact {
2876 reference: FactReference::from_path(vec!["x".to_string(), "y".to_string()]),
2877 value: ParsedFactValue::Literal(Value::Number(2.into())),
2878 source_location: test_source(),
2879 });
2880
2881 let result = build_graph(&doc, &[doc.clone()], test_sources());
2882 assert!(
2883 result.is_err(),
2884 "Overriding x.y must fail when x is not a document reference"
2885 );
2886 }
2887
2888 #[test]
2889 fn should_reject_fact_and_rule_name_collision() {
2890 let mut doc = create_test_doc("test");
2895 doc = doc.add_fact(create_literal_fact("x", Value::Number(1.into())));
2896 doc = doc.add_rule(LemmaRule {
2897 name: "x".to_string(),
2898 expression: create_literal_expr(Value::Number(2.into())),
2899 unless_clauses: Vec::new(),
2900 source_location: test_source(),
2901 });
2902
2903 let result = build_graph(&doc, &[doc.clone()], test_sources());
2904 assert!(
2905 result.is_err(),
2906 "Fact and rule name collisions should be rejected"
2907 );
2908 }
2909
2910 #[test]
2911 fn test_duplicate_fact() {
2912 let mut doc = create_test_doc("test");
2913 doc = doc.add_fact(create_literal_fact(
2914 "age",
2915 Value::Number(rust_decimal::Decimal::from(25)),
2916 ));
2917 doc = doc.add_fact(create_literal_fact(
2918 "age",
2919 Value::Number(rust_decimal::Decimal::from(30)),
2920 ));
2921
2922 let result = build_graph(&doc, &[doc.clone()], test_sources());
2923 assert!(result.is_err(), "Should detect duplicate fact");
2924
2925 let errors = result.unwrap_err();
2926 assert!(errors
2927 .iter()
2928 .any(|e| e.to_string().contains("Duplicate fact") && e.to_string().contains("age")));
2929 }
2930
2931 #[test]
2932 fn test_duplicate_rule() {
2933 let mut doc = create_test_doc("test");
2934
2935 let rule1 = LemmaRule {
2936 name: "test_rule".to_string(),
2937 expression: create_literal_expr(Value::Boolean(BooleanValue::True)),
2938 unless_clauses: Vec::new(),
2939 source_location: test_source(),
2940 };
2941 let rule2 = LemmaRule {
2942 name: "test_rule".to_string(),
2943 expression: create_literal_expr(Value::Boolean(BooleanValue::False)),
2944 unless_clauses: Vec::new(),
2945 source_location: test_source(),
2946 };
2947
2948 doc = doc.add_rule(rule1);
2949 doc = doc.add_rule(rule2);
2950
2951 let result = build_graph(&doc, &[doc.clone()], test_sources());
2952 assert!(result.is_err(), "Should detect duplicate rule");
2953
2954 let errors = result.unwrap_err();
2955 assert!(errors.iter().any(
2956 |e| e.to_string().contains("Duplicate rule") && e.to_string().contains("test_rule")
2957 ));
2958 }
2959
2960 #[test]
2961 fn test_missing_fact_reference() {
2962 let mut doc = create_test_doc("test");
2963
2964 let missing_fact_expr = ast::Expression {
2965 kind: ast::ExpressionKind::FactReference(FactReference {
2966 segments: Vec::new(),
2967 fact: "nonexistent".to_string(),
2968 }),
2969 source_location: Some(test_source()),
2970 };
2971
2972 let rule = LemmaRule {
2973 name: "test_rule".to_string(),
2974 expression: missing_fact_expr,
2975 unless_clauses: Vec::new(),
2976 source_location: test_source(),
2977 };
2978 doc = doc.add_rule(rule);
2979
2980 let result = build_graph(&doc, &[doc.clone()], test_sources());
2981 assert!(result.is_err(), "Should detect missing fact");
2982
2983 let errors = result.unwrap_err();
2984 assert!(errors
2985 .iter()
2986 .any(|e| e.to_string().contains("Fact 'nonexistent' not found")));
2987 }
2988
2989 #[test]
2990 fn test_missing_document_reference() {
2991 let mut doc = create_test_doc("test");
2992
2993 let fact = LemmaFact {
2994 reference: FactReference {
2995 segments: Vec::new(),
2996 fact: "contract".to_string(),
2997 },
2998 value: ParsedFactValue::DocumentReference(crate::DocRef::local("nonexistent")),
2999 source_location: test_source(),
3000 };
3001 doc = doc.add_fact(fact);
3002
3003 let result = build_graph(&doc, &[doc.clone()], test_sources());
3004 assert!(result.is_err(), "Should detect missing document");
3005
3006 let errors = result.unwrap_err();
3007 assert!(errors
3008 .iter()
3009 .any(|e| e.to_string().contains("Document 'nonexistent' not found")));
3010 }
3011
3012 #[test]
3013 fn test_fact_reference_conversion() {
3014 let mut doc = create_test_doc("test");
3015 doc = doc.add_fact(create_literal_fact(
3016 "age",
3017 Value::Number(rust_decimal::Decimal::from(25)),
3018 ));
3019
3020 let age_expr = ast::Expression {
3021 kind: ast::ExpressionKind::FactReference(FactReference {
3022 segments: Vec::new(),
3023 fact: "age".to_string(),
3024 }),
3025 source_location: Some(test_source()),
3026 };
3027
3028 let rule = LemmaRule {
3029 name: "test_rule".to_string(),
3030 expression: age_expr,
3031 unless_clauses: Vec::new(),
3032 source_location: test_source(),
3033 };
3034 doc = doc.add_rule(rule);
3035
3036 let result = build_graph(&doc, &[doc.clone()], test_sources());
3037 assert!(result.is_ok(), "Should build graph successfully");
3038
3039 let graph = result.unwrap();
3040 let rule_node = graph.rules().values().next().unwrap();
3041
3042 assert!(matches!(
3043 rule_node.branches[0].1.kind,
3044 ExpressionKind::FactPath(_)
3045 ));
3046 }
3047
3048 #[test]
3049 fn test_rule_reference_conversion() {
3050 let mut doc = create_test_doc("test");
3051
3052 let rule1_expr = ast::Expression {
3053 kind: ast::ExpressionKind::FactReference(FactReference {
3054 segments: Vec::new(),
3055 fact: "age".to_string(),
3056 }),
3057 source_location: Some(test_source()),
3058 };
3059
3060 let rule1 = LemmaRule {
3061 name: "rule1".to_string(),
3062 expression: rule1_expr,
3063 unless_clauses: Vec::new(),
3064 source_location: test_source(),
3065 };
3066 doc = doc.add_rule(rule1);
3067
3068 let rule2_expr = ast::Expression {
3069 kind: ast::ExpressionKind::RuleReference(RuleReference {
3070 segments: Vec::new(),
3071 rule: "rule1".to_string(),
3072 }),
3073 source_location: Some(test_source()),
3074 };
3075
3076 let rule2 = LemmaRule {
3077 name: "rule2".to_string(),
3078 expression: rule2_expr,
3079 unless_clauses: Vec::new(),
3080 source_location: test_source(),
3081 };
3082 doc = doc.add_rule(rule2);
3083
3084 doc = doc.add_fact(create_literal_fact(
3085 "age",
3086 Value::Number(rust_decimal::Decimal::from(25)),
3087 ));
3088
3089 let result = build_graph(&doc, &[doc.clone()], test_sources());
3090 assert!(result.is_ok(), "Should build graph successfully");
3091
3092 let graph = result.unwrap();
3093 let rule2_node = graph
3094 .rules()
3095 .get(&RulePath {
3096 segments: Vec::new(),
3097 rule: "rule2".to_string(),
3098 })
3099 .unwrap();
3100
3101 assert_eq!(rule2_node.depends_on_rules.len(), 1);
3102 assert!(matches!(
3103 rule2_node.branches[0].1.kind,
3104 ExpressionKind::RulePath(_)
3105 ));
3106 }
3107
3108 #[test]
3109 fn test_collect_multiple_errors() {
3110 let mut doc = create_test_doc("test");
3111 doc = doc.add_fact(create_literal_fact(
3112 "age",
3113 Value::Number(rust_decimal::Decimal::from(25)),
3114 ));
3115 doc = doc.add_fact(create_literal_fact(
3116 "age",
3117 Value::Number(rust_decimal::Decimal::from(30)),
3118 ));
3119
3120 let missing_fact_expr = ast::Expression {
3121 kind: ast::ExpressionKind::FactReference(FactReference {
3122 segments: Vec::new(),
3123 fact: "nonexistent".to_string(),
3124 }),
3125 source_location: Some(test_source()),
3126 };
3127
3128 let rule = LemmaRule {
3129 name: "test_rule".to_string(),
3130 expression: missing_fact_expr,
3131 unless_clauses: Vec::new(),
3132 source_location: test_source(),
3133 };
3134 doc = doc.add_rule(rule);
3135
3136 let result = build_graph(&doc, &[doc.clone()], test_sources());
3137 assert!(result.is_err(), "Should collect multiple errors");
3138
3139 let errors = result.unwrap_err();
3140 assert!(errors.len() >= 2, "Should have at least 2 errors");
3141 assert!(errors
3142 .iter()
3143 .any(|e| e.to_string().contains("Duplicate fact")));
3144 assert!(errors
3145 .iter()
3146 .any(|e| e.to_string().contains("Fact 'nonexistent' not found")));
3147 }
3148
3149 #[test]
3150 fn test_type_registration_collects_multiple_errors() {
3151 use crate::parsing::ast::TypeDef;
3152
3153 let type_source = Source::new(
3154 "a.lemma",
3155 Span {
3156 start: 0,
3157 end: 0,
3158 line: 1,
3159 col: 0,
3160 },
3161 "doc_a",
3162 Arc::from("doc test\nfact x = 1\nrule result = x"),
3163 );
3164 let doc_a = create_test_doc("doc_a")
3165 .with_attribute("a.lemma".to_string())
3166 .add_type(TypeDef::Regular {
3167 source_location: type_source.clone(),
3168 name: "money".to_string(),
3169 parent: "number".to_string(),
3170 constraints: None,
3171 })
3172 .add_type(TypeDef::Regular {
3173 source_location: type_source,
3174 name: "money".to_string(),
3175 parent: "number".to_string(),
3176 constraints: None,
3177 });
3178
3179 let type_source_b = Source::new(
3180 "b.lemma",
3181 Span {
3182 start: 0,
3183 end: 0,
3184 line: 1,
3185 col: 0,
3186 },
3187 "doc_b",
3188 Arc::from("doc test\nfact x = 1\nrule result = x"),
3189 );
3190 let doc_b = create_test_doc("doc_b")
3191 .with_attribute("b.lemma".to_string())
3192 .add_type(TypeDef::Regular {
3193 source_location: type_source_b.clone(),
3194 name: "length".to_string(),
3195 parent: "number".to_string(),
3196 constraints: None,
3197 })
3198 .add_type(TypeDef::Regular {
3199 source_location: type_source_b,
3200 name: "length".to_string(),
3201 parent: "number".to_string(),
3202 constraints: None,
3203 });
3204
3205 let mut sources = HashMap::new();
3206 sources.insert(
3207 "a.lemma".to_string(),
3208 "doc doc_a\ntype money = number\ntype money = number".to_string(),
3209 );
3210 sources.insert(
3211 "b.lemma".to_string(),
3212 "doc doc_b\ntype length = number\ntype length = number".to_string(),
3213 );
3214
3215 let result = build_graph(&doc_a, &[doc_a.clone(), doc_b.clone()], sources);
3216 assert!(result.is_err(), "Should fail with duplicate type errors");
3217 let errors = result.unwrap_err();
3218 assert!(
3219 errors.len() >= 2,
3220 "Should collect duplicate type error from each document, got {}",
3221 errors.len()
3222 );
3223 assert!(
3224 errors
3225 .iter()
3226 .any(|e| e.to_string().contains("Type 'money' is already defined")),
3227 "Should report duplicate 'money' in doc_a: {:?}",
3228 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
3229 );
3230 assert!(
3231 errors
3232 .iter()
3233 .any(|e| e.to_string().contains("Type 'length' is already defined")),
3234 "Should report duplicate 'length' in doc_b: {:?}",
3235 errors.iter().map(|e| e.to_string()).collect::<Vec<_>>()
3236 );
3237 }
3238}