Skip to main content

conjure_cp_core/rule_engine/
rule.rs

1use std::collections::BTreeSet;
2use std::fmt::{self, Debug, Display, Formatter};
3use std::hash::Hash;
4use std::sync::Arc;
5
6use thiserror::Error;
7
8use crate::Model;
9use crate::ast::{
10    CnfClause, DeclarationKind, DeclarationPtr, Expression, Metadata, Name, SymbolTable,
11};
12
13#[derive(Debug, Error)]
14pub enum ApplicationError {
15    #[error("Rule is not applicable")]
16    RuleNotApplicable,
17
18    #[error("Could not calculate the expression domain")]
19    DomainError,
20}
21
22/// Represents the result of applying a rule to an expression within a model.
23///
24/// A `RuleEffect` encapsulates the changes made to a model during a rule application.
25/// It includes a new expression to replace the original one, an optional top-level constraint
26/// to be added to the model, and any updates to the model's symbol table.
27///
28/// This struct allows for representing side-effects of rule applications, ensuring that
29/// all modifications, including symbol table expansions and additional constraints, are
30/// accounted for and can be applied to the model consistently.
31///
32/// # Fields
33/// - `new_expression`: The updated [`Expression`] that replaces the original one after applying the rule.
34/// - `new_top`: An additional top-level [`Vec<Expression>`] constraint that should be added to the model. If no top-level
35///   constraint is needed, this field can be set to an empty vector [`Vec::new()`].
36/// - `symbols`: A [`SymbolTable`] containing any new symbol definitions or modifications to be added to the model's
37///   symbol table. If no symbols are modified, this field can be set to an empty symbol table.
38///
39/// # Usage
40/// A `RuleEffect` can be created using one of the provided constructors:
41/// - [`RuleEffect::new`]: Creates an effect with a new expression, top-level constraint, and symbol modifications.
42/// - [`RuleEffect::pure`]: Creates an effect with only a new expression and no side-effects on the symbol table or constraints.
43/// - [`RuleEffect::with_symbols`]: Creates an effect with a new expression and symbol table modifications, but no top-level constraint.
44/// - [`RuleEffect::with_top`]: Creates an effect with a new expression and a top-level constraint, but no symbol table modifications.
45/// - [`RuleEffect::cnf`]: Creates an effect with a new expression, cnf clauses and symbol modifications, but no top-level constraints.
46///
47/// The `apply` method allows for applying the changes represented by the `RuleEffect` to a [`Model`].
48///
49/// # Example
50/// ```
51/// // Need to add an example
52/// ```
53///
54/// # See Also
55/// - [`ApplicationResult`]: Represents the result of applying a rule, which may either be a `RuleEffect` or an `ApplicationError`.
56/// - [`Model`]: The structure to which the `RuleEffect` changes are applied.
57#[non_exhaustive]
58#[derive(Clone)]
59pub struct RuleEffect {
60    pub new_expression: Expression,
61    pub new_top: Vec<Expression>,
62    pub symbols: SymbolTable,
63    pub new_clauses: Vec<CnfClause>,
64    /// Shared declarations to update if this effect is selected.
65    pub(crate) declaration_updates: Vec<DeclarationUpdate>,
66    materialise: Option<DeferredRuleEffect>,
67}
68
69/// An in-place update to a shared declaration, applied only after its rule is selected.
70#[derive(Clone, Debug)]
71pub(crate) struct DeclarationUpdate {
72    target: DeclarationPtr,
73    replacement_kind: DeclarationKind,
74}
75
76impl DeclarationUpdate {
77    /// Creates a deferred replacement for a shared declaration's kind.
78    fn new(target: DeclarationPtr, replacement_kind: DeclarationKind) -> Self {
79        Self {
80            target,
81            replacement_kind,
82        }
83    }
84
85    /// Returns the name of the declaration that will be updated.
86    pub(crate) fn name(&self) -> Name {
87        self.target.name().clone()
88    }
89
90    /// Commits the replacement while retaining the declaration's identity.
91    pub(crate) fn apply(mut self) {
92        let _ = self.target.replace_kind(self.replacement_kind);
93    }
94
95    /// Creates an unshared declaration containing the pending replacement.
96    fn preview(&self) -> DeclarationPtr {
97        DeclarationPtr::new(self.name(), self.replacement_kind.clone())
98    }
99}
100
101/// Deferred constructor for a concrete rule effect.
102type DeferredRuleEffect = Arc<dyn Fn(&SymbolTable) -> RuleEffect + Send + Sync>;
103
104/// The result of applying a rule to an expression.
105/// Contains either a set of rule effects or an error.
106pub type ApplicationResult = Result<RuleEffect, ApplicationError>;
107
108impl Debug for RuleEffect {
109    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
110        f.debug_struct("RuleEffect")
111            .field("new_expression", &self.new_expression)
112            .field("new_top", &self.new_top)
113            .field("symbols", &self.symbols)
114            .field("new_clauses", &self.new_clauses)
115            .field("declaration_updates", &self.declaration_updates)
116            .field("is_deferred", &self.materialise.is_some())
117            .finish()
118    }
119}
120
121impl RuleEffect {
122    pub fn new(new_expression: Expression, new_top: Vec<Expression>, symbols: SymbolTable) -> Self {
123        Self {
124            new_expression,
125            new_top,
126            symbols,
127            new_clauses: Vec::new(),
128            declaration_updates: Vec::new(),
129            materialise: None,
130        }
131    }
132
133    /// Represents an effect with no side effects on the model.
134    pub fn pure(new_expression: Expression) -> Self {
135        Self {
136            new_expression,
137            new_top: Vec::new(),
138            symbols: SymbolTable::new(),
139            new_clauses: Vec::new(),
140            declaration_updates: Vec::new(),
141            materialise: None,
142        }
143    }
144
145    /// Represents an effect that also modifies the symbol table.
146    pub fn with_symbols(new_expression: Expression, symbols: SymbolTable) -> Self {
147        Self {
148            new_expression,
149            new_top: Vec::new(),
150            symbols,
151            new_clauses: Vec::new(),
152            declaration_updates: Vec::new(),
153            materialise: None,
154        }
155    }
156
157    /// Represents an effect that also adds a top-level constraint to the model.
158    pub fn with_top(new_expression: Expression, new_top: Vec<Expression>) -> Self {
159        Self {
160            new_expression,
161            new_top,
162            symbols: SymbolTable::new(),
163            new_clauses: Vec::new(),
164            declaration_updates: Vec::new(),
165            materialise: None,
166        }
167    }
168
169    /// Represents an effect that also adds clauses to the model.
170    pub fn cnf(
171        new_expression: Expression,
172        new_clauses: Vec<CnfClause>,
173        symbols: SymbolTable,
174    ) -> Self {
175        Self {
176            new_expression,
177            new_top: Vec::new(),
178            symbols,
179            new_clauses,
180            declaration_updates: Vec::new(),
181            materialise: None,
182        }
183    }
184
185    /// Defers constructing a concrete effect until the rewriter chooses to apply this rule.
186    ///
187    /// This is intended for rule effects that allocate fresh names or otherwise depend on global
188    /// model state. Applicability checks can return a deferred effect without consuming those
189    /// effects; the rewriter calls [`RuleEffect::materialise`] only for the selected rule.
190    pub fn deferred(
191        materialise: impl Fn(&SymbolTable) -> RuleEffect + Send + Sync + 'static,
192    ) -> Self {
193        Self {
194            new_expression: Expression::Root(Metadata::new(), Vec::new()),
195            new_top: Vec::new(),
196            symbols: SymbolTable::new(),
197            new_clauses: Vec::new(),
198            declaration_updates: Vec::new(),
199            materialise: Some(Arc::new(materialise)),
200        }
201    }
202
203    /// Returns the concrete effect for the current symbol table.
204    ///
205    /// This consumes the selected effect: cloning a concrete effect can duplicate its expression,
206    /// top-level constraints, clauses, and speculative symbol table. Before returning, the symbol
207    /// snapshot is reduced to the bindings that the effect actually changes.
208    pub fn materialise(mut self, symbols: &SymbolTable) -> Self {
209        if let Some(materialise) = self.materialise.take() {
210            return materialise(symbols).materialise(symbols);
211        }
212
213        self.symbols.retain_local_changes_from(symbols);
214        self
215    }
216
217    pub(crate) fn is_deferred(&self) -> bool {
218        self.materialise.is_some()
219    }
220
221    /// Adds declaration replacements that are committed only if this effect is selected.
222    pub fn with_declaration_updates(
223        mut self,
224        updates: impl IntoIterator<Item = (DeclarationPtr, DeclarationKind)>,
225    ) -> Self {
226        self.declaration_updates.extend(
227            updates
228                .into_iter()
229                .map(|(target, kind)| DeclarationUpdate::new(target, kind)),
230        );
231        self
232    }
233
234    /// Iterates over the names changed by deferred declaration updates.
235    pub fn updated_declaration_names(&self) -> impl Iterator<Item = Name> + '_ {
236        self.declaration_updates.iter().map(DeclarationUpdate::name)
237    }
238
239    /// Applies pending declaration replacements to a detached symbol-table preview.
240    pub(crate) fn preview_declaration_updates(&self, symbols: &mut SymbolTable) {
241        for update in &self.declaration_updates {
242            symbols.update_insert(update.preview());
243        }
244    }
245
246    /// Applies side-effects (e.g. symbol table updates)
247    pub fn apply(self, model: &mut Model) {
248        debug_assert!(
249            self.materialise.is_none(),
250            "deferred rule effects must be materialised before being applied"
251        );
252        for update in self.declaration_updates {
253            update.apply();
254        }
255        model.symbols_mut().extend(self.symbols); // Add new assignments to the symbol table
256        model.add_constraints(self.new_top);
257        model.add_clauses(self.new_clauses);
258    }
259
260    /// Gets symbols added by this effect.
261    ///
262    /// Walks this effect's own symbols rather than diffing two whole tables. The rewriter asks
263    /// this once per applied rule, and most effects are pure, so diffing made every rewrite cost
264    /// a clone and a sort of the entire model symbol table.
265    pub fn added_symbols(&self, initial_symbols: &SymbolTable) -> BTreeSet<Name> {
266        self.symbols
267            .iter_local()
268            .filter(|(name, _)| initial_symbols.lookup_local(name).is_none())
269            .map(|(name, _)| name.clone())
270            .collect()
271    }
272
273    /// Gets symbols changed by this effect.
274    ///
275    /// Returns a list of tuples of (name, domain before effect, domain after effect), ordered by
276    /// name.
277    ///
278    /// Walks this effect's symbols for the same reason as [`RuleEffect::added_symbols`]: a symbol
279    /// can only have changed if this effect carries its new value.
280    pub fn changed_symbols(
281        &self,
282        initial_symbols: &SymbolTable,
283    ) -> Vec<(Name, DeclarationPtr, DeclarationPtr)> {
284        let mut changes: Vec<(Name, DeclarationPtr, DeclarationPtr)> = self
285            .symbols
286            .iter_local()
287            .filter_map(|(name, new_value)| {
288                let initial_value = initial_symbols.lookup_local(name)?;
289                (!new_value.content_eq(&initial_value))
290                    .then(|| (name.clone(), initial_value.clone(), new_value.clone()))
291            })
292            .collect();
293        changes.sort_by(|(lhs, ..), (rhs, ..)| lhs.cmp(rhs));
294        changes
295    }
296}
297
298/// The function type used in a [`Rule`].
299pub type RuleFn = fn(&Expression, &SymbolTable) -> ApplicationResult;
300
301/// Atomic expression subvariants that can be used as rule prefilters.
302#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
303pub enum AtomKind {
304    /// An `Atomic` expression containing a literal.
305    Literal,
306    /// An `Atomic` expression containing a reference.
307    Reference,
308}
309
310/// A complete rule prefilter alternative.
311///
312/// A rule matches when any of its prefilter alternatives matches the focused expression. This keeps
313/// compound filters such as `And / Comprehension` paired instead of forming a cross product with
314/// other alternatives in the same rule declaration.
315#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
316pub enum RulePrefilter {
317    /// Focused expression must have this `Expression` variant.
318    Variant(usize),
319    /// Focused expression must have an immediate child with this `Expression` variant.
320    Child { child: usize },
321    /// Focused expression must have this variant and an immediate child with `child`'s variant.
322    VariantChild { variant: usize, child: usize },
323    /// Focused expression must be an `Atomic` expression with this atomic subvariant.
324    Atom(AtomKind),
325    /// Focused expression must have an immediate `Atomic` child with this atomic subvariant.
326    ChildAtom(AtomKind),
327}
328
329/// State changes that can invalidate a failed rule application.
330///
331/// Most rules may become applicable when either their focused expression or the symbol table
332/// changes. A small number of Root rules use the expression only as an entry point and decide
333/// applicability entirely from declarations. Remembering their failed applications until the
334/// symbol table changes avoids repeatedly scanning every declaration after unrelated expression
335/// rewrites.
336#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
337pub enum RuleFailureInvalidation {
338    /// Reconsider a failed application after any relevant expression or symbol-table change.
339    #[default]
340    ExpressionOrSymbols,
341    /// Reconsider a failed application only after the symbol table changes.
342    SymbolsOnly,
343}
344
345/**
346 * A rule with a name, application function, and rule sets.
347 *
348 * # Fields
349 * - `name` The name of the rule.
350 * - `application` The function to apply the rule.
351 * - `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.
352 */
353#[derive(Clone, Debug)]
354pub struct Rule<'a> {
355    pub name: &'a str,
356    pub application: RuleFn,
357    pub rule_sets: &'a [(&'a str, u16)], // (name, priority). At runtime, we add the rule to rulesets
358    /// Complete prefilter alternatives this rule applies to, or `None` for universal rules.
359    pub prefilters: Option<&'static [RulePrefilter]>,
360    /// Which state changes can make a failed application become applicable.
361    pub failure_invalidation: RuleFailureInvalidation,
362}
363
364impl<'a> Rule<'a> {
365    pub const fn new(
366        name: &'a str,
367        application: RuleFn,
368        rule_sets: &'a [(&'static str, u16)],
369    ) -> Self {
370        Self {
371            name,
372            application,
373            rule_sets,
374            prefilters: None,
375            failure_invalidation: RuleFailureInvalidation::ExpressionOrSymbols,
376        }
377    }
378
379    pub fn apply(&self, expr: &Expression, symbols: &SymbolTable) -> ApplicationResult {
380        (self.application)(expr, symbols)
381    }
382}
383
384impl Display for Rule<'_> {
385    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
386        write!(f, "{}", self.name)
387    }
388}
389
390impl PartialEq for Rule<'_> {
391    fn eq(&self, other: &Self) -> bool {
392        self.name == other.name
393    }
394}
395
396impl Eq for Rule<'_> {}
397
398impl Hash for Rule<'_> {
399    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
400        self.name.hash(state);
401    }
402}
403
404#[cfg(test)]
405mod tests {
406    use super::*;
407    use crate::ast::{DecisionVariable, Domain, Range, Reference};
408    use crate::rule_engine::rewriter_common::{
409        snapshot_symbols_after_effect, snapshot_variable_declarations,
410    };
411
412    #[test]
413    fn declaration_updates_are_applied_only_when_effect_is_committed() {
414        let old_domain = Domain::int(vec![Range::Bounded(1, 3)]);
415        let new_domain = Domain::int(vec![Range::Bounded(1, 2)]);
416        let mut model = Model::new(Default::default());
417        let declaration = DeclarationPtr::new_find(Name::user("x"), old_domain.clone());
418        let reference = Reference::new(declaration.clone());
419        model.symbols_mut().insert(declaration.clone()).unwrap();
420
421        let effect = RuleEffect::pure(Expression::from(1)).with_declaration_updates([(
422            declaration,
423            DeclarationKind::Find(DecisionVariable::new(new_domain.clone())),
424        )]);
425
426        let before = snapshot_variable_declarations(&model.symbols());
427        let after = snapshot_symbols_after_effect(&model.symbols(), &effect);
428        assert_eq!(before.get(&Name::user("x")).unwrap(), "find x: int(1..3)");
429        assert_eq!(after.get(&Name::user("x")).unwrap(), "find x: int(1..2)");
430        assert_eq!(reference.domain(), Some(old_domain));
431        effect.apply(&mut model);
432        assert_eq!(reference.domain(), Some(new_domain));
433    }
434}