1use crate::planning::graph::Graph;
8use crate::semantic::{
9 Expression, FactPath, FactReference, FactValue, LemmaType, LiteralValue, RulePath,
10};
11use crate::LemmaError;
12use crate::ResourceLimits;
13use crate::Source;
14use serde::{Deserialize, Serialize};
15use std::collections::{HashMap, HashSet};
16use std::sync::Arc;
17
18#[derive(Debug, Clone, Serialize, Deserialize)]
23pub struct ExecutionPlan {
24 pub doc_name: String,
26
27 #[serde(serialize_with = "crate::serialization::serialize_fact_type_map")]
31 #[serde(deserialize_with = "crate::serialization::deserialize_fact_type_map")]
32 pub fact_schema: HashMap<FactPath, LemmaType>,
33
34 #[serde(serialize_with = "crate::serialization::serialize_fact_value_map")]
36 #[serde(deserialize_with = "crate::serialization::deserialize_fact_value_map")]
37 pub fact_values: HashMap<FactPath, LiteralValue>,
38
39 #[serde(serialize_with = "crate::serialization::serialize_fact_doc_ref_map")]
41 #[serde(deserialize_with = "crate::serialization::deserialize_fact_doc_ref_map")]
42 pub doc_refs: HashMap<FactPath, String>,
43
44 #[serde(serialize_with = "crate::serialization::serialize_fact_source_map")]
46 #[serde(deserialize_with = "crate::serialization::deserialize_fact_source_map")]
47 pub fact_sources: HashMap<FactPath, Source>,
48
49 pub rules: Vec<ExecutableRule>,
51
52 pub sources: HashMap<String, String>,
54}
55
56#[derive(Debug, Clone, Serialize, Deserialize)]
60pub struct ExecutableRule {
61 pub path: RulePath,
63
64 pub name: String,
66
67 pub branches: Vec<Branch>,
72
73 #[serde(serialize_with = "crate::serialization::serialize_fact_path_set")]
75 #[serde(deserialize_with = "crate::serialization::deserialize_fact_path_set")]
76 pub needs_facts: HashSet<FactPath>,
77
78 pub source: Option<Source>,
80
81 pub rule_type: LemmaType,
84}
85
86#[derive(Debug, Clone, Serialize, Deserialize)]
88pub struct Branch {
89 pub condition: Option<Expression>,
91
92 pub result: Expression,
94
95 pub source: Option<Source>,
97}
98
99pub(crate) fn build_execution_plan(graph: &Graph, main_doc_name: &str) -> ExecutionPlan {
102 let execution_order = graph.execution_order();
103 let mut fact_schema: HashMap<FactPath, LemmaType> = HashMap::new();
104 let mut fact_values: HashMap<FactPath, LiteralValue> = HashMap::new();
105 let mut doc_refs: HashMap<FactPath, String> = HashMap::new();
106 let mut fact_sources: HashMap<FactPath, Source> = HashMap::new();
107
108 for (path, fact) in graph.facts().iter() {
110 if let Some(src) = fact.source_location.clone() {
111 fact_sources.insert(path.clone(), src);
112 }
113 match &fact.value {
114 FactValue::Literal(lit) => {
115 fact_values.insert(path.clone(), lit.clone());
116
117 let fact_ref = FactReference {
121 segments: path.segments.iter().map(|s| s.fact.clone()).collect(),
122 fact: path.fact.clone(),
123 };
124
125 let context_doc = if let Some(first_segment) = path.segments.first() {
128 first_segment.doc.as_str()
129 } else {
130 let fact_ref_segments: Vec<String> =
132 path.segments.iter().map(|s| s.fact.clone()).collect();
133
134 let mut found_doc = None;
135 for (doc_name, doc) in graph.all_docs() {
136 for orig_fact in &doc.facts {
137 if orig_fact.reference.segments == fact_ref_segments
138 && orig_fact.reference.fact == path.fact
139 {
140 found_doc = Some(doc_name.as_str());
141 break;
142 }
143 }
144 if found_doc.is_some() {
145 break;
146 }
147 }
148 found_doc.unwrap_or(main_doc_name)
149 };
150
151 if let Some(orig_doc) = graph.all_docs().get(context_doc) {
155 for orig_fact in &orig_doc.facts {
156 if orig_fact.reference.segments.is_empty()
159 && orig_fact.reference.fact == fact_ref.fact
160 {
161 if let FactValue::TypeDeclaration { .. } = &orig_fact.value {
163 let orig_source = orig_fact.source_location.as_ref().unwrap_or_else(|| {
165 unreachable!(
166 "BUG: fact '{}' missing source_location during type resolution",
167 orig_fact.reference.fact
168 )
169 });
170 match graph.resolve_type_declaration(
171 &orig_fact.value,
172 orig_source,
173 context_doc,
174 ) {
175 Ok(lemma_type) => {
176 fact_schema.insert(path.clone(), lemma_type);
177 }
178 Err(e) => {
179 unreachable!(
182 "Failed to resolve type for fact {}: {}. This indicates a bug in validation - all types should be validated before execution plan building.",
183 path, e
184 );
185 }
186 }
187 }
188 break;
189 }
190 }
191 }
192
193 if !fact_schema.contains_key(path) {
196 fact_schema.insert(path.clone(), lit.get_type().clone());
197 }
198 }
199 FactValue::TypeDeclaration { .. } => {
200 let fact_ref = FactReference {
202 segments: path.segments.iter().map(|s| s.fact.clone()).collect(),
203 fact: path.fact.clone(),
204 };
205
206 let mut found_inline_type = false;
209 for (_doc_name, document_types) in graph.resolved_types().iter() {
210 if let Some(resolved_type) =
211 document_types.inline_type_definitions.get(&fact_ref)
212 {
213 fact_schema.insert(path.clone(), resolved_type.clone());
215 found_inline_type = true;
216 break;
217 }
218 }
219 if found_inline_type {
220 continue; }
222
223 let context_doc = if let Some(first_segment) = path.segments.first() {
227 first_segment.doc.as_str()
228 } else {
229 let fact_ref_segments: Vec<String> =
231 path.segments.iter().map(|s| s.fact.clone()).collect();
232
233 let mut found_doc = None;
234 for (doc_name, doc) in graph.all_docs() {
235 for fact in &doc.facts {
236 if fact.reference.segments == fact_ref_segments
237 && fact.reference.fact == path.fact
238 {
239 found_doc = Some(doc_name.as_str());
240 break;
241 }
242 }
243 if found_doc.is_some() {
244 break;
245 }
246 }
247
248 found_doc.unwrap_or_else(|| {
249 unreachable!(
250 "Cannot determine document context for fact '{}'. This indicates a bug in graph building.",
251 path
252 );
253 })
254 };
255
256 let fact_source = fact.source_location.as_ref().unwrap_or_else(|| {
257 unreachable!(
258 "BUG: fact '{}' missing source_location during type resolution",
259 fact.reference.fact
260 )
261 });
262 match graph.resolve_type_declaration(&fact.value, fact_source, context_doc) {
263 Ok(lemma_type) => {
264 fact_schema.insert(path.clone(), lemma_type);
265 }
266 Err(e) => {
267 unreachable!(
268 "Failed to resolve type for fact {}: {}. This indicates a bug in validation.",
269 path, e
270 );
271 }
272 }
273 }
274 FactValue::DocumentReference(doc_name) => {
275 doc_refs.insert(path.clone(), doc_name.clone());
276 }
277 }
278 }
279
280 for (path, schema_type) in &fact_schema {
282 if fact_values.contains_key(path) {
283 continue; }
285 if let Some(default_value) = schema_type.create_default_value() {
286 fact_values.insert(path.clone(), default_value);
287 }
288 }
289
290 for (path, value) in fact_values.iter_mut() {
297 let Some(schema_type) = fact_schema.get(path).cloned() else {
298 continue;
299 };
300
301 match coerce_literal_to_schema_type(value, &schema_type) {
302 Ok(coerced) => {
303 *value = coerced;
304 }
305 Err(msg) => {
306 unreachable!(
307 "Fact {} literal value is incompatible with declared type {}: {}. \
308 This should have been caught during validation. If you see a type definition here, \
309 it indicates a bug: type definitions cannot override typed facts.",
310 path,
311 schema_type.name(),
312 msg
313 );
314 }
315 }
316 }
317
318 let mut executable_rules: Vec<ExecutableRule> = Vec::new();
319
320 for rule_path in execution_order {
321 let rule_node = graph.rules().get(rule_path).expect(
322 "bug: rule from topological sort not in graph - validation should have caught this",
323 );
324
325 let mut executable_branches = Vec::new();
326 for (condition, result) in &rule_node.branches {
327 executable_branches.push(Branch {
328 condition: condition.clone(),
329 result: result.clone(),
330 source: Some(rule_node.source.clone()),
331 });
332 }
333
334 executable_rules.push(ExecutableRule {
335 path: rule_path.clone(),
336 name: rule_path.rule.clone(),
337 branches: executable_branches,
338 source: Some(rule_node.source.clone()),
339 needs_facts: HashSet::new(),
340 rule_type: rule_node.rule_type.clone(),
341 });
342 }
343
344 populate_needs_facts(&mut executable_rules, graph);
345
346 ExecutionPlan {
347 doc_name: main_doc_name.to_string(),
348 fact_schema,
349 fact_values,
350 doc_refs,
351 fact_sources,
352 rules: executable_rules,
353 sources: graph.sources().clone(),
354 }
355}
356
357fn coerce_literal_to_schema_type(
358 lit: &LiteralValue,
359 schema_type: &LemmaType,
360) -> Result<LiteralValue, String> {
361 use crate::semantic::TypeSpecification;
362 use crate::Value;
363
364 if lit.lemma_type.specifications == schema_type.specifications {
366 let mut out = lit.clone();
367 out.lemma_type = schema_type.clone();
368 return Ok(out);
369 }
370
371 match (&schema_type.specifications, &lit.value) {
372 (TypeSpecification::Number { .. }, Value::Number(_))
374 | (TypeSpecification::Text { .. }, Value::Text(_))
375 | (TypeSpecification::Boolean { .. }, Value::Boolean(_))
376 | (TypeSpecification::Date { .. }, Value::Date(_))
377 | (TypeSpecification::Time { .. }, Value::Time(_))
378 | (TypeSpecification::Duration { .. }, Value::Duration(_, _))
379 | (TypeSpecification::Ratio { .. }, Value::Ratio(_, _))
380 | (TypeSpecification::Scale { .. }, Value::Scale(_, _)) => {
381 let mut out = lit.clone();
382 out.lemma_type = schema_type.clone();
383 Ok(out)
384 }
385
386 (TypeSpecification::Ratio { .. }, Value::Number(n)) => {
388 Ok(LiteralValue::ratio_with_type(*n, None, schema_type.clone()))
389 }
390
391 _ => Err(format!(
392 "value {} cannot be used as type {}",
393 lit,
394 schema_type.name()
395 )),
396 }
397}
398
399fn populate_needs_facts(rules: &mut [ExecutableRule], graph: &Graph) {
400 let mut direct: HashMap<RulePath, HashSet<FactPath>> = HashMap::new();
402 for rule in rules.iter() {
403 let mut facts = HashSet::new();
404 for branch in &rule.branches {
405 if let Some(cond) = &branch.condition {
406 cond.collect_fact_paths(&mut facts);
407 }
408 branch.result.collect_fact_paths(&mut facts);
409 }
410 direct.insert(rule.path.clone(), facts);
411 }
412
413 fn compute_all_facts(
415 rule_path: &RulePath,
416 graph: &Graph,
417 direct: &HashMap<RulePath, HashSet<FactPath>>,
418 memo: &mut HashMap<RulePath, HashSet<FactPath>>,
419 visiting: &mut HashSet<RulePath>,
420 ) -> HashSet<FactPath> {
421 if let Some(cached) = memo.get(rule_path) {
422 return cached.clone();
423 }
424
425 if !visiting.insert(rule_path.clone()) {
427 return direct.get(rule_path).cloned().unwrap_or_default();
428 }
429
430 let mut out = direct.get(rule_path).cloned().unwrap_or_default();
431 if let Some(node) = graph.rules().get(rule_path) {
432 for dep in &node.depends_on_rules {
433 if direct.contains_key(dep) {
435 out.extend(compute_all_facts(dep, graph, direct, memo, visiting));
436 }
437 }
438 }
439
440 visiting.remove(rule_path);
441 memo.insert(rule_path.clone(), out.clone());
442 out
443 }
444
445 let mut memo: HashMap<RulePath, HashSet<FactPath>> = HashMap::new();
446 let mut visiting: HashSet<RulePath> = HashSet::new();
447
448 for rule in rules.iter_mut() {
449 rule.needs_facts = compute_all_facts(&rule.path, graph, &direct, &mut memo, &mut visiting);
450 }
451}
452
453impl ExecutionPlan {
454 pub fn get_fact_path_by_str(&self, name: &str) -> Option<&FactPath> {
456 self.fact_schema
457 .keys()
458 .find(|path| path.to_string() == name)
459 }
460
461 pub fn get_rule(&self, name: &str) -> Option<&ExecutableRule> {
463 self.rules
464 .iter()
465 .find(|r| r.name == name && r.path.segments.is_empty())
466 }
467
468 pub fn get_rule_by_path(&self, rule_path: &RulePath) -> Option<&ExecutableRule> {
470 self.rules.iter().find(|r| &r.path == rule_path)
471 }
472
473 pub fn get_fact_value(&self, path: &FactPath) -> Option<&LiteralValue> {
475 self.fact_values.get(path)
476 }
477
478 pub fn with_values(
482 mut self,
483 values: HashMap<String, String>,
484 limits: &ResourceLimits,
485 ) -> Result<Self, LemmaError> {
486 for (name, raw_value) in values {
487 let fact_path = self.get_fact_path_by_str(&name).ok_or_else(|| {
488 let available: Vec<String> =
489 self.fact_schema.keys().map(|p| p.to_string()).collect();
490 LemmaError::engine(
491 format!(
492 "Fact '{}' not found. Available facts: {}",
493 name,
494 available.join(", ")
495 ),
496 crate::parsing::ast::Span {
497 start: 0,
498 end: 0,
499 line: 1,
500 col: 0,
501 },
502 "<input>",
503 std::sync::Arc::from(""),
504 &self.doc_name,
505 1,
506 None::<String>,
507 )
508 })?;
509 let fact_path = fact_path.clone();
510
511 let fact_source = self
512 .fact_sources
513 .get(&fact_path)
514 .cloned()
515 .ok_or_else(|| {
516 LemmaError::engine(
517 format!(
518 "Invalid execution plan: missing source location for fact '{}'. \
519 This plan is incomplete/corrupted (missing ExecutionPlan.fact_sources entry).",
520 name
521 ),
522 crate::parsing::ast::Span {
523 start: 0,
524 end: 0,
525 line: 1,
526 col: 0,
527 },
528 "<execution-plan>",
529 std::sync::Arc::from(""),
530 &self.doc_name,
531 1,
532 None::<String>,
533 )
534 })?;
535 let source_text: Arc<str> = self
536 .sources
537 .get(&fact_source.attribute)
538 .map(|s| Arc::from(s.as_str()))
539 .unwrap_or_else(|| Arc::from(""));
540
541 let expected_type = self.fact_schema.get(&fact_path).cloned().ok_or_else(|| {
542 LemmaError::engine(
543 format!("Unknown fact: {}", name),
544 crate::parsing::ast::Span {
545 start: 0,
546 end: 0,
547 line: 1,
548 col: 0,
549 },
550 "<input>",
551 std::sync::Arc::from(""),
552 &self.doc_name,
553 1,
554 None::<String>,
555 )
556 })?;
557
558 let literal_value = expected_type
560 .parse_value(
561 &raw_value,
562 fact_source.span.clone(),
563 &fact_source.attribute,
564 &fact_source.doc_name,
565 )
566 .map_err(|e| {
567 LemmaError::engine(
568 format!(
569 "Failed to parse fact '{}' as {}: {}",
570 name,
571 expected_type.name(),
572 e
573 ),
574 fact_source.span.clone(),
575 &fact_source.attribute,
576 source_text.clone(),
577 &fact_source.doc_name,
578 1,
579 None::<String>,
580 )
581 })?;
582
583 let size = literal_value.byte_size();
585 if size > limits.max_fact_value_bytes {
586 return Err(LemmaError::ResourceLimitExceeded {
587 limit_name: "max_fact_value_bytes".to_string(),
588 limit_value: limits.max_fact_value_bytes.to_string(),
589 actual_value: size.to_string(),
590 suggestion: format!(
591 "Reduce the size of fact values to {} bytes or less",
592 limits.max_fact_value_bytes
593 ),
594 });
595 }
596
597 validate_value_against_type(&expected_type, &literal_value).map_err(|msg| {
599 LemmaError::engine(
600 format!(
601 "Invalid value for fact {} (expected {}): {}",
602 name,
603 expected_type.name(),
604 msg
605 ),
606 fact_source.span.clone(),
607 &fact_source.attribute,
608 source_text.clone(),
609 &fact_source.doc_name,
610 1,
611 None::<String>,
612 )
613 })?;
614
615 self.fact_values.insert(fact_path, literal_value);
616 }
617
618 Ok(self)
619 }
620}
621
622fn validate_value_against_type(
623 expected_type: &LemmaType,
624 value: &LiteralValue,
625) -> Result<(), String> {
626 use crate::semantic::TypeSpecification;
627 use crate::Value;
628
629 let effective_decimals = |n: rust_decimal::Decimal| n.scale();
630
631 match (&expected_type.specifications, &value.value) {
632 (
633 TypeSpecification::Number {
634 minimum,
635 maximum,
636 decimals,
637 ..
638 },
639 Value::Number(n),
640 ) => {
641 if let Some(min) = minimum {
642 if n < min {
643 return Err(format!("{} is below minimum {}", n, min));
644 }
645 }
646 if let Some(max) = maximum {
647 if n > max {
648 return Err(format!("{} is above maximum {}", n, max));
649 }
650 }
651 if let Some(d) = decimals {
652 if effective_decimals(*n) > u32::from(*d) {
653 return Err(format!("{} has more than {} decimals", n, d));
654 }
655 }
656 Ok(())
657 }
658 (
659 TypeSpecification::Scale {
660 minimum,
661 maximum,
662 decimals,
663 ..
664 },
665 Value::Scale(n, _unit),
666 ) => {
667 if let Some(min) = minimum {
668 if n < min {
669 return Err(format!("{} is below minimum {}", n, min));
670 }
671 }
672 if let Some(max) = maximum {
673 if n > max {
674 return Err(format!("{} is above maximum {}", n, max));
675 }
676 }
677 if let Some(d) = decimals {
678 if effective_decimals(*n) > u32::from(*d) {
679 return Err(format!("{} has more than {} decimals", n, d));
680 }
681 }
682 Ok(())
683 }
684 (TypeSpecification::Text { options, .. }, Value::Text(s)) => {
685 if !options.is_empty() && !options.iter().any(|opt| opt == s) {
686 return Err(format!(
687 "'{}' is not in allowed options: {}",
688 s,
689 options.join(", ")
690 ));
691 }
692 Ok(())
693 }
694 _ => Ok(()),
696 }
697}
698
699pub(crate) fn validate_literal_facts_against_types(plan: &ExecutionPlan) -> Vec<LemmaError> {
700 let mut errors = Vec::new();
701
702 for (fact_path, lit) in &plan.fact_values {
703 let Some(expected_type) = plan.fact_schema.get(fact_path) else {
704 continue;
705 };
706
707 if let Err(msg) = validate_value_against_type(expected_type, lit) {
708 let (span, attribute, source_text, doc_name) = plan
709 .fact_sources
710 .get(fact_path)
711 .map(|s| {
712 let source_text: Arc<str> = plan
713 .sources
714 .get(&s.attribute)
715 .map(|t| Arc::from(t.as_str()))
716 .unwrap_or_else(|| Arc::from(""));
717 (
718 s.span.clone(),
719 s.attribute.as_str(),
720 source_text,
721 s.doc_name.as_str(),
722 )
723 })
724 .unwrap_or_else(|| {
725 (
726 crate::parsing::ast::Span {
727 start: 0,
728 end: 0,
729 line: 1,
730 col: 0,
731 },
732 "<input>",
733 Arc::from(""),
734 plan.doc_name.as_str(),
735 )
736 });
737 errors.push(LemmaError::engine(
738 format!(
739 "Invalid value for fact {} (expected {}): {}",
740 fact_path,
741 expected_type.name(),
742 msg
743 ),
744 span,
745 attribute,
746 source_text,
747 doc_name,
748 1,
749 None::<String>,
750 ));
751 }
752 }
753
754 errors
755}
756
757#[cfg(test)]
758mod tests {
759 use super::*;
760 use crate::semantic::{BooleanValue, Expression, FactPath, LiteralValue, RulePath, Value};
761 use crate::Engine;
762 use serde_json;
763 use std::str::FromStr;
764 use std::sync::Arc;
765
766 fn default_limits() -> ResourceLimits {
767 ResourceLimits::default()
768 }
769
770 #[test]
771 fn test_with_raw_values() {
772 let mut engine = Engine::new();
773 engine
774 .add_lemma_code(
775 r#"
776 doc test
777 fact age = [number -> default 25]
778 "#,
779 "test.lemma",
780 )
781 .unwrap();
782
783 let plan = engine.get_execution_plan("test").unwrap().clone();
784 let fact_path = FactPath::local("age".to_string());
785
786 let mut values = HashMap::new();
787 values.insert("age".to_string(), "30".to_string());
788
789 let updated_plan = plan.with_values(values, &default_limits()).unwrap();
790 let updated_value = updated_plan.fact_values.get(&fact_path).unwrap();
791 match &updated_value.value {
792 Value::Number(n) => assert_eq!(n, &rust_decimal::Decimal::from(30)),
793 other => panic!("Expected number literal, got {:?}", other),
794 }
795 }
796
797 #[test]
798 fn test_with_raw_values_type_mismatch() {
799 let mut engine = Engine::new();
800 engine
801 .add_lemma_code(
802 r#"
803 doc test
804 fact age = [number]
805 "#,
806 "test.lemma",
807 )
808 .unwrap();
809
810 let plan = engine.get_execution_plan("test").unwrap().clone();
811
812 let mut values = HashMap::new();
813 values.insert("age".to_string(), "thirty".to_string());
814
815 assert!(plan.with_values(values, &default_limits()).is_err());
816 }
817
818 #[test]
819 fn test_with_raw_values_unknown_fact() {
820 let mut engine = Engine::new();
821 engine
822 .add_lemma_code(
823 r#"
824 doc test
825 fact known = [number]
826 "#,
827 "test.lemma",
828 )
829 .unwrap();
830
831 let plan = engine.get_execution_plan("test").unwrap().clone();
832
833 let mut values = HashMap::new();
834 values.insert("unknown".to_string(), "30".to_string());
835
836 assert!(plan.with_values(values, &default_limits()).is_err());
837 }
838
839 #[test]
840 fn test_with_raw_values_nested() {
841 let mut engine = Engine::new();
842 engine
843 .add_lemma_code(
844 r#"
845 doc private
846 fact base_price = [number]
847
848 doc test
849 fact rules = doc private
850 "#,
851 "test.lemma",
852 )
853 .unwrap();
854
855 let plan = engine.get_execution_plan("test").unwrap().clone();
856
857 let mut values = HashMap::new();
858 values.insert("rules.base_price".to_string(), "100".to_string());
859
860 let updated_plan = plan.with_values(values, &default_limits()).unwrap();
861 let fact_path = FactPath {
862 segments: vec![crate::semantic::PathSegment {
863 fact: "rules".to_string(),
864 doc: "private".to_string(),
865 }],
866 fact: "base_price".to_string(),
867 };
868 let updated_value = updated_plan.fact_values.get(&fact_path).unwrap();
869 match &updated_value.value {
870 Value::Number(n) => assert_eq!(n, &rust_decimal::Decimal::from(100)),
871 other => panic!("Expected number literal, got {:?}", other),
872 }
873 }
874
875 fn create_literal_expr(value: LiteralValue) -> Expression {
876 use crate::semantic::ExpressionKind;
877 Expression::new(ExpressionKind::Literal(value), None)
878 }
879
880 fn create_number_literal(n: rust_decimal::Decimal) -> LiteralValue {
881 LiteralValue::number(n)
882 }
883
884 fn create_boolean_literal(b: BooleanValue) -> LiteralValue {
885 LiteralValue::boolean(b)
886 }
887
888 fn create_text_literal(s: String) -> LiteralValue {
889 LiteralValue::text(s)
890 }
891
892 #[test]
893 fn with_values_should_enforce_number_maximum_constraint() {
894 let fact_path = FactPath::local("x".to_string());
897
898 let mut fact_schema = HashMap::new();
899 let max10 = crate::LemmaType::without_name(crate::TypeSpecification::Number {
900 minimum: None,
901 maximum: Some(rust_decimal::Decimal::from_str("10").unwrap()),
902 decimals: None,
903 precision: None,
904 help: None,
905 default: None,
906 });
907 fact_schema.insert(fact_path.clone(), max10.clone());
908 let fact_sources = HashMap::from([(
909 fact_path.clone(),
910 Source::new(
911 "<test>",
912 crate::parsing::ast::Span {
913 start: 0,
914 end: 0,
915 line: 1,
916 col: 0,
917 },
918 "test",
919 ),
920 )]);
921
922 let plan = ExecutionPlan {
923 doc_name: "test".to_string(),
924 fact_schema,
925 fact_values: HashMap::new(),
926 doc_refs: HashMap::new(),
927 fact_sources,
928 rules: Vec::new(),
929 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
930 };
931
932 let mut values = HashMap::new();
933 values.insert("x".to_string(), "11".to_string());
934
935 assert!(
936 plan.with_values(values, &default_limits()).is_err(),
937 "Providing x=11 should fail due to maximum 10"
938 );
939 }
940
941 #[test]
942 fn with_values_should_enforce_text_enum_options() {
943 let fact_path = FactPath::local("tier".to_string());
945
946 let mut fact_schema = HashMap::new();
947 let tier = crate::LemmaType::without_name(crate::TypeSpecification::Text {
948 minimum: None,
949 maximum: None,
950 length: None,
951 options: vec!["silver".to_string(), "gold".to_string()],
952 help: None,
953 default: None,
954 });
955 fact_schema.insert(fact_path.clone(), tier.clone());
956 let fact_sources = HashMap::from([(
957 fact_path.clone(),
958 Source::new(
959 "<test>",
960 crate::parsing::ast::Span {
961 start: 0,
962 end: 0,
963 line: 1,
964 col: 0,
965 },
966 "test",
967 ),
968 )]);
969
970 let plan = ExecutionPlan {
971 doc_name: "test".to_string(),
972 fact_schema,
973 fact_values: HashMap::new(),
974 doc_refs: HashMap::new(),
975 fact_sources,
976 rules: Vec::new(),
977 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
978 };
979
980 let mut values = HashMap::new();
981 values.insert("tier".to_string(), "platinum".to_string());
982
983 assert!(
984 plan.with_values(values, &default_limits()).is_err(),
985 "Invalid enum value should be rejected (tier='platinum')"
986 );
987 }
988
989 #[test]
990 fn with_values_should_enforce_scale_decimals() {
991 let fact_path = FactPath::local("price".to_string());
994
995 let mut fact_schema = HashMap::new();
996 let money = crate::LemmaType::without_name(crate::TypeSpecification::Scale {
997 minimum: None,
998 maximum: None,
999 decimals: Some(2),
1000 precision: None,
1001 units: vec![crate::semantic::Unit {
1002 name: "eur".to_string(),
1003 value: rust_decimal::Decimal::from_str("1.0").unwrap(),
1004 }],
1005 help: None,
1006 default: None,
1007 });
1008 fact_schema.insert(fact_path.clone(), money.clone());
1009 let fact_sources = HashMap::from([(
1010 fact_path.clone(),
1011 Source::new(
1012 "<test>",
1013 crate::parsing::ast::Span {
1014 start: 0,
1015 end: 0,
1016 line: 1,
1017 col: 0,
1018 },
1019 "test",
1020 ),
1021 )]);
1022
1023 let plan = ExecutionPlan {
1024 doc_name: "test".to_string(),
1025 fact_schema,
1026 fact_values: HashMap::new(),
1027 doc_refs: HashMap::new(),
1028 fact_sources,
1029 rules: Vec::new(),
1030 sources: HashMap::from([("<test>".to_string(), "".to_string())]),
1031 };
1032
1033 let mut values = HashMap::new();
1034 values.insert("price".to_string(), "1.234 eur".to_string());
1035
1036 assert!(
1037 plan.with_values(values, &default_limits()).is_err(),
1038 "Scale decimals=2 should reject 1.234 eur"
1039 );
1040 }
1041
1042 #[test]
1043 fn test_serialize_deserialize_execution_plan() {
1044 let fact_path = FactPath {
1045 segments: vec![],
1046 fact: "age".to_string(),
1047 };
1048 let plan = ExecutionPlan {
1049 doc_name: "test".to_string(),
1050 fact_schema: {
1051 let mut s = HashMap::new();
1052 s.insert(
1053 fact_path.clone(),
1054 crate::semantic::standard_number().clone(),
1055 );
1056 s
1057 },
1058 fact_values: HashMap::new(),
1059 doc_refs: HashMap::new(),
1060 fact_sources: HashMap::new(),
1061 rules: Vec::new(),
1062 sources: {
1063 let mut s = HashMap::new();
1064 s.insert("test.lemma".to_string(), "fact age: number".to_string());
1065 s
1066 },
1067 };
1068
1069 let json = serde_json::to_string(&plan).expect("Should serialize");
1070 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1071
1072 assert_eq!(deserialized.doc_name, plan.doc_name);
1073 assert_eq!(deserialized.fact_schema.len(), plan.fact_schema.len());
1074 assert_eq!(deserialized.fact_values.len(), plan.fact_values.len());
1075 assert_eq!(deserialized.doc_refs.len(), plan.doc_refs.len());
1076 assert_eq!(deserialized.fact_sources.len(), plan.fact_sources.len());
1077 assert_eq!(deserialized.rules.len(), plan.rules.len());
1078 assert_eq!(deserialized.sources.len(), plan.sources.len());
1079 }
1080
1081 #[test]
1082 fn test_serialize_deserialize_plan_with_rules() {
1083 use crate::semantic::ExpressionKind;
1084
1085 let mut plan = ExecutionPlan {
1086 doc_name: "test".to_string(),
1087 fact_schema: HashMap::new(),
1088 fact_values: HashMap::new(),
1089 doc_refs: HashMap::new(),
1090 fact_sources: HashMap::new(),
1091 rules: Vec::new(),
1092 sources: HashMap::new(),
1093 };
1094
1095 let age_path = FactPath::local("age".to_string());
1096 plan.fact_schema
1097 .insert(age_path.clone(), crate::semantic::standard_number().clone());
1098
1099 let rule = ExecutableRule {
1100 path: RulePath::local("can_drive".to_string()),
1101 name: "can_drive".to_string(),
1102 branches: vec![Branch {
1103 condition: Some(Expression::new(
1104 ExpressionKind::Comparison(
1105 Arc::new(Expression::new(
1106 ExpressionKind::FactPath(age_path.clone()),
1107 None,
1108 )),
1109 crate::ComparisonComputation::GreaterThanOrEqual,
1110 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1111 ),
1112 None,
1113 )),
1114 result: create_literal_expr(create_boolean_literal(crate::BooleanValue::True)),
1115 source: None,
1116 }],
1117 needs_facts: {
1118 let mut set = HashSet::new();
1119 set.insert(age_path);
1120 set
1121 },
1122 source: None,
1123 rule_type: crate::semantic::standard_boolean().clone(),
1124 };
1125
1126 plan.rules.push(rule);
1127
1128 let json = serde_json::to_string(&plan).expect("Should serialize");
1129 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1130
1131 assert_eq!(deserialized.doc_name, plan.doc_name);
1132 assert_eq!(deserialized.fact_schema.len(), plan.fact_schema.len());
1133 assert_eq!(deserialized.rules.len(), plan.rules.len());
1134 assert_eq!(deserialized.rules[0].name, "can_drive");
1135 assert_eq!(deserialized.rules[0].branches.len(), 1);
1136 assert_eq!(deserialized.rules[0].needs_facts.len(), 1);
1137 }
1138
1139 #[test]
1140 fn test_serialize_deserialize_plan_with_nested_fact_paths() {
1141 use crate::semantic::PathSegment;
1142 let fact_path = FactPath {
1143 segments: vec![PathSegment {
1144 fact: "employee".to_string(),
1145 doc: "private".to_string(),
1146 }],
1147 fact: "salary".to_string(),
1148 };
1149
1150 let plan = ExecutionPlan {
1151 doc_name: "test".to_string(),
1152 fact_schema: {
1153 let mut s = HashMap::new();
1154 s.insert(
1155 fact_path.clone(),
1156 crate::semantic::standard_number().clone(),
1157 );
1158 s
1159 },
1160 fact_values: HashMap::new(),
1161 doc_refs: HashMap::new(),
1162 fact_sources: HashMap::new(),
1163 rules: Vec::new(),
1164 sources: HashMap::new(),
1165 };
1166
1167 let json = serde_json::to_string(&plan).expect("Should serialize");
1168 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1169
1170 assert_eq!(deserialized.fact_schema.len(), 1);
1171 let (deserialized_path, _) = deserialized.fact_schema.iter().next().unwrap();
1172 assert_eq!(deserialized_path.segments.len(), 1);
1173 assert_eq!(deserialized_path.segments[0].fact, "employee");
1174 assert_eq!(deserialized_path.fact, "salary");
1175 }
1176
1177 #[test]
1178 fn test_serialize_deserialize_plan_with_multiple_fact_types() {
1179 let name_path = FactPath::local("name".to_string());
1180 let age_path = FactPath::local("age".to_string());
1181 let active_path = FactPath::local("active".to_string());
1182
1183 let mut fact_schema = HashMap::new();
1184 fact_schema.insert(name_path.clone(), crate::semantic::standard_text().clone());
1185 fact_schema.insert(age_path.clone(), crate::semantic::standard_number().clone());
1186 fact_schema.insert(
1187 active_path.clone(),
1188 crate::semantic::standard_boolean().clone(),
1189 );
1190
1191 let mut fact_values = HashMap::new();
1192 fact_values.insert(name_path.clone(), create_text_literal("Alice".to_string()));
1193 fact_values.insert(age_path.clone(), create_number_literal(30.into()));
1194 fact_values.insert(
1195 active_path.clone(),
1196 create_boolean_literal(crate::BooleanValue::True),
1197 );
1198
1199 let plan = ExecutionPlan {
1200 doc_name: "test".to_string(),
1201 fact_schema,
1202 fact_values,
1203 doc_refs: HashMap::new(),
1204 fact_sources: HashMap::new(),
1205 rules: Vec::new(),
1206 sources: HashMap::new(),
1207 };
1208
1209 let json = serde_json::to_string(&plan).expect("Should serialize");
1210 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1211
1212 assert_eq!(deserialized.fact_values.len(), 3);
1213
1214 assert_eq!(
1215 deserialized.fact_values.get(&name_path).unwrap().value,
1216 Value::Text("Alice".to_string())
1217 );
1218 assert_eq!(
1219 deserialized.fact_values.get(&age_path).unwrap().value,
1220 Value::Number(30.into())
1221 );
1222 assert_eq!(
1223 deserialized.fact_values.get(&active_path).unwrap().value,
1224 Value::Boolean(crate::BooleanValue::True)
1225 );
1226 }
1227
1228 #[test]
1229 fn test_serialize_deserialize_plan_with_multiple_branches() {
1230 use crate::semantic::ExpressionKind;
1231
1232 let mut plan = ExecutionPlan {
1233 doc_name: "test".to_string(),
1234 fact_schema: HashMap::new(),
1235 fact_values: HashMap::new(),
1236 doc_refs: HashMap::new(),
1237 fact_sources: HashMap::new(),
1238 rules: Vec::new(),
1239 sources: HashMap::new(),
1240 };
1241
1242 let points_path = FactPath::local("points".to_string());
1243 plan.fact_schema.insert(
1244 points_path.clone(),
1245 crate::semantic::standard_number().clone(),
1246 );
1247
1248 let rule = ExecutableRule {
1249 path: RulePath::local("tier".to_string()),
1250 name: "tier".to_string(),
1251 branches: vec![
1252 Branch {
1253 condition: None,
1254 result: create_literal_expr(create_text_literal("bronze".to_string())),
1255 source: None,
1256 },
1257 Branch {
1258 condition: Some(Expression::new(
1259 ExpressionKind::Comparison(
1260 Arc::new(Expression::new(
1261 ExpressionKind::FactPath(points_path.clone()),
1262 None,
1263 )),
1264 crate::ComparisonComputation::GreaterThanOrEqual,
1265 Arc::new(create_literal_expr(create_number_literal(100.into()))),
1266 ),
1267 None,
1268 )),
1269 result: create_literal_expr(create_text_literal("silver".to_string())),
1270 source: None,
1271 },
1272 Branch {
1273 condition: Some(Expression::new(
1274 ExpressionKind::Comparison(
1275 Arc::new(Expression::new(
1276 ExpressionKind::FactPath(points_path.clone()),
1277 None,
1278 )),
1279 crate::ComparisonComputation::GreaterThanOrEqual,
1280 Arc::new(create_literal_expr(create_number_literal(500.into()))),
1281 ),
1282 None,
1283 )),
1284 result: create_literal_expr(create_text_literal("gold".to_string())),
1285 source: None,
1286 },
1287 ],
1288 needs_facts: {
1289 let mut set = HashSet::new();
1290 set.insert(points_path);
1291 set
1292 },
1293 source: None,
1294 rule_type: crate::semantic::standard_text().clone(),
1295 };
1296
1297 plan.rules.push(rule);
1298
1299 let json = serde_json::to_string(&plan).expect("Should serialize");
1300 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1301
1302 assert_eq!(deserialized.rules.len(), 1);
1303 assert_eq!(deserialized.rules[0].branches.len(), 3);
1304 assert!(deserialized.rules[0].branches[0].condition.is_none());
1305 assert!(deserialized.rules[0].branches[1].condition.is_some());
1306 assert!(deserialized.rules[0].branches[2].condition.is_some());
1307 }
1308
1309 #[test]
1310 fn test_serialize_deserialize_empty_plan() {
1311 let plan = ExecutionPlan {
1312 doc_name: "empty".to_string(),
1313 fact_schema: HashMap::new(),
1314 fact_values: HashMap::new(),
1315 doc_refs: HashMap::new(),
1316 fact_sources: HashMap::new(),
1317 rules: Vec::new(),
1318 sources: HashMap::new(),
1319 };
1320
1321 let json = serde_json::to_string(&plan).expect("Should serialize");
1322 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1323
1324 assert_eq!(deserialized.doc_name, "empty");
1325 assert_eq!(deserialized.fact_schema.len(), 0);
1326 assert_eq!(deserialized.fact_values.len(), 0);
1327 assert_eq!(deserialized.rules.len(), 0);
1328 assert_eq!(deserialized.sources.len(), 0);
1329 }
1330
1331 #[test]
1332 fn test_serialize_deserialize_plan_with_arithmetic_expressions() {
1333 use crate::semantic::ExpressionKind;
1334
1335 let mut plan = ExecutionPlan {
1336 doc_name: "test".to_string(),
1337 fact_schema: HashMap::new(),
1338 fact_values: HashMap::new(),
1339 doc_refs: HashMap::new(),
1340 fact_sources: HashMap::new(),
1341 rules: Vec::new(),
1342 sources: HashMap::new(),
1343 };
1344
1345 let x_path = FactPath::local("x".to_string());
1346 plan.fact_schema
1347 .insert(x_path.clone(), crate::semantic::standard_number().clone());
1348
1349 let rule = ExecutableRule {
1350 path: RulePath::local("doubled".to_string()),
1351 name: "doubled".to_string(),
1352 branches: vec![Branch {
1353 condition: None,
1354 result: Expression::new(
1355 ExpressionKind::Arithmetic(
1356 Arc::new(Expression::new(
1357 ExpressionKind::FactPath(x_path.clone()),
1358 None,
1359 )),
1360 crate::ArithmeticComputation::Multiply,
1361 Arc::new(create_literal_expr(create_number_literal(2.into()))),
1362 ),
1363 None,
1364 ),
1365 source: None,
1366 }],
1367 needs_facts: {
1368 let mut set = HashSet::new();
1369 set.insert(x_path);
1370 set
1371 },
1372 source: None,
1373 rule_type: crate::semantic::standard_number().clone(),
1374 };
1375
1376 plan.rules.push(rule);
1377
1378 let json = serde_json::to_string(&plan).expect("Should serialize");
1379 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1380
1381 assert_eq!(deserialized.rules.len(), 1);
1382 match &deserialized.rules[0].branches[0].result.kind {
1383 ExpressionKind::Arithmetic(left, op, right) => {
1384 assert_eq!(*op, crate::ArithmeticComputation::Multiply);
1385 match &left.kind {
1386 ExpressionKind::FactPath(_) => {}
1387 _ => panic!("Expected FactPath in left operand"),
1388 }
1389 match &right.kind {
1390 ExpressionKind::Literal(_) => {}
1391 _ => panic!("Expected Literal in right operand"),
1392 }
1393 }
1394 _ => panic!("Expected Arithmetic expression"),
1395 }
1396 }
1397
1398 #[test]
1399 fn test_serialize_deserialize_round_trip_equality() {
1400 use crate::semantic::ExpressionKind;
1401
1402 let mut plan = ExecutionPlan {
1403 doc_name: "test".to_string(),
1404 fact_schema: HashMap::new(),
1405 fact_values: HashMap::new(),
1406 doc_refs: HashMap::new(),
1407 fact_sources: HashMap::new(),
1408 rules: Vec::new(),
1409 sources: {
1410 let mut s = HashMap::new();
1411 s.insert("test.lemma".to_string(), "fact age: number".to_string());
1412 s
1413 },
1414 };
1415
1416 let age_path = FactPath::local("age".to_string());
1417 plan.fact_schema
1418 .insert(age_path.clone(), crate::semantic::standard_number().clone());
1419
1420 let rule = ExecutableRule {
1421 path: RulePath::local("is_adult".to_string()),
1422 name: "is_adult".to_string(),
1423 branches: vec![Branch {
1424 condition: Some(Expression::new(
1425 ExpressionKind::Comparison(
1426 Arc::new(Expression::new(
1427 ExpressionKind::FactPath(age_path.clone()),
1428 None,
1429 )),
1430 crate::ComparisonComputation::GreaterThanOrEqual,
1431 Arc::new(create_literal_expr(create_number_literal(18.into()))),
1432 ),
1433 None,
1434 )),
1435 result: create_literal_expr(create_boolean_literal(crate::BooleanValue::True)),
1436 source: None,
1437 }],
1438 needs_facts: {
1439 let mut set = HashSet::new();
1440 set.insert(age_path);
1441 set
1442 },
1443 source: None,
1444 rule_type: crate::semantic::standard_boolean().clone(),
1445 };
1446
1447 plan.rules.push(rule);
1448
1449 let json = serde_json::to_string(&plan).expect("Should serialize");
1450 let deserialized: ExecutionPlan = serde_json::from_str(&json).expect("Should deserialize");
1451
1452 let json2 = serde_json::to_string(&deserialized).expect("Should serialize again");
1453 let deserialized2: ExecutionPlan =
1454 serde_json::from_str(&json2).expect("Should deserialize again");
1455
1456 assert_eq!(deserialized2.doc_name, plan.doc_name);
1457 assert_eq!(deserialized2.fact_schema.len(), plan.fact_schema.len());
1458 assert_eq!(deserialized2.rules.len(), plan.rules.len());
1459 assert_eq!(deserialized2.sources.len(), plan.sources.len());
1460 assert_eq!(deserialized2.rules[0].name, plan.rules[0].name);
1461 assert_eq!(
1462 deserialized2.rules[0].branches.len(),
1463 plan.rules[0].branches.len()
1464 );
1465 }
1466}