lemma/planning/
execution_plan.rs

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