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
100pub struct Validator;
102
103impl Validator {
104 pub fn new() -> Self {
106 Self
107 }
108
109 pub fn validate_all(&self, docs: Vec<LemmaDoc>) -> LemmaResult<ValidatedDocuments> {
111 self.validate_duplicates(&docs)?;
113
114 self.validate_document_references(&docs)?;
116
117 self.validate_rule_references(&docs)?;
119
120 self.check_circular_dependencies(&docs)?;
122
123 self.validate_expression_types(&docs)?;
125
126 Ok(ValidatedDocuments { documents: docs })
127 }
128
129 fn validate_duplicates(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
131 for doc in docs {
132 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 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 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 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 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 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 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 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 fn get_referenced_doc<'a>(
305 &self,
306 fact_name: &str,
307 doc: &LemmaDoc,
308 all_docs: &'a [LemmaDoc],
309 ) -> Option<&'a LemmaDoc> {
310 let fact = doc.facts.iter().find(|f| match &f.fact_type {
312 FactType::Local(name) => name == fact_name,
313 _ => false,
314 })?;
315
316 if let FactValue::DocumentReference(ref_doc_name) = &fact.value {
318 all_docs.iter().find(|d| d.name == *ref_doc_name)
320 } else {
321 None
322 }
323 }
324
325 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 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 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 if let Some(referenced_doc) =
358 self.get_referenced_doc(doc_ref, current_doc, all_docs)
359 {
360 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 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 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 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 if let Some(referenced_doc) =
412 self.get_referenced_doc(doc_ref, current_doc, all_docs)
413 {
414 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 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 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 }
471 _ => {}
472 }
473 Ok(())
474 }
475
476 fn check_circular_dependencies(&self, docs: &[LemmaDoc]) -> LemmaResult<()> {
478 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 fn build_dependency_graph(&self, rules: &[LemmaRule]) -> HashMap<String, HashSet<String>> {
510 crate::analysis::build_dependency_graph(rules)
511 }
512
513 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 match (&value_type, target) {
883 (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 (_, 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 (_, ConversionTarget::Percentage) => Ok(ExpressionType::Percentage),
936 }
937 }
938 }
939 }
940}
941
942impl Default for Validator {
943 fn default() -> Self {
944 Self::new()
945 }
946}