lemma/
validator.rs

1/// Type of an expression for validation.
2///
3/// Used during semantic analysis to catch type errors early,
4/// before code execution. Allows validation of logical operators,
5/// type compatibility, and currency matching.
6#[derive(Debug, Clone, PartialEq)]
7enum ExpressionType {
8    Boolean,
9    Number,
10    Percentage,
11    Text,
12    Money,
13    Mass,
14    Length,
15    Volume,
16    Duration,
17    Temperature,
18    Power,
19    Force,
20    Pressure,
21    Energy,
22    Frequency,
23    Data,
24    Date,
25    Unknown,
26    Never,
27}
28
29impl ExpressionType {
30    /// Returns true if this type is boolean
31    fn is_boolean(&self) -> bool {
32        matches!(self, ExpressionType::Boolean)
33    }
34
35    /// Returns a human-readable name for this type
36    fn name(&self) -> &'static str {
37        match self {
38            ExpressionType::Boolean => "boolean",
39            ExpressionType::Number => "number",
40            ExpressionType::Percentage => "percentage",
41            ExpressionType::Text => "text",
42            ExpressionType::Money => "money",
43            ExpressionType::Mass => "mass",
44            ExpressionType::Length => "length",
45            ExpressionType::Volume => "volume",
46            ExpressionType::Duration => "duration",
47            ExpressionType::Temperature => "temperature",
48            ExpressionType::Power => "power",
49            ExpressionType::Force => "force",
50            ExpressionType::Pressure => "pressure",
51            ExpressionType::Energy => "energy",
52            ExpressionType::Frequency => "frequency",
53            ExpressionType::Data => "data",
54            ExpressionType::Date => "date",
55            ExpressionType::Unknown => "unknown",
56            ExpressionType::Never => "never",
57        }
58    }
59
60    /// Infer the type from a literal value
61    fn from_literal(lit: &crate::LiteralValue) -> Self {
62        match lit {
63            crate::LiteralValue::Boolean(_) => ExpressionType::Boolean,
64            crate::LiteralValue::Number(_) => ExpressionType::Number,
65            crate::LiteralValue::Percentage(_) => ExpressionType::Percentage,
66            crate::LiteralValue::Text(_) => ExpressionType::Text,
67            crate::LiteralValue::Unit(unit) => match unit {
68                crate::NumericUnit::Money(_, _) => ExpressionType::Money,
69                crate::NumericUnit::Mass(_, _) => ExpressionType::Mass,
70                crate::NumericUnit::Length(_, _) => ExpressionType::Length,
71                crate::NumericUnit::Volume(_, _) => ExpressionType::Volume,
72                crate::NumericUnit::Duration(_, _) => ExpressionType::Duration,
73                crate::NumericUnit::Temperature(_, _) => ExpressionType::Temperature,
74                crate::NumericUnit::Power(_, _) => ExpressionType::Power,
75                crate::NumericUnit::Force(_, _) => ExpressionType::Force,
76                crate::NumericUnit::Pressure(_, _) => ExpressionType::Pressure,
77                crate::NumericUnit::Energy(_, _) => ExpressionType::Energy,
78                crate::NumericUnit::Frequency(_, _) => ExpressionType::Frequency,
79                crate::NumericUnit::Data(_, _) => ExpressionType::Data,
80            },
81            crate::LiteralValue::Date(_) => ExpressionType::Date,
82            _ => ExpressionType::Unknown,
83        }
84    }
85}
86
87use crate::{
88    ConversionTarget, Expression, ExpressionKind, FactType, FactValue, LemmaDoc, LemmaError,
89    LemmaResult, LemmaRule, Span,
90};
91use std::collections::{HashMap, HashSet};
92use std::sync::Arc;
93
94/// Documents that have passed semantic validation
95#[derive(Debug, Clone)]
96pub struct ValidatedDocuments {
97    pub documents: Vec<LemmaDoc>,
98}
99
100/// Comprehensive semantic validator that runs after parsing but before evaluation
101#[derive(Default)]
102pub struct Validator;
103
104impl Validator {
105    /// Create a new validator
106    pub fn new() -> Self {
107        Self
108    }
109
110    /// Validate all documents and return validated documents
111    pub fn validate_all(&self, docs: Vec<LemmaDoc>) -> LemmaResult<ValidatedDocuments> {
112        // Phase 1: Check for duplicate facts and rules within each document
113        self.validate_duplicates(&docs)?;
114
115        // Phase 2: Validate cross-document references
116        self.validate_document_references(&docs)?;
117
118        // Phase 3: Validate all rule references (fact vs rule reference types)
119        self.validate_rule_references(&docs)?;
120
121        // Phase 4: Check for circular dependencies
122        self.check_circular_dependencies(&docs)?;
123
124        // Phase 5: Validate expression types
125        self.validate_expression_types(&docs)?;
126
127        Ok(ValidatedDocuments { documents: docs })
128    }
129
130    /// Check for duplicate facts and rules within each document
131    fn validate_duplicates(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
132        for doc in docs {
133            // Check for duplicate facts
134            let mut fact_names: HashMap<String, Span> = HashMap::new();
135            for fact in &doc.facts {
136                let fact_name = crate::analysis::fact_display_name(fact);
137
138                if let Some(first_span) = fact_names.get(&fact_name) {
139                    let duplicate_span = fact.span.clone().unwrap_or(Span {
140                        start: 0,
141                        end: 0,
142                        line: 0,
143                        col: 0,
144                    });
145                    let first_doc_line = if first_span.line >= doc.start_line {
146                        first_span.line - doc.start_line + 1
147                    } else {
148                        first_span.line
149                    };
150
151                    let error_message = match fact.fact_type {
152                        FactType::Local(_) => format!("Duplicate fact definition: '{}'", fact_name),
153                        FactType::Foreign(_) => format!("Duplicate fact override: '{}'", fact_name),
154                    };
155
156                    let suggestion = match fact.fact_type {
157                        FactType::Local(_) => format!(
158                            "Fact '{}' was already defined at doc line {} (file line {}). Each fact can only be defined once per document.",
159                            fact_name, first_doc_line, first_span.line
160                        ),
161                        FactType::Foreign(_) => format!(
162                            "Fact override '{}' was already defined at doc line {} (file line {}). Each fact can only be overridden once per document.",
163                            fact_name, first_doc_line, first_span.line
164                        ),
165                    };
166
167                    return Err(LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
168                        message: error_message,
169                        span: duplicate_span,
170                        source_id: doc.source.clone().unwrap_or_else(|| "<input>".to_string()),
171                        source_text: Arc::from(""),
172                        doc_name: doc.name.clone(),
173                        doc_start_line: doc.start_line,
174                        suggestion: Some(suggestion),
175                    })));
176                }
177
178                if let Some(span) = &fact.span {
179                    fact_names.insert(fact_name, span.clone());
180                }
181            }
182
183            // Check for duplicate rules
184            let mut rule_names: HashMap<String, Span> = HashMap::new();
185            for rule in &doc.rules {
186                if let Some(first_span) = rule_names.get(&rule.name) {
187                    let duplicate_span = rule.span.clone().unwrap_or(Span {
188                        start: 0,
189                        end: 0,
190                        line: 0,
191                        col: 0,
192                    });
193                    let first_doc_line = if first_span.line >= doc.start_line {
194                        first_span.line - doc.start_line + 1
195                    } else {
196                        first_span.line
197                    };
198                    return Err(LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
199                        message: format!("Duplicate rule definition: '{}'", rule.name),
200                        span: duplicate_span,
201                        source_id: doc.source.clone().unwrap_or_else(|| "<input>".to_string()),
202                        source_text: Arc::from(""),
203                        doc_name: doc.name.clone(),
204                        doc_start_line: doc.start_line,
205                        suggestion: Some(format!(
206                            "Rule '{}' was already defined at doc line {} (file line {}). Each rule can only be defined once per document. Consider using 'unless' clauses for conditional logic.",
207                            rule.name, first_doc_line, first_span.line
208                        )),
209            })));
210                }
211
212                if let Some(span) = &rule.span {
213                    rule_names.insert(rule.name.clone(), span.clone());
214                }
215            }
216
217            // Check for name conflicts between facts and rules
218            for rule in &doc.rules {
219                if let Some(fact_span) = fact_names.get(&rule.name) {
220                    let rule_span = rule.span.clone().unwrap_or(Span {
221                        start: 0,
222                        end: 0,
223                        line: 0,
224                        col: 0,
225                    });
226                    let fact_doc_line = if fact_span.line >= doc.start_line {
227                        fact_span.line - doc.start_line + 1
228                    } else {
229                        fact_span.line
230                    };
231
232                    return Err(LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
233                        message: format!("Name conflict: '{}' is defined as both a fact and a rule", rule.name),
234                        span: rule_span,
235                        source_id: doc.source.clone().unwrap_or_else(|| "<input>".to_string()),
236                        source_text: Arc::from(""),
237                        doc_name: doc.name.clone(),
238                        doc_start_line: doc.start_line,
239                        suggestion: Some(format!(
240                            "A fact named '{}' was already defined at doc line {} (file line {}). Facts and rules cannot share the same name within a document. Choose a different name for either the fact or the rule.",
241                            rule.name, fact_doc_line, fact_span.line
242                        )),
243            })));
244                }
245            }
246        }
247        Ok(())
248    }
249
250    /// Validate document references (facts that reference other documents)
251    fn validate_document_references(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
252        for doc in docs {
253            for fact in &doc.facts {
254                if let FactValue::DocumentReference(ref_doc_name) = &fact.value {
255                    // Check if the referenced document exists
256                    if !docs.iter().any(|d| d.name == *ref_doc_name) {
257                        return Err(LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
258                            message: format!("Document reference error: '{}' does not exist", ref_doc_name),
259                            span: fact.span.clone().unwrap_or(Span { start: 0, end: 0, line: 0, col: 0 }),
260                            source_id: doc.source.clone().unwrap_or_else(|| "<input>".to_string()),
261                            source_text: Arc::from(""),
262                            doc_name: doc.name.clone(),
263                            doc_start_line: doc.start_line,
264                            suggestion: Some(format!(
265                                "Document '{}' is referenced but not defined. Make sure the document exists in your workspace.",
266                                ref_doc_name
267                            )),
268            })));
269                    }
270                }
271            }
272        }
273        Ok(())
274    }
275
276    /// Validate all rule references (fact vs rule reference types)
277    fn validate_rule_references(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
278        for doc in docs {
279            for rule in &doc.rules {
280                self.validate_expression_references(&rule.expression, doc, docs)?;
281
282                for unless_clause in &rule.unless_clauses {
283                    self.validate_expression_references(&unless_clause.condition, doc, docs)?;
284                    self.validate_expression_references(&unless_clause.result, doc, docs)?;
285                }
286            }
287        }
288        Ok(())
289    }
290
291    /// Helper: Check if a name is a fact in a document
292    fn is_fact_in_doc(&self, fact_name: &str, doc: &LemmaDoc) -> bool {
293        doc.facts.iter().any(|f| match &f.fact_type {
294            FactType::Local(name) => name == fact_name,
295            FactType::Foreign(foreign) => foreign.reference.join(".") == fact_name,
296        })
297    }
298
299    /// Helper: Check if a name is a rule in a document
300    fn is_rule_in_doc(&self, rule_name: &str, doc: &LemmaDoc) -> bool {
301        doc.rules.iter().any(|r| r.name == rule_name)
302    }
303
304    /// Helper: Find the document that a fact references (if it's a document reference fact)
305    fn get_referenced_doc<'a>(
306        &self,
307        fact_name: &str,
308        doc: &LemmaDoc,
309        all_docs: &'a [LemmaDoc],
310    ) -> Option<&'a LemmaDoc> {
311        // Find the fact in the current document
312        let fact = doc.facts.iter().find(|f| match &f.fact_type {
313            FactType::Local(name) => name == fact_name,
314            _ => false,
315        })?;
316
317        // Check if it's a document reference
318        if let FactValue::DocumentReference(ref_doc_name) = &fact.value {
319            // Find and return the referenced document
320            all_docs.iter().find(|d| d.name == *ref_doc_name)
321        } else {
322            None
323        }
324    }
325
326    /// Validate references within an expression
327    fn validate_expression_references(
328        &self,
329        expr: &Expression,
330        current_doc: &LemmaDoc,
331        all_docs: &[LemmaDoc],
332    ) -> LemmaResult<()> {
333        match &expr.kind {
334            ExpressionKind::FactReference(fact_ref) => {
335                self.validate_fact_reference(fact_ref, expr, current_doc, all_docs)
336            }
337            ExpressionKind::RuleReference(rule_ref) => {
338                self.validate_rule_reference(rule_ref, expr, current_doc, all_docs)
339            }
340            // Recursively validate nested expressions
341            ExpressionKind::LogicalAnd(left, right) | ExpressionKind::LogicalOr(left, right) => {
342                self.validate_expression_references(left, current_doc, all_docs)?;
343                self.validate_expression_references(right, current_doc, all_docs)
344            }
345            ExpressionKind::Arithmetic(left, _, right)
346            | ExpressionKind::Comparison(left, _, right) => {
347                self.validate_expression_references(left, current_doc, all_docs)?;
348                self.validate_expression_references(right, current_doc, all_docs)
349            }
350            ExpressionKind::LogicalNegation(inner, _)
351            | ExpressionKind::MathematicalOperator(_, inner)
352            | ExpressionKind::UnitConversion(inner, _) => {
353                self.validate_expression_references(inner, current_doc, all_docs)
354            }
355            ExpressionKind::FactHasAnyValue(_fact_ref) => {
356                // For "have" expressions, we don't validate the fact reference as it's a dynamic check
357                Ok(())
358            }
359            _ => Ok(()),
360        }
361    }
362
363    /// Validate a fact reference (without '?')
364    fn validate_fact_reference(
365        &self,
366        fact_ref: &crate::FactReference,
367        expr: &Expression,
368        current_doc: &LemmaDoc,
369        all_docs: &[LemmaDoc],
370    ) -> LemmaResult<()> {
371        let ref_name = fact_ref.reference.join(".");
372
373        // Single-segment reference
374        if fact_ref.reference.len() == 1 {
375            return self.validate_single_segment_fact_ref(&ref_name, expr, current_doc);
376        }
377
378        // Multi-segment reference
379        if fact_ref.reference.len() < 2 {
380            return Ok(());
381        }
382
383        let doc_ref = &fact_ref.reference[0];
384        let field_name = fact_ref.reference[1..].join(".");
385
386        self.validate_multi_segment_fact_ref(
387            &ref_name,
388            doc_ref,
389            &field_name,
390            expr,
391            current_doc,
392            all_docs,
393        )
394    }
395
396    /// Validate a single-segment fact reference
397    fn validate_single_segment_fact_ref(
398        &self,
399        ref_name: &str,
400        expr: &Expression,
401        current_doc: &LemmaDoc,
402    ) -> LemmaResult<()> {
403        if self.is_rule_in_doc(ref_name, current_doc) {
404            return Err(self.create_reference_error(
405                format!(
406                    "Reference error: '{}' is a rule and must be referenced with '?' (e.g., '{}?')",
407                    ref_name, ref_name
408                ),
409                format!("Use '{}?' to reference the rule '{}'", ref_name, ref_name),
410                expr,
411                current_doc,
412            ));
413        }
414        Ok(())
415    }
416
417    /// Validate a multi-segment fact reference
418    fn validate_multi_segment_fact_ref(
419        &self,
420        ref_name: &str,
421        doc_ref: &str,
422        field_name: &str,
423        expr: &Expression,
424        current_doc: &LemmaDoc,
425        all_docs: &[LemmaDoc],
426    ) -> LemmaResult<()> {
427        // Check if first segment is a fact that references a document
428        if let Some(referenced_doc) = self.get_referenced_doc(doc_ref, current_doc, all_docs) {
429            if self.is_rule_in_doc(field_name, referenced_doc) {
430                return Err(self.create_reference_error(
431                    format!("Reference error: '{}' references a rule in document '{}' and must use '?' (e.g., '{}?')", ref_name, referenced_doc.name, ref_name),
432                    format!("Use '{}?' to reference the rule '{}' in document '{}'", ref_name, field_name, referenced_doc.name),
433                    expr,
434                    current_doc,
435                ));
436            }
437            return Ok(());
438        }
439
440        // Check if it's a rule in the current document
441        if self.is_rule_in_doc(field_name, current_doc) {
442            return Err(self.create_reference_error(
443                format!("Reference error: '{}' appears to reference a rule and must use '?' (e.g., '{}?')", ref_name, ref_name),
444                format!("Use '{}?' to reference the rule '{}'", ref_name, ref_name),
445                expr,
446                current_doc,
447            ));
448        }
449        Ok(())
450    }
451
452    /// Validate a rule reference (with '?')
453    fn validate_rule_reference(
454        &self,
455        rule_ref: &crate::RuleReference,
456        expr: &Expression,
457        current_doc: &LemmaDoc,
458        all_docs: &[LemmaDoc],
459    ) -> LemmaResult<()> {
460        let ref_name = rule_ref.reference.join(".");
461
462        // Single-segment reference
463        if rule_ref.reference.len() == 1 {
464            return self.validate_single_segment_rule_ref(&ref_name, expr, current_doc);
465        }
466
467        // Multi-segment reference
468        if rule_ref.reference.len() < 2 {
469            return Ok(());
470        }
471
472        let doc_ref = &rule_ref.reference[0];
473        let field_name = rule_ref.reference[1..].join(".");
474
475        self.validate_multi_segment_rule_ref(
476            &ref_name,
477            doc_ref,
478            &field_name,
479            expr,
480            current_doc,
481            all_docs,
482        )
483    }
484
485    /// Validate a single-segment rule reference
486    fn validate_single_segment_rule_ref(
487        &self,
488        ref_name: &str,
489        expr: &Expression,
490        current_doc: &LemmaDoc,
491    ) -> LemmaResult<()> {
492        if self.is_fact_in_doc(ref_name, current_doc) {
493            return Err(self.create_reference_error(
494                format!("Reference error: '{}' is a fact and should not use '?' (use '{}' instead of '{}?')", ref_name, ref_name, ref_name),
495                format!("Use '{}' to reference the fact '{}' (remove the '?')", ref_name, ref_name),
496                expr,
497                current_doc,
498            ));
499        }
500        Ok(())
501    }
502
503    /// Validate a multi-segment rule reference
504    fn validate_multi_segment_rule_ref(
505        &self,
506        ref_name: &str,
507        doc_ref: &str,
508        field_name: &str,
509        expr: &Expression,
510        current_doc: &LemmaDoc,
511        all_docs: &[LemmaDoc],
512    ) -> LemmaResult<()> {
513        // Check if first segment is a fact that references a document
514        if let Some(referenced_doc) = self.get_referenced_doc(doc_ref, current_doc, all_docs) {
515            if self.is_fact_in_doc(field_name, referenced_doc) {
516                return Err(self.create_reference_error(
517                    format!("Reference error: '{}' references a fact in document '{}' and should not use '?' (use '{}' instead of '{}?')", ref_name, referenced_doc.name, ref_name, ref_name),
518                    format!("Use '{}' to reference the fact '{}' in document '{}' (remove the '?')", ref_name, field_name, referenced_doc.name),
519                    expr,
520                    current_doc,
521                ));
522            }
523            return Ok(());
524        }
525
526        // Check if it's a fact in the current document
527        if self.is_fact_in_doc(field_name, current_doc) {
528            return Err(self.create_reference_error(
529                format!("Reference error: '{}' appears to reference a fact and should not use '?' (use '{}' instead of '{}?')", ref_name, ref_name, ref_name),
530                format!("Use '{}' to reference the fact '{}' (remove the '?')", ref_name, ref_name),
531                expr,
532                current_doc,
533            ));
534        }
535        Ok(())
536    }
537
538    /// Helper to create a semantic error for reference validation
539    fn create_reference_error(
540        &self,
541        message: String,
542        suggestion: String,
543        expr: &Expression,
544        current_doc: &LemmaDoc,
545    ) -> LemmaError {
546        LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
547            message,
548            span: expr.span.clone().unwrap_or(Span {
549                start: 0,
550                end: 0,
551                line: 0,
552                col: 0,
553            }),
554            source_id: current_doc
555                .source
556                .clone()
557                .unwrap_or_else(|| "<input>".to_string()),
558            source_text: Arc::from(""),
559            doc_name: current_doc.name.clone(),
560            doc_start_line: current_doc.start_line,
561            suggestion: Some(suggestion),
562        }))
563    }
564
565    /// Check for circular dependencies in rules (moved from document transpiler)
566    fn check_circular_dependencies(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
567        // Build dependency graph from all rules across all documents
568        let mut all_rules = Vec::new();
569        for doc in docs {
570            all_rules.extend(doc.rules.iter().cloned());
571        }
572
573        let graph = self.build_dependency_graph(&all_rules);
574        let mut visited = HashSet::new();
575
576        for rule_name in graph.keys() {
577            if !visited.contains(rule_name) {
578                let mut visiting = HashSet::new();
579                let mut path = Vec::new();
580
581                if let Some(cycle) =
582                    Self::detect_cycle(&graph, rule_name, &mut visiting, &mut visited, &mut path)
583                {
584                    let cycle_display = cycle.join(" -> ");
585                    return Err(LemmaError::CircularDependency(format!(
586                        "Circular dependency detected: {}. Rules cannot depend on themselves directly or indirectly.",
587                        cycle_display
588                    )));
589                }
590            }
591        }
592
593        Ok(())
594    }
595
596    /// Build a dependency graph of rules (local document only)
597    fn build_dependency_graph(&self, rules: &[LemmaRule]) -> HashMap<String, HashSet<String>> {
598        let mut graph = HashMap::new();
599
600        for rule in rules {
601            let mut dependencies = HashSet::new();
602            let refs = crate::analysis::extract_references(&rule.expression);
603            for rule_ref in refs.rules {
604                dependencies.insert(rule_ref.join("."));
605            }
606            for uc in &rule.unless_clauses {
607                let cond_refs = crate::analysis::extract_references(&uc.condition);
608                let res_refs = crate::analysis::extract_references(&uc.result);
609                for rule_ref in cond_refs.rules.into_iter().chain(res_refs.rules) {
610                    dependencies.insert(rule_ref.join("."));
611                }
612            }
613            graph.insert(rule.name.clone(), dependencies);
614        }
615
616        graph
617    }
618
619    /// Detect cycles in the dependency graph using DFS (moved from document transpiler)
620    fn detect_cycle(
621        graph: &HashMap<String, HashSet<String>>,
622        node: &str,
623        visiting: &mut HashSet<String>,
624        visited: &mut HashSet<String>,
625        path: &mut Vec<String>,
626    ) -> Option<Vec<String>> {
627        if visiting.contains(node) {
628            let cycle_start = path.iter().position(|n| n == node).unwrap_or(0);
629            let mut cycle = path[cycle_start..].to_vec();
630            cycle.push(node.to_string());
631            return Some(cycle);
632        }
633
634        if visited.contains(node) {
635            return None;
636        }
637
638        visiting.insert(node.to_string());
639        path.push(node.to_string());
640
641        if let Some(dependencies) = graph.get(node) {
642            for dep in dependencies {
643                if graph.contains_key(dep) {
644                    if let Some(cycle) = Self::detect_cycle(graph, dep, visiting, visited, path) {
645                        return Some(cycle);
646                    }
647                }
648            }
649        }
650
651        path.pop();
652        visiting.remove(node);
653        visited.insert(node.to_string());
654
655        None
656    }
657
658    /// Validate expression types - ensure logical operators only have boolean operands
659    fn validate_expression_types(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
660        for doc in docs {
661            for rule in &doc.rules {
662                self.validate_expression_type(&rule.expression, doc)?;
663                for unless_clause in &rule.unless_clauses {
664                    // Validate condition is boolean
665                    let condition_type = self
666                        .infer_expression_type_with_context(&unless_clause.condition, Some(doc))?;
667                    if condition_type != ExpressionType::Unknown && !condition_type.is_boolean() {
668                        return Err(LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
669                            message: format!(
670                                "Type error: Unless condition must be boolean, but got {}",
671                                condition_type.name()
672                            ),
673                            span: unless_clause.condition.span.clone().unwrap_or(Span {
674                                start: 0,
675                                end: 0,
676                                line: 0,
677                                col: 0,
678                            }),
679                            source_id: doc.source.clone().unwrap_or_else(|| "<input>".to_string()),
680                            source_text: Arc::from(""),
681                            doc_name: doc.name.clone(),
682                            doc_start_line: doc.start_line,
683                            suggestion: Some(
684                                "Use a comparison or boolean expression for unless conditions"
685                                    .to_string(),
686                            ),
687                        })));
688                    }
689
690                    self.validate_expression_type(&unless_clause.condition, doc)?;
691                    self.validate_expression_type(&unless_clause.result, doc)?;
692                }
693                self.validate_rule_type_consistency(rule, doc)?;
694            }
695        }
696        Ok(())
697    }
698
699    /// Validate a single expression for type correctness
700    fn validate_expression_type(&self, expr: &Expression, doc: &LemmaDoc) -> LemmaResult<()> {
701        match &expr.kind {
702            ExpressionKind::LogicalAnd(left, right) => {
703                self.validate_logical_operand(left, doc, "and")?;
704                self.validate_logical_operand(right, doc, "and")?;
705                self.validate_expression_type(left, doc)?;
706                self.validate_expression_type(right, doc)?;
707            }
708            ExpressionKind::LogicalOr(left, right) => {
709                self.validate_logical_operand(left, doc, "or")?;
710                self.validate_logical_operand(right, doc, "or")?;
711                self.validate_expression_type(left, doc)?;
712                self.validate_expression_type(right, doc)?;
713            }
714            ExpressionKind::Arithmetic(left, _op, right) => {
715                self.validate_expression_type(left, doc)?;
716                self.validate_expression_type(right, doc)?;
717                self.validate_money_arithmetic(left, right, doc)?;
718            }
719            ExpressionKind::Comparison(left, _op, right) => {
720                self.validate_expression_type(left, doc)?;
721                self.validate_expression_type(right, doc)?;
722                self.validate_money_comparison(left, right, doc)?;
723            }
724            ExpressionKind::LogicalNegation(inner, _negation_type) => {
725                self.validate_expression_type(inner, doc)?;
726            }
727            ExpressionKind::MathematicalOperator(_op, operand) => {
728                self.validate_expression_type(operand, doc)?;
729            }
730            ExpressionKind::UnitConversion(value, _target) => {
731                self.validate_expression_type(value, doc)?;
732            }
733            _ => {}
734        }
735        Ok(())
736    }
737
738    /// Helper to validate that an operand is boolean for logical operators
739    fn validate_logical_operand(
740        &self,
741        operand: &Expression,
742        doc: &LemmaDoc,
743        operator: &str,
744    ) -> LemmaResult<()> {
745        let operand_type = self.infer_expression_type(operand)?;
746
747        // Only validate if we know the type (not Unknown)
748        if operand_type == ExpressionType::Unknown || operand_type.is_boolean() {
749            return Ok(());
750        }
751
752        Err(LemmaError::Semantic(Box::new(crate::error::ErrorDetails {
753            message: format!(
754                "Type error: Logical operator '{}' requires boolean operands, but operand has type {}",
755                operator,
756                operand_type.name()
757            ),
758            span: operand.span.clone().unwrap_or(Span { start: 0, end: 0, line: 0, col: 0 }),
759            source_id: doc.source.clone().unwrap_or_else(|| "<input>".to_string()),
760            source_text: Arc::from(""),
761            doc_name: doc.name.clone(),
762            doc_start_line: doc.start_line,
763            suggestion: Some("Use a boolean expression or comparison for logical operations".to_string()),
764        })))
765    }
766
767    /// Validate that all branches of a rule return compatible types
768    fn validate_rule_type_consistency(&self, rule: &LemmaRule, doc: &LemmaDoc) -> LemmaResult<()> {
769        if rule.unless_clauses.is_empty() {
770            return Ok(());
771        }
772
773        let default_type = self.infer_expression_type_with_context(&rule.expression, Some(doc))?;
774
775        let mut non_veto_types = Vec::new();
776        if default_type != ExpressionType::Never {
777            non_veto_types.push(("default expression", default_type.clone()));
778        }
779
780        for (idx, unless_clause) in rule.unless_clauses.iter().enumerate() {
781            let result_type =
782                self.infer_expression_type_with_context(&unless_clause.result, Some(doc))?;
783            if result_type != ExpressionType::Never {
784                non_veto_types.push((
785                    if idx == 0 {
786                        "first unless clause"
787                    } else {
788                        "unless clause"
789                    },
790                    result_type,
791                ));
792            }
793        }
794
795        if non_veto_types.is_empty() {
796            return Ok(());
797        }
798
799        let (first_label, first_type) = &non_veto_types[0];
800        for (label, branch_type) in &non_veto_types[1..] {
801            if !self.are_types_compatible(first_type, branch_type) {
802                return Err(LemmaError::Engine(format!(
803                    "Rule '{}' has incompatible return types: {} returns {} but {} returns {}",
804                    rule.name,
805                    first_label,
806                    first_type.name(),
807                    label,
808                    branch_type.name()
809                )));
810            }
811        }
812
813        Ok(())
814    }
815
816    /// Check if two types are compatible
817    fn are_types_compatible(&self, type1: &ExpressionType, type2: &ExpressionType) -> bool {
818        if type1 == type2 {
819            return true;
820        }
821
822        if type1 == &ExpressionType::Unknown || type2 == &ExpressionType::Unknown {
823            return true;
824        }
825
826        false
827    }
828
829    /// Validate that money arithmetic uses the same currency
830    fn validate_money_arithmetic(
831        &self,
832        left: &Expression,
833        right: &Expression,
834        doc: &LemmaDoc,
835    ) -> LemmaResult<()> {
836        let left_currency = self.extract_currency(left, doc);
837        let right_currency = self.extract_currency(right, doc);
838
839        if let (Some(left_curr), Some(right_curr)) = (left_currency, right_currency) {
840            if left_curr != right_curr {
841                return Err(LemmaError::Engine(format!(
842                    "Cannot perform arithmetic with different currencies: {} and {}",
843                    left_curr, right_curr
844                )));
845            }
846        }
847
848        Ok(())
849    }
850
851    /// Validate that money comparisons use the same currency
852    fn validate_money_comparison(
853        &self,
854        left: &Expression,
855        right: &Expression,
856        doc: &LemmaDoc,
857    ) -> LemmaResult<()> {
858        let left_currency = self.extract_currency(left, doc);
859        let right_currency = self.extract_currency(right, doc);
860
861        if let (Some(left_curr), Some(right_curr)) = (left_currency, right_currency) {
862            if left_curr != right_curr {
863                return Err(LemmaError::Engine(format!(
864                    "Cannot compare different currencies: {} and {}",
865                    left_curr, right_curr
866                )));
867            }
868        }
869
870        Ok(())
871    }
872
873    /// Extract currency from an expression if it's a Money type
874    fn extract_currency(&self, expr: &Expression, doc: &LemmaDoc) -> Option<crate::MoneyUnit> {
875        match &expr.kind {
876            ExpressionKind::Literal(crate::LiteralValue::Unit(crate::NumericUnit::Money(
877                _,
878                currency,
879            ))) => Some(currency.clone()),
880            ExpressionKind::FactReference(fact_ref) => {
881                let fact_name = &fact_ref.reference[0];
882                for fact in &doc.facts {
883                    if let crate::FactType::Local(name) = &fact.fact_type {
884                        if name == fact_name {
885                            if let crate::FactValue::Literal(crate::LiteralValue::Unit(
886                                crate::NumericUnit::Money(_, currency),
887                            )) = &fact.value
888                            {
889                                return Some(currency.clone());
890                            }
891                        }
892                    }
893                }
894                None
895            }
896            _ => None,
897        }
898    }
899
900    /// Infer the type of an expression
901    fn infer_expression_type(&self, expr: &Expression) -> LemmaResult<ExpressionType> {
902        self.infer_expression_type_with_context(expr, None)
903    }
904
905    #[allow(clippy::only_used_in_recursion)]
906    fn infer_expression_type_with_context(
907        &self,
908        expr: &Expression,
909        doc: Option<&LemmaDoc>,
910    ) -> LemmaResult<ExpressionType> {
911        match &expr.kind {
912            ExpressionKind::Literal(lit) => Ok(ExpressionType::from_literal(lit)),
913            ExpressionKind::Comparison(_, _, _) => Ok(ExpressionType::Boolean),
914            ExpressionKind::LogicalAnd(_, _) => Ok(ExpressionType::Boolean),
915            ExpressionKind::LogicalOr(_, _) => Ok(ExpressionType::Boolean),
916            ExpressionKind::LogicalNegation(_, _) => Ok(ExpressionType::Boolean),
917            ExpressionKind::FactHasAnyValue(_) => Ok(ExpressionType::Boolean),
918            ExpressionKind::Veto(_) => Ok(ExpressionType::Never),
919            ExpressionKind::FactReference(fact_ref) => {
920                // Try to resolve fact type from document
921                let Some(d) = doc else {
922                    return Ok(ExpressionType::Unknown);
923                };
924
925                let ref_name = fact_ref.reference.join(".");
926                for fact in &d.facts {
927                    let fact_name = crate::analysis::fact_display_name(fact);
928                    if fact_name != ref_name {
929                        continue;
930                    }
931                    if let FactValue::Literal(lit) = &fact.value {
932                        return Ok(ExpressionType::from_literal(lit));
933                    }
934                }
935                Ok(ExpressionType::Unknown)
936            }
937            ExpressionKind::RuleReference(_) => {
938                // Rules can't be resolved without full dependency analysis
939                Ok(ExpressionType::Unknown)
940            }
941            ExpressionKind::Arithmetic(left, _, right) => {
942                let left_type = self.infer_expression_type_with_context(left, doc)?;
943                let right_type = self.infer_expression_type_with_context(right, doc)?;
944                if left_type == ExpressionType::Unknown || right_type == ExpressionType::Unknown {
945                    return Ok(ExpressionType::Unknown);
946                }
947                // Division of numbers (or other compatible types) produces a number
948                Ok(ExpressionType::Number)
949            }
950            ExpressionKind::MathematicalOperator(_, _) => Ok(ExpressionType::Number),
951            ExpressionKind::UnitConversion(value_expr, target) => {
952                let value_type = self.infer_expression_type_with_context(value_expr, doc)?;
953                Ok(self.infer_conversion_result_type(&value_type, target))
954            }
955        }
956    }
957
958    /// Helper to infer the result type of a unit conversion
959    fn infer_conversion_result_type(
960        &self,
961        value_type: &ExpressionType,
962        target: &ConversionTarget,
963    ) -> ExpressionType {
964        match (value_type, target) {
965            // Number to Unit conversions
966            (ExpressionType::Number, ConversionTarget::Mass(_)) => ExpressionType::Mass,
967            (ExpressionType::Number, ConversionTarget::Length(_)) => ExpressionType::Length,
968            (ExpressionType::Number, ConversionTarget::Volume(_)) => ExpressionType::Volume,
969            (ExpressionType::Number, ConversionTarget::Duration(_)) => ExpressionType::Duration,
970            (ExpressionType::Number, ConversionTarget::Temperature(_)) => {
971                ExpressionType::Temperature
972            }
973            (ExpressionType::Number, ConversionTarget::Power(_)) => ExpressionType::Power,
974            (ExpressionType::Number, ConversionTarget::Force(_)) => ExpressionType::Force,
975            (ExpressionType::Number, ConversionTarget::Pressure(_)) => ExpressionType::Pressure,
976            (ExpressionType::Number, ConversionTarget::Energy(_)) => ExpressionType::Energy,
977            (ExpressionType::Number, ConversionTarget::Frequency(_)) => ExpressionType::Frequency,
978            (ExpressionType::Number, ConversionTarget::Data(_)) => ExpressionType::Data,
979            (ExpressionType::Number, ConversionTarget::Money(_)) => ExpressionType::Money,
980            (ExpressionType::Number, ConversionTarget::Percentage) => ExpressionType::Percentage,
981
982            // Unit to Number conversions (all physical units) and Percentage conversions
983            (_, ConversionTarget::Percentage) => ExpressionType::Percentage,
984            _ => ExpressionType::Number,
985        }
986    }
987}