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