lemma/
engine.rs

1use crate::evaluator::Evaluator;
2use crate::{parse, LemmaDoc, LemmaError, LemmaResult, ResourceLimits, Response, Validator};
3use std::collections::HashMap;
4
5/// Engine for evaluating Lemma rules
6///
7/// Pure Rust implementation that evaluates Lemma docs directly from the AST.
8pub struct Engine {
9    documents: HashMap<String, LemmaDoc>,
10    sources: HashMap<String, String>,
11    validator: Validator,
12    evaluator: Evaluator,
13    limits: ResourceLimits,
14}
15
16impl Default for Engine {
17    fn default() -> Self {
18        Self {
19            documents: HashMap::new(),
20            sources: HashMap::new(),
21            validator: Validator,
22            evaluator: Evaluator,
23            limits: ResourceLimits::default(),
24        }
25    }
26}
27
28impl Engine {
29    pub fn new() -> Self {
30        Self::default()
31    }
32
33    /// Create an engine with custom resource limits
34    pub fn with_limits(limits: ResourceLimits) -> Self {
35        Self {
36            documents: HashMap::new(),
37            sources: HashMap::new(),
38            validator: Validator,
39            evaluator: Evaluator,
40            limits,
41        }
42    }
43
44    /// Get the current resource limits
45    pub fn limits(&self) -> &ResourceLimits {
46        &self.limits
47    }
48
49    pub fn add_lemma_code(&mut self, lemma_code: &str, source: &str) -> LemmaResult<()> {
50        let new_docs = parse(lemma_code, Some(source.to_owned()), &self.limits)?;
51
52        for doc in &new_docs {
53            let source_id = doc.source.clone().unwrap_or_else(|| "<input>".to_owned());
54            self.sources.insert(source_id, lemma_code.to_owned());
55        }
56
57        let mut all_docs: Vec<crate::LemmaDoc> = self.documents.values().cloned().collect();
58        all_docs.extend(new_docs);
59
60        let validated = self.validator.validate_all(all_docs)?;
61
62        for doc in validated.documents {
63            self.documents.insert(doc.name.clone(), doc);
64        }
65
66        Ok(())
67    }
68
69    pub fn remove_document(&mut self, doc_name: &str) {
70        self.documents.remove(doc_name);
71    }
72
73    pub fn list_documents(&self) -> Vec<String> {
74        self.documents.keys().cloned().collect()
75    }
76
77    pub fn get_document(&self, doc_name: &str) -> Option<&crate::LemmaDoc> {
78        self.documents.get(doc_name)
79    }
80
81    pub fn get_document_facts(&self, doc_name: &str) -> Vec<&crate::LemmaFact> {
82        if let Some(doc) = self.documents.get(doc_name) {
83            doc.facts.iter().collect()
84        } else {
85            Vec::new()
86        }
87    }
88
89    pub fn get_document_rules(&self, doc_name: &str) -> Vec<&crate::LemmaRule> {
90        if let Some(doc) = self.documents.get(doc_name) {
91            doc.rules.iter().collect()
92        } else {
93            Vec::new()
94        }
95    }
96
97    /// Evaluate rules in a document with optional fact overrides
98    ///
99    /// If `rule_names` is None, evaluates all rules.
100    /// If `rule_names` is Some, only returns results for the specified rules,
101    /// but still computes their dependencies.
102    ///
103    /// Fact overrides must be pre-parsed using `parse_facts()`.
104    pub fn evaluate(
105        &self,
106        doc_name: &str,
107        rule_names: Option<Vec<String>>,
108        fact_overrides: Option<Vec<crate::LemmaFact>>,
109    ) -> LemmaResult<Response> {
110        let overrides = fact_overrides.unwrap_or_default();
111
112        for fact in &overrides {
113            if let crate::FactValue::Literal(lit) = &fact.value {
114                let size = lit.byte_size();
115                if size > self.limits.max_fact_value_bytes {
116                    return Err(LemmaError::ResourceLimitExceeded {
117                        limit_name: "max_fact_value_bytes".to_string(),
118                        limit_value: self.limits.max_fact_value_bytes.to_string(),
119                        actual_value: size.to_string(),
120                        suggestion: format!(
121                            "Reduce the size of fact values to {} bytes or less",
122                            self.limits.max_fact_value_bytes
123                        ),
124                    });
125                }
126            }
127        }
128
129        self.evaluator.evaluate_document(
130            doc_name,
131            &self.documents,
132            &self.sources,
133            overrides,
134            rule_names,
135            &self.limits,
136        )
137    }
138
139    /// Get all documents (needed by serializers for schema resolution)
140    pub fn get_all_documents(&self) -> &HashMap<String, crate::LemmaDoc> {
141        &self.documents
142    }
143
144    /// Invert a rule to find input domains that produce a desired outcome
145    ///
146    /// Returns a vector of solutions, where each solution is a map from
147    /// fact paths to their valid domains. Multiple solutions represent different
148    /// ways to satisfy the target outcome (disjunction).
149    ///
150    /// Use `given_facts` to constrain the search to specific known values.
151    pub fn invert(
152        &self,
153        document: &str,
154        rule: &str,
155        target: crate::Target,
156        given_facts: HashMap<String, crate::LiteralValue>,
157    ) -> LemmaResult<Vec<HashMap<crate::FactReference, crate::Domain>>> {
158        let shape = crate::inversion::inverter::invert(
159            document,
160            rule,
161            target,
162            given_facts,
163            &self.documents,
164        )?;
165        crate::inversion::domain_extraction::shape_to_domains(&shape)
166    }
167}