1#[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 fn is_boolean(&self) -> bool {
32 matches!(self, ExpressionType::Boolean)
33 }
34
35 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 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#[derive(Debug, Clone)]
96pub struct ValidatedDocuments {
97 pub documents: Vec<LemmaDoc>,
98}
99
100#[derive(Default)]
102pub struct Validator;
103
104impl Validator {
105 pub fn new() -> Self {
107 Self
108 }
109
110 pub fn validate_all(&self, docs: Vec<LemmaDoc>) -> LemmaResult<ValidatedDocuments> {
112 self.validate_duplicates(&docs)?;
114
115 self.validate_document_references(&docs)?;
117
118 self.validate_rule_references(&docs)?;
120
121 self.check_circular_dependencies(&docs)?;
123
124 self.validate_expression_types(&docs)?;
126
127 Ok(ValidatedDocuments { documents: docs })
128 }
129
130 fn validate_duplicates(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
132 for doc in docs {
133 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 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 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 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 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 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 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 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 fn get_referenced_doc<'a>(
306 &self,
307 fact_name: &str,
308 doc: &LemmaDoc,
309 all_docs: &'a [LemmaDoc],
310 ) -> Option<&'a LemmaDoc> {
311 let fact = doc.facts.iter().find(|f| match &f.fact_type {
313 FactType::Local(name) => name == fact_name,
314 _ => false,
315 })?;
316
317 if let FactValue::DocumentReference(ref_doc_name) = &fact.value {
319 all_docs.iter().find(|d| d.name == *ref_doc_name)
321 } else {
322 None
323 }
324 }
325
326 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 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 Ok(())
358 }
359 _ => Ok(()),
360 }
361 }
362
363 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 if fact_ref.reference.len() == 1 {
375 return self.validate_single_segment_fact_ref(&ref_name, expr, current_doc);
376 }
377
378 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 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 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 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 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 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 if rule_ref.reference.len() == 1 {
464 return self.validate_single_segment_rule_ref(&ref_name, expr, current_doc);
465 }
466
467 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 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 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 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 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 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 fn check_circular_dependencies(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
567 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 fn infer_conversion_result_type(
960 &self,
961 value_type: &ExpressionType,
962 target: &ConversionTarget,
963 ) -> ExpressionType {
964 match (value_type, target) {
965 (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 (_, ConversionTarget::Percentage) => ExpressionType::Percentage,
984 _ => ExpressionType::Number,
985 }
986 }
987}