Skip to main content

lemma/planning/
graph.rs

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