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}