Skip to main content

lemma/planning/
graph.rs

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>, // Store all_docs for document traversal and context determination
23    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    /// Resolve a standard type by name (helper function)
56    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    /// Resolve a TypeDeclaration to a LemmaType
72    ///
73    /// This resolves both type references (e.g., [money]) and inline type definitions
74    /// (e.g., [money -> minimal 100] or [number -> minimal 100]) to their final LemmaType.
75    ///
76    /// # Arguments
77    /// * `type_decl` - The TypeDeclaration to resolve
78    /// * `decl_source` - The source location for error messages
79    /// * `context_doc` - The document context where this type is being used
80    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        // Get resolved types for the source document
96        // If 'from' is specified, resolve from that document; otherwise use context_doc
97        let source_doc = from.as_deref().unwrap_or(context_doc);
98
99        // Try to resolve as a standard type first (number, boolean, etc.)
100        let base_lemma_type = if let Some(specs) = Self::resolve_standard_type(base) {
101            // Standard type - create LemmaType without name
102            LemmaType::without_name(specs)
103        } else {
104            // Custom type - look up in resolved types
105            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        // Apply inline overrides if any
135        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        // Create final LemmaType
153        // For standard types, use without_name(); for custom types, preserve the name
154        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    /// First branch has condition=None (default expression), subsequent branches are unless clauses.
275    /// Expressions are already converted (Reference -> FactPath, RuleReference -> RulePath).
276    pub branches: Vec<(Option<Expression>, Expression)>,
277    pub source: Source,
278
279    pub depends_on_rules: HashSet<RulePath>,
280
281    /// Computed type of this rule's result (populated during validation)
282    /// Every rule MUST have a type (Lemma is strictly typed)
283    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        // Create and populate TypeRegistry
302        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        // Pre-resolve named types for every document up-front.
321        //
322        // Graph construction and execution-plan building may need to resolve types "from" other
323        // documents even if those documents are not reachable through document references.
324        //
325        // We only resolve *named* types here because inline type definitions are registered while
326        // traversing facts during graph building and must be resolved afterwards per document.
327        for doc in all_docs {
328            match type_registry.resolve_named_types(&doc.name) {
329                Ok(document_types) => {
330                    // Validate type specifications for all resolved named types
331                    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        // Validate and compute execution order
379        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                // Only use effective_doc_refs for the FIRST segment
480                // Subsequent segments use the actual document references from traversed documents
481                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        // Skip override facts - they are applied when the original fact is processed
526        // The override's value will be used instead of the original fact's value
527        // Don't build nested documents here - that happens when the base fact is processed
528        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        // Check for duplicates
538        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                // Check if there's an override for this literal fact
556                let effective_value = if let Some(overrides) =
557                    pending_overrides.get(&fact.reference.fact)
558                {
559                    // An override applies when we've traversed all its segments from the entry point
560                    // entry_depth + segments.len() == current_depth
561                    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                // Only register as inline type definition if we have 'from' OR 'overrides'
586                // If both are None, it's just a direct type reference [coffee], not an inline type definition
587                let is_inline_type_definition = from.is_some() || inline_overrides.is_some();
588
589                // Only register inline type definitions when processing the document directly,
590                // not when processing it as a nested reference. This prevents duplicate registrations
591                // and ensures literal overrides don't trigger type definition registration.
592                if is_inline_type_definition && current_segments.is_empty() {
593                    // Register inline type definition in TypeRegistry
594                    // Create a TypeDef for this inline type definition
595                    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                    // Register in the current document
610                    let doc_name = current_doc.name.clone();
611
612                    // Register the inline type definition
613                    if let Err(e) = type_registry.register_type(&doc_name, inline_type_def) {
614                        self.errors.push(e);
615                    }
616                }
617
618                // Check if there's an override for this type fact
619                let effective_value = if let Some(overrides) =
620                    pending_overrides.get(&fact.reference.fact)
621                {
622                    // An override applies when we've traversed all its segments from the entry point
623                    // entry_depth + segments.len() == current_depth
624                    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                // Check if there's an override for this document reference
652                let (effective_doc_name, effective_source) = if let Some(overrides) =
653                    pending_overrides.get(&fact.reference.fact)
654                {
655                    // An override applies when we've traversed all its segments from the entry point
656                    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                // Store the fact with the effective document reference
691                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                // Collect overrides for the nested document
699                // Each override is (fact, entry_depth) where entry_depth is when it was added
700                // Key by the next segment or fact name
701                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                            // Calculate how many segments we've traversed from entry point
707                            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                // Build nested document with the effective document
721                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        // Merge overrides with additional pending overrides from this document
747        // New overrides from this doc get entry_depth = current_segments.len()
748        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        // Build effective_facts_map with overridden values
761        // Key: fact name, Value: effective document name (for document references)
762        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                    // Check if there's an override for this fact
767                    // Override applies when entry_depth + segments.len() == current_depth
768                    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        // Original facts_map for basic lookups
795        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                &current_segments,
805                &pending_overrides,
806                doc,
807                type_registry,
808            );
809        }
810
811        // Resolve types for this document after all facts are registered
812        match type_registry.resolve_types(&doc.name) {
813            Ok(document_types) => {
814                // Validate type specifications for inline type definitions
815                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                // Always overwrite: inline type definitions may have been registered while processing facts.
838                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        // Process all rules (now has access to resolved types)
847        for rule in &doc.rules {
848            self.add_rule(
849                rule,
850                doc,
851                &facts_map,
852                &current_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(), // Initialized to veto_type; actual type computed in compute_all_rule_types during validation
932        };
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                // Convert Reference to FactReference and recurse
979                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                // Get resolved types for current document from self.resolved_types
1001                // Types must be resolved by this point (after facts, before rules)
1002                // Even empty documents get resolved types (with empty maps) - so get() should never fail
1003                let document_types = self.resolved_types.get(&current_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                // Lookup unit in unit_index
1011                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()), // Store the unit name with the value
1041                            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()), // Store the unit name with the value
1063                            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                // Validate that the referenced fact exists
1095                // For local facts (no segments), check current facts_map
1096                // For cross-document facts, the path segments validation already happened
1097                if fact_ref.segments.is_empty() && !facts_map.contains_key(&fact_ref.fact) {
1098                    // Check if this is actually a rule name - provide helpful error message
1099                    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        // Collect all non-Veto types from branches
1309        // Veto is a runtime exception, not a type that should affect the rule's type
1310        // If a branch returns Veto, it's handled at runtime, but the rule type is the non-Veto type
1311        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 we haven't seen a non-Veto type yet, store it
1348                // All non-Veto branches must have the same standard type (enforced by validate_branch_type_consistency)
1349                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                    // Check that this branch has the same standard type as the first non-veto type
1353                    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        // Every rule MUST have a type (Lemma is strictly typed)
1450        // If all branches return Veto, the rule type is Veto
1451        // Otherwise, use the first non-Veto type (typically the default branch)
1452        // All non-Veto branches must have the same type (enforced by validate_branch_type_consistency)
1453        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    // Boolean comparisons: only equality operators.
1649    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    // Text comparisons: only equality operators.
1662    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    // Numbers compare with numbers only.
1675    if left_type.is_number() && right_type.is_number() {
1676        return;
1677    }
1678
1679    // Ratios compare with ratios only.
1680    if left_type.is_ratio() && right_type.is_ratio() {
1681        return;
1682    }
1683
1684    // Dates compare with dates only.
1685    if left_type.is_date() && right_type.is_date() {
1686        return;
1687    }
1688
1689    // Times compare with times only.
1690    if left_type.is_time() && right_type.is_time() {
1691        return;
1692    }
1693
1694    // Scales compare with scales of the same scale type only.
1695    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    // Duration compares with duration and (for now) plain numbers.
1712    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    // Check for temporal arithmetic (Date/Time)
1739    if left_type.is_date() || left_type.is_time() || right_type.is_date() || right_type.is_time() {
1740        // Validate temporal arithmetic is supported
1741        // compute_temporal_arithmetic_result_type will return a fallback if unsupported
1742        // but we check here to provide a better error message
1743        let result = compute_temporal_arithmetic_result_type(left_type, right_type, operator);
1744        // If result is duration but operator is not Subtract/Add, it's invalid
1745        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    // CRITICAL: If both operands are different Scale types, reject ALL arithmetic operations
1765    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    // Check for valid arithmetic type combinations
1787    // Scale, Number, Ratio, and Duration can participate in arithmetic
1788    // but with specific constraints handled in validate_arithmetic_operator_constraints
1789    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                // Modulo: dividend % divisor
1850                // Dividend can be Scale or Number (custom or standard)
1851                // Divisor must be Number (dimensionless, not Scale)
1852                // Allow: Scale % Number → result is Scale
1853                // Allow: Number % Number → result is Number
1854                // Error: Scale % Scale (divisor must be dimensionless)
1855                // Error: Number % Scale (divisor must be dimensionless)
1856                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            // If right is Number, allow it (left can be Scale or Number)
1867        }
1868        ArithmeticComputation::Multiply | ArithmeticComputation::Divide => {
1869            // Multiply/Divide: Different Scale types are already rejected in validate_arithmetic_types
1870            // At this point, if both are Scale, they must be the same Scale type
1871
1872            // - Same standard type: allowed (Number * Number, Scale * Scale, Ratio * Ratio, etc.)
1873            // - Scale * Number, Number * Scale: allowed
1874            // - Scale * Ratio, Ratio * Scale: allowed
1875            // - Number * Ratio, Ratio * Number: allowed
1876            // - Duration * Number: allowed (Multiply only)
1877            // - Number * Duration: allowed (Multiply only)
1878            // - Duration / Number: allowed (Divide only)
1879            // - Number / Duration: NOT allowed
1880
1881            if !left_type.has_same_base_type(right_type) {
1882                // Check if Scale * Number or Number * Scale (allowed)
1883                let is_scale_number = (left_type.is_scale() && right_type.is_number())
1884                    || (left_type.is_number() && right_type.is_scale());
1885
1886                // Check if Scale * Ratio or Ratio * Scale (allowed)
1887                let is_scale_ratio = (left_type.is_scale() && right_type.is_ratio())
1888                    || (left_type.is_ratio() && right_type.is_scale());
1889
1890                // Check if Number * Ratio or Ratio * Number (allowed)
1891                let is_number_ratio = (left_type.is_number() && right_type.is_ratio())
1892                    || (left_type.is_ratio() && right_type.is_number());
1893
1894                // Check Duration combinations
1895                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                    // Duration * Number or Number * Duration: only Multiply is allowed
1900                    // Duration / Number: only Divide is allowed (when Duration is left)
1901                    // Number / Duration: NOT allowed
1902                    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                    // Otherwise, Duration * Number or Number * Duration (Multiply) or Duration / Number (Divide) are allowed
1914                } else if !is_scale_number && !is_scale_ratio && !is_number_ratio {
1915                    // Not the special case - types are incompatible
1916                    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                // Types have the same standard type - always allowed (even with different constraints)
1930            }
1931        }
1932        ArithmeticComputation::Add | ArithmeticComputation::Subtract => {
1933            // Different Scale types are already rejected in validate_arithmetic_types
1934            // At this point, if both are Scale, they must be the same Scale type
1935
1936            // - Same standard type: allowed (Number + Number, Scale + Scale, etc.) - even with different constraints
1937            // - Scale + Number: allowed (result is Scale)
1938            // - Number + Scale: allowed (result is Scale)
1939            // - Number + Ratio: allowed (result is Number with ratio semantics)
1940            // - Scale + Ratio: allowed (result is Scale with ratio semantics)
1941            if !left_type.has_same_base_type(right_type) {
1942                // Check if Scale + Number or Number + Scale (allowed)
1943                let is_scale_number = (left_type.is_scale() && right_type.is_number())
1944                    || (left_type.is_number() && right_type.is_scale());
1945
1946                // Check if Scale op Ratio or Ratio op Scale (allowed)
1947                let is_scale_ratio = (left_type.is_scale() && right_type.is_ratio())
1948                    || (left_type.is_ratio() && right_type.is_scale());
1949
1950                // Check if Number op Ratio or Ratio op Number (allowed with ratio semantics)
1951                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                    // Not the special case - types are incompatible
1956                    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                // Types have the same standard type - always allowed (even with different constraints)
1970            }
1971        }
1972        ArithmeticComputation::Power => {
1973            // Power: base ^ exponent
1974            // Base can be Scale or Number (custom or standard)
1975            // Exponent must be Number or Ratio (dimensionless, not Scale)
1976            // Allow: Scale ^ Number → result is Scale
1977            // Allow: Number ^ Number → result is Number
1978            // Error: Scale ^ Scale (exponent must be dimensionless)
1979            // Error: Number ^ Scale (exponent must be dimensionless)
1980            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            // If right is Number or Ratio, allow it (left can be Scale or Number)
1992        }
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            // Allow conversion from Scale/Number to compatible numeric types
2049            // Scale and Number are both numeric and can be converted to each other
2050            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    // Mathematical functions work on Scale and Number (not Ratio or Duration)
2072    // Both Scale and Number are numeric types suitable for mathematical operations
2073    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            // This can happen when a rule is referenced without `?` and ends up as a FactPath
2096            // (e.g. `employee.annual`). Do not panic: report a semantic error at the source span.
2097            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                // If it isn't a rule either, then this is user code referencing something
2117                // that doesn't exist. That's a semantic error.
2118                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            // Benign fallback type to avoid cascaded panics.
2130            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            // Use TypeRegistry to determine document context and resolve type
2137            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 inline type definitions, check if they exist in resolved_types
2143            // Inline type definitions are already fully resolved during type resolution, so just use them directly
2144            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                    // Inline type definition already resolved - return it directly
2147                    return resolved_type.clone();
2148                }
2149            }
2150
2151            // Find which document this fact belongs to
2152            // Use the document from the first segment (set during graph building)
2153            // This is more reliable than searching, especially for nested facts
2154            let context_doc: &str = if let Some(first_segment) = fact_path.segments.first() {
2155                // Use the document from the segment - this is set during graph building
2156                &first_segment.doc
2157            } else {
2158                // Top-level fact - try to find it by searching documents
2159                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 not found by searching, use the document from the fact's source_location
2175                // This is reliable since facts are always added from a specific document
2176                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            // Use Graph::resolve_type_declaration which uses TypeRegistry
2191            // For direct type references [coffee] (from=None, overrides=None), this looks up the named type directly
2192            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                    // For direct type references, we should get the actual named type back
2201                    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    // Handle Scale + Number or Number + Scale: result is Scale (Scale has units, Number doesn't)
2246    if left.is_scale() && right.is_number() {
2247        return left_type; // Scale + Number → Scale
2248    }
2249    if left.is_number() && right.is_scale() {
2250        return right_type; // Number + Scale → Scale
2251    }
2252
2253    // Handle Ratio operations
2254    // Ratio op Number or Number op Ratio → Number
2255    if left.is_ratio() && right.is_number() {
2256        return standard_number().clone(); // Ratio op Number → Number
2257    }
2258    if left.is_number() && right.is_ratio() {
2259        return standard_number().clone(); // Number op Ratio → Number
2260    }
2261    // Ratio op Ratio → Ratio
2262    if left.is_ratio() && right.is_ratio() {
2263        return left_type; // Ratio op Ratio → Ratio (preserve Ratio type)
2264    }
2265    // Ratio op Scale or Scale op Ratio → Scale
2266    if left.is_ratio() && right.is_scale() {
2267        return right_type; // Ratio op Scale → Scale
2268    }
2269    if left.is_scale() && right.is_ratio() {
2270        return left_type; // Scale op Ratio → Scale
2271    }
2272
2273    // Handle standard (no name) + custom (has name) case: result is the custom type
2274    // This handles: STANDARD_SCALE + custom_scale, STANDARD_NUMBER + custom_scale, etc.
2275    // ORDER DOES NOT MATTER for Add/Subtract/Multiply/Divide - both orders return the custom type
2276    // For Power/Modulo, validation ensures correct order (custom op standard)
2277    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        // One is standard, one is custom → result is the custom type (order-independent)
2281        // Return whichever operand is the custom type (has a name)
2282        if left_type.name.is_some() {
2283            return left_type;
2284        } else {
2285            return right_type;
2286        }
2287    }
2288
2289    // Both are numeric types, check if we can preserve custom type
2290    // If we reach here, validation should have ensured types are compatible
2291    if left.name.is_some() && right.name.is_some() {
2292        // Both are custom types
2293        // Different Scale types are already rejected in validate_arithmetic_types
2294        // But different custom Number types with same base are allowed
2295        // Return the left type (result type is left operand for same base operations)
2296        return left_type;
2297    }
2298
2299    // Both are standard types (both name.is_none()) - determine result type
2300    // Scale op Scale (same type) → Scale
2301    // Number op Number → Number
2302    // Scale op Number → Scale (handled above)
2303    // Number op Scale → Scale (handled above)
2304    if left.is_scale() && right.is_scale() {
2305        // Both are Scale - they must be the same type (validation ensures this)
2306        return left_type;
2307    }
2308    if left.is_number() && right.is_number() {
2309        // Both are Number
2310        return standard_number().clone();
2311    }
2312
2313    // Fallback (should not reach here if validation is correct)
2314    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            // Date - Date → Duration (supported)
2325            if left.is_date() && right.is_date() {
2326                return standard_duration().clone();
2327            }
2328            // Time - Time → Duration (supported)
2329            if left.is_time() && right.is_time() {
2330                return standard_duration().clone();
2331            }
2332            // Date - Time → Duration (supported: datetime - time = duration)
2333            if left.is_date() && right.is_time() {
2334                return standard_duration().clone();
2335            }
2336            // Time - Date → Duration (supported: time - datetime = duration)
2337            if left.is_time() && right.is_date() {
2338                return standard_duration().clone();
2339            }
2340            // Date - Duration → Date (supported)
2341            if left.is_date() && right.is_duration() {
2342                return left.clone();
2343            }
2344            // Time - Duration → Time (supported)
2345            if left.is_time() && right.is_duration() {
2346                return left.clone();
2347            }
2348        }
2349        ArithmeticComputation::Add => {
2350            // Date + Duration → Date (supported)
2351            if left.is_date() && right.is_duration() {
2352                return left.clone();
2353            }
2354            // Time + Duration → Time (supported)
2355            if left.is_time() && right.is_duration() {
2356                return left.clone();
2357            }
2358            // Duration + Date → Date (supported)
2359            if left.is_duration() && right.is_date() {
2360                return right.clone();
2361            }
2362            // Duration + Time → Time (supported)
2363            if left.is_duration() && right.is_time() {
2364                return right.clone();
2365            }
2366        }
2367        _ => {}
2368    }
2369    // Unsupported temporal arithmetic - validation should have caught this
2370    // Return fallback type (validation will fail due to errors vector)
2371    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 any fact path like `a.b.c`, each segment (`a`, `a.b`, ...) must be a document reference.
2395    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    // Also validate *syntactic override facts* from source documents.
2439    //
2440    // GraphBuilder intentionally skips registering override facts (facts with reference.segments),
2441    // so they are not present in `graph.facts()`. However, they are still part of the language and
2442    // must be validated. We validate by traversing document references through the source docs:
2443    // for an override `x.y.z = ...`, each segment (`x`, then `y`) must be a document reference in
2444    // the document reached after traversing the previous segment.
2445    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(&current_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 traversal succeeded, validate that we're not overriding a typed fact with a type definition
2528            if path_valid {
2529                if let Some(target_doc) = graph.all_docs.get(&current_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                        // Check if both original and override are type declarations
2534                        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    // Disallow fact/rule name collision in the same namespace (same traversal segments).
2565    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        // Higher-standard language rule:
2749        // if `x` is a literal (not a document reference), `x.y = ...` must be rejected.
2750        //
2751        // This is currently expected to FAIL until graph building enforces it consistently.
2752        let mut doc = create_test_doc("test");
2753        doc = doc.add_fact(create_literal_fact("x", LiteralValue::number(1)));
2754
2755        // Override x.y, but x is not a document reference.
2756        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        // Higher-standard language rule: fact and rule names should not collide.
2772        // It's ambiguous for humans and leads to confusing error messages.
2773        //
2774        // This is currently expected to FAIL until the language enforces it.
2775        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}