conjure_core/rule_engine/
rule.rs

1use std::collections::BTreeSet;
2use std::fmt::{self, Display, Formatter};
3use std::hash::Hash;
4use std::rc::Rc;
5
6use thiserror::Error;
7
8use crate::ast::Declaration;
9use crate::ast::{Expression, Name, SubModel, SymbolTable};
10use tree_morph::Rule as MorphRule;
11
12#[derive(Debug, Error)]
13pub enum ApplicationError {
14    #[error("Rule is not applicable")]
15    RuleNotApplicable,
16
17    #[error("Could not calculate the expression domain")]
18    DomainError,
19}
20
21/// Represents the result of applying a rule to an expression within a model.
22///
23/// A `Reduction` encapsulates the changes made to a model during a rule application.
24/// It includes a new expression to replace the original one, an optional top-level constraint
25/// to be added to the model, and any updates to the model's symbol table.
26///
27/// This struct allows for representing side-effects of rule applications, ensuring that
28/// all modifications, including symbol table expansions and additional constraints, are
29/// accounted for and can be applied to the model consistently.
30///
31/// # Fields
32/// - `new_expression`: The updated [`Expression`] that replaces the original one after applying the rule.
33/// - `new_top`: An additional top-level [`Vec<Expression>`] constraint that should be added to the model. If no top-level
34///   constraint is needed, this field can be set to an empty vector [`Vec::new()`].
35/// - `symbols`: A [`SymbolTable`] containing any new symbol definitions or modifications to be added to the model's
36///   symbol table. If no symbols are modified, this field can be set to an empty symbol table.
37///
38/// # Usage
39/// A `Reduction` can be created using one of the provided constructors:
40/// - [`Reduction::new`]: Creates a reduction with a new expression, top-level constraint, and symbol modifications.
41/// - [`Reduction::pure`]: Creates a reduction with only a new expression and no side-effects on the symbol table or constraints.
42/// - [`Reduction::with_symbols`]: Creates a reduction with a new expression and symbol table modifications, but no top-level constraint.
43/// - [`Reduction::with_top`]: Creates a reduction with a new expression and a top-level constraint, but no symbol table modifications.
44///
45/// The `apply` method allows for applying the changes represented by the `Reduction` to a [`Model`].
46///
47/// # Example
48/// ```
49/// // Need to add an example
50/// ```
51///
52/// # See Also
53/// - [`ApplicationResult`]: Represents the result of applying a rule, which may either be a `Reduction` or an `ApplicationError`.
54/// - [`Model`]: The structure to which the `Reduction` changes are applied.
55#[non_exhaustive]
56#[derive(Clone, Debug)]
57pub struct Reduction {
58    pub new_expression: Expression,
59    pub new_top: Vec<Expression>,
60    pub symbols: SymbolTable,
61}
62
63/// The result of applying a rule to an expression.
64/// Contains either a set of reduction instructions or an error.
65pub type ApplicationResult = Result<Reduction, ApplicationError>;
66
67impl Reduction {
68    pub fn new(new_expression: Expression, new_top: Vec<Expression>, symbols: SymbolTable) -> Self {
69        Self {
70            new_expression,
71            new_top,
72            symbols,
73        }
74    }
75
76    /// Represents a reduction with no side effects on the model.
77    pub fn pure(new_expression: Expression) -> Self {
78        Self {
79            new_expression,
80            new_top: Vec::new(),
81            symbols: SymbolTable::new(),
82        }
83    }
84
85    /// Represents a reduction that also modifies the symbol table.
86    pub fn with_symbols(new_expression: Expression, symbols: SymbolTable) -> Self {
87        Self {
88            new_expression,
89            new_top: Vec::new(),
90            symbols,
91        }
92    }
93
94    /// Represents a reduction that also adds a top-level constraint to the model.
95    pub fn with_top(new_expression: Expression, new_top: Vec<Expression>) -> Self {
96        Self {
97            new_expression,
98            new_top,
99            symbols: SymbolTable::new(),
100        }
101    }
102
103    /// Applies side-effects (e.g. symbol table updates)
104    pub fn apply(self, model: &mut SubModel) {
105        model.symbols_mut().extend(self.symbols); // Add new assignments to the symbol table
106        model.add_constraints(self.new_top.clone());
107    }
108
109    /// Gets symbols added by this reduction
110    pub fn added_symbols(&self, initial_symbols: &SymbolTable) -> BTreeSet<Name> {
111        let initial_symbols_set: BTreeSet<Name> = initial_symbols
112            .clone()
113            .into_iter_local()
114            .map(|x| x.0)
115            .collect();
116        let new_symbols_set: BTreeSet<Name> = self
117            .symbols
118            .clone()
119            .into_iter_local()
120            .map(|x| x.0)
121            .collect();
122
123        new_symbols_set
124            .difference(&initial_symbols_set)
125            .cloned()
126            .collect()
127    }
128
129    /// Gets symbols changed by this reduction
130    ///
131    /// Returns a list of tuples of (name, domain before reduction, domain after reduction)
132    pub fn changed_symbols(
133        &self,
134        initial_symbols: &SymbolTable,
135    ) -> Vec<(Name, Rc<Declaration>, Rc<Declaration>)> {
136        let mut changes: Vec<(Name, Rc<Declaration>, Rc<Declaration>)> = vec![];
137
138        for (var_name, initial_value) in initial_symbols.clone().into_iter_local() {
139            let Some(new_value) = self.symbols.lookup(&var_name) else {
140                continue;
141            };
142
143            if new_value != initial_value {
144                changes.push((var_name.clone(), initial_value.clone(), new_value.clone()));
145            }
146        }
147        changes
148    }
149}
150
151/// The function type used in a [`Rule`].
152pub type RuleFn = fn(&Expression, &SymbolTable) -> ApplicationResult;
153
154/**
155 * A rule with a name, application function, and rule sets.
156 *
157 * # Fields
158 * - `name` The name of the rule.
159 * - `application` The function to apply the rule.
160 * - `rule_sets` A list of rule set names and priorities that this rule is a part of. This is used to populate rulesets at runtime.
161 */
162#[derive(Clone, Debug)]
163pub struct Rule<'a> {
164    pub name: &'a str,
165    pub application: RuleFn,
166    pub rule_sets: &'a [(&'a str, u16)], // (name, priority). At runtime, we add the rule to rulesets
167}
168
169impl<'a> Rule<'a> {
170    pub const fn new(
171        name: &'a str,
172        application: RuleFn,
173        rule_sets: &'a [(&'static str, u16)],
174    ) -> Self {
175        Self {
176            name,
177            application,
178            rule_sets,
179        }
180    }
181
182    pub fn apply(&self, expr: &Expression, symbols: &SymbolTable) -> ApplicationResult {
183        (self.application)(expr, symbols)
184    }
185}
186
187impl Display for Rule<'_> {
188    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
189        write!(f, "{}", self.name)
190    }
191}
192
193impl PartialEq for Rule<'_> {
194    fn eq(&self, other: &Self) -> bool {
195        self.name == other.name
196    }
197}
198
199impl Eq for Rule<'_> {}
200
201impl Hash for Rule<'_> {
202    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
203        self.name.hash(state);
204    }
205}
206
207impl MorphRule<Expression, SymbolTable> for Rule<'_> {
208    fn apply(
209        &self,
210        commands: &mut tree_morph::Commands<Expression, SymbolTable>,
211        subtree: &Expression,
212        meta: &SymbolTable,
213    ) -> Option<Expression> {
214        let reduction = self.apply(subtree, meta).ok()?;
215        commands.mut_meta(Box::new(|m: &mut SymbolTable| m.extend(reduction.symbols)));
216        commands.transform(Box::new(|m| m.extend_root(reduction.new_top)));
217        Some(reduction.new_expression)
218    }
219}
220
221impl MorphRule<Expression, SymbolTable> for &Rule<'_> {
222    fn apply(
223        &self,
224        commands: &mut tree_morph::Commands<Expression, SymbolTable>,
225        subtree: &Expression,
226        meta: &SymbolTable,
227    ) -> Option<Expression> {
228        let reduction = Rule::apply(self, subtree, meta).ok()?;
229        commands.mut_meta(Box::new(|m: &mut SymbolTable| m.extend(reduction.symbols)));
230        if !reduction.new_top.is_empty() {
231            commands.transform(Box::new(|m| m.extend_root(reduction.new_top)));
232        }
233        Some(reduction.new_expression)
234    }
235}