conjure_core/rule_engine/rewrite.rs
1use std::env;
2
3use itertools::Itertools;
4use uniplate::Uniplate;
5
6use crate::ast::{Expression, ReturnType};
7use crate::bug;
8use crate::rule_engine::{get_rules, Reduction, RuleSet};
9use crate::stats::RewriterStats;
10use crate::Model;
11
12use super::resolve_rules::RuleData;
13use super::rewriter_common::{log_rule_application, RewriteError, RuleResult};
14
15/// Checks if the OPTIMIZATIONS environment variable is set to "1".
16fn optimizations_enabled() -> bool {
17 match env::var("OPTIMIZATIONS") {
18 Ok(val) => val == "1",
19 Err(_) => false, // Assume optimizations are disabled if the environment variable is not set
20 }
21}
22
23/// Rewrites the given model by applying a set of rules to all its constraints, until no more rules can be applied.
24///
25/// Rules are applied in order of priority (from highest to lowest)
26/// Rules can:
27/// - Apply transformations to the constraints in the model (or their sub-expressions)
28/// - Add new constraints to the model
29/// - Modify the symbol table (e.g. add new variables)
30///
31/// # Parameters
32/// - `model`: A reference to the [`Model`] to be rewritten.
33/// - `rule_sets`: A vector of references to [`RuleSet`]s to be applied.
34///
35/// Each `RuleSet` contains a map of rules to priorities.
36///
37/// # Returns
38/// - `Ok(Model)`: If successful, it returns a modified copy of the [`Model`]
39/// - `Err(RewriteError)`: If an error occurs during rule application (e.g., invalid rules)
40///
41/// # Side-Effects
42/// - Rules can apply side-effects to the model (e.g. adding new constraints or variables).
43/// The original model is cloned and a modified copy is returned.
44/// - Rule engine statistics (e.g. number of rule applications, run time) are collected and stored in the new model's context.
45///
46/// # Example
47/// - Using `rewrite_model` with the constraint `(a + min(x, y)) = b`
48///
49/// Original model:
50/// ```text
51/// model: {
52/// constraints: [(a + min(x, y) + 42 - 10) = b],
53/// symbols: [a, b, x, y]
54/// }
55/// rule_sets: [{
56/// name: "MyRuleSet",
57/// rules: [
58/// min_to_var: 10,
59/// const_eval: 20
60/// ]
61/// }]
62/// ```
63///
64/// Rules:
65/// - `min_to_var`: min([a, b]) ~> c ; c <= a & c <= b & (c = a \/ c = b)
66/// - `const_eval`: c1 + c2 ~> (c1 + c2) ; c1, c2 are constants
67///
68/// Result:
69/// ```text
70/// model: {
71/// constraints: [
72/// (a + aux + 32) = b,
73/// aux <= x,
74/// aux <= y,
75/// aux = x \/ aux = y
76/// ],
77/// symbols: [a, b, x, y, aux]
78/// }
79/// ```
80///
81/// Process:
82/// 1. We traverse the expression tree until a rule can be applied.
83/// 2. If multiple rules can be applied to the same expression, the higher priority one goes first.
84/// In this case, `const_eval` is applied before `min_to_var`.
85/// 3. The rule `min_to_var` adds a new variable `aux` and new constraints to the model.
86/// 4. When no more rules can be applied, the resulting model is returned.
87///
88/// Details for this process can be found in [`rewrite_iteration`] documentation.
89///
90/// # Performance Considerations
91/// - We recursively traverse the tree multiple times to check if any rules can be applied.
92/// - Expressions are cloned on each rule application
93///
94/// This can be expensive for large models
95///
96/// # Panics
97/// - This function may panic if the model's context is unavailable or if there is an issue with locking the context.
98///
99/// # See Also
100/// - [`get_rules`]: Resolves the rules from the provided rule sets and sorts them by priority.
101/// - [`rewrite_iteration`]: Executes a single iteration of rewriting the model using the specified rules.
102pub fn rewrite_model<'a>(
103 model: &Model,
104 rule_sets: &Vec<&'a RuleSet<'a>>,
105) -> Result<Model, RewriteError> {
106 let rules = get_rules(rule_sets)?.into_iter().collect();
107 let mut new_model = model.clone();
108 let mut stats = RewriterStats {
109 is_optimization_enabled: Some(optimizations_enabled()),
110 rewriter_run_time: None,
111 rewriter_rule_application_attempts: Some(0),
112 rewriter_rule_applications: Some(0),
113 };
114
115 // Check if optimizations are enabled
116 let apply_optimizations = optimizations_enabled();
117
118 let start = std::time::Instant::now();
119
120 //the while loop is exited when None is returned implying the sub-expression is clean
121 let mut i: usize = 0;
122 while i < new_model.as_submodel().constraints().len() {
123 while let Some(step) = rewrite_iteration(
124 &new_model.as_submodel().constraints()[i],
125 &new_model,
126 &rules,
127 apply_optimizations,
128 &mut stats,
129 ) {
130 debug_assert!(is_vec_bool(&step.new_top)); // All new_top expressions should be boolean
131 new_model.as_submodel_mut().constraints_mut()[i] = step.new_expression.clone();
132 step.apply(new_model.as_submodel_mut()); // Apply side-effects (e.g., symbol table updates)
133 }
134
135 // If new constraints are added, continue processing them in the next iterations.
136 i += 1;
137 }
138
139 stats.rewriter_run_time = Some(start.elapsed());
140 model.context.write().unwrap().stats.add_rewriter_run(stats);
141 Ok(new_model)
142}
143
144/// Checks if all expressions in `Vec<Expr>` are booleans.
145/// All top-level constraints in a model should be boolean expressions.
146fn is_vec_bool(exprs: &[Expression]) -> bool {
147 exprs
148 .iter()
149 .all(|expr| expr.return_type() == Some(ReturnType::Bool))
150}
151
152/// Attempts to apply a set of rules to the given expression and its sub-expressions in the model.
153///
154/// 1. Checks if the expression is "clean" (all possible rules have been applied).
155/// 2. Tries to apply rules to the top-level expression, in oprder of priority.
156/// 3. If no rules can be applied to the top-level expression, recurses into its sub-expressions.
157///
158/// When a successful rule application is found, immediately returns a `Reduction` and stops.
159/// The `Reduction` contains the new expression and any side-effects (e.g., new constraints, variables).
160/// If no rule applications are possible in this expression tree, returns `None`.
161///
162/// # Parameters
163/// - `expression`: The [`Expression`] to be rewritten.
164/// - `model`: The root [`Model`] for access to the context and symbol table.
165/// - `rules`: A max-heap of [`RuleData`] containing rules, priorities, and metadata. Ordered by rule priority.
166/// - `apply_optimizations`: If `true`, skip already "clean" expressions to avoid redundant work.
167/// - `stats`: A mutable reference to [`RewriterStats`] to collect statistics
168///
169/// # Returns
170/// - `Some(<Reduction>)`: If a rule is successfully applied to the expression or any of its sub-expressions.
171/// Contains the new expression and any side-effects to apply to the model.
172/// - `None`: If no rule is applicable to the expression or any of its sub-expressions.
173///
174/// # Example
175///
176/// - Rewriting the expression `a + min(x, y)`:
177///
178/// Input:
179/// ```text
180/// expression: a + min(x, y)
181/// rules: [min_to_var]
182/// model: {
183/// constraints: [(a + min(x, y)) = b],
184/// symbols: [a, b, x, y]
185/// }
186/// apply_optimizations: true
187/// ```
188///
189/// Process:
190/// 1. Initially, the expression is dirty, so we proceed with the rewrite.
191/// 2. No rules can be applied to the top-level expression `a + min(x, y)`.
192/// Try its children: `a` and `min(x, y)`.
193/// 3. No rules can be applied to `a`. Mark it as clean and return None.
194/// 4. The rule `min_to_var` can be applied to `min(x, y)`. Return the `Reduction`.
195/// ```text
196/// Reduction {
197/// new_expression: aux,
198/// new_top: [aux <= x, aux <= y, aux = x \/ aux = y],
199/// symbols: [a, b, x, y, aux]
200/// }
201/// ```
202/// 5. Update the parent expression `a + min(x, y)` with the new child `a + aux`.
203/// Add new constraints and variables to the model.
204/// 6. No more rules can be applied to this expression. Mark it as clean and return a pure `Reduction`.
205fn rewrite_iteration(
206 expression: &Expression,
207 model: &Model,
208 rules: &Vec<RuleData<'_>>,
209 apply_optimizations: bool,
210 stats: &mut RewriterStats,
211) -> Option<Reduction> {
212 if apply_optimizations && expression.is_clean() {
213 // Skip processing this expression if it's clean
214 return None;
215 }
216
217 // Mark the expression as clean - will be marked dirty if any rule is applied
218 let mut expression = expression.clone();
219
220 let rule_results = apply_all_rules(&expression, model, rules, stats);
221 if let Some(result) = choose_rewrite(&rule_results, &expression) {
222 // If a rule is applied, mark the expression as dirty
223 log_rule_application(&result, &expression, model.as_submodel());
224 return Some(result.reduction);
225 }
226
227 let mut sub = expression.children();
228 for i in 0..sub.len() {
229 if let Some(red) = rewrite_iteration(&sub[i], model, rules, apply_optimizations, stats) {
230 sub[i] = red.new_expression;
231 let res = expression.with_children(sub.clone());
232 return Some(Reduction::new(res, red.new_top, red.symbols));
233 }
234 }
235 // If all children are clean, mark this expression as clean
236 if apply_optimizations {
237 assert!(expression.children().iter().all(|c| c.is_clean()));
238 expression.set_clean(true);
239 return Some(Reduction::pure(expression));
240 }
241 None
242}
243
244/// Tries to apply rules to an expression and returns a list of successful applications.
245///
246/// The expression or model is NOT modified directly.
247/// We create a list of `RuleResult`s containing the reductions and pass it to `choose_rewrite` to select one to apply.
248///
249/// # Parameters
250/// - `expression`: A reference to the [`Expression`] to evaluate.
251/// - `model`: A reference to the [`Model`] for access to the symbol table and context.
252/// - `rules`: A vector of references to [`Rule`]s to try.
253/// - `stats`: A mutable reference to [`RewriterStats`] used to track the number of rule applications and other statistics.
254///
255/// # Returns
256/// - A `Vec<RuleResult>` containing all successful rule applications to the expression.
257/// Each `RuleResult` contains the rule that was applied and the resulting `Reduction`.
258///
259/// # Side-Effects
260/// - The function updates the provided `stats` with the number of rule application attempts and successful applications.
261/// - Debug or trace logging may be performed to track which rules were applicable or not for a given expression.
262///
263/// # Example
264/// let applicable_rules = apply_all_rules(&expr, &model, &rules, &mut stats);
265/// if !applicable_rules.is_empty() {
266/// for result in applicable_rules {
267/// println!("Rule applied: {:?}", result.rule_data.rule);
268/// }
269/// }
270///
271/// ## Note
272/// - Rules are applied only to the given expression, not its children.
273///
274/// # See Also
275/// - [`choose_rewrite`]: Chooses a single reduction from the rule results provided by `apply_all_rules`.
276fn apply_all_rules<'a>(
277 expression: &Expression,
278 model: &Model,
279 rules: &Vec<RuleData<'a>>,
280 stats: &mut RewriterStats,
281) -> Vec<RuleResult<'a>> {
282 let mut results = Vec::new();
283 for rule_data in rules {
284 match rule_data
285 .rule
286 .apply(expression, &model.as_submodel().symbols())
287 {
288 Ok(red) => {
289 stats.rewriter_rule_application_attempts =
290 Some(stats.rewriter_rule_application_attempts.unwrap() + 1);
291 stats.rewriter_rule_applications =
292 Some(stats.rewriter_rule_applications.unwrap() + 1);
293 // Assert no clean children
294 // assert!(!red.new_expression.children().iter().any(|c| c.is_clean()), "Rule that caused assertion to fail: {:?}", rule.name);
295 // assert!(!red.new_expression.children().iter().any(|c| c.children().iter().any(|c| c.is_clean())));
296 results.push(RuleResult {
297 rule_data: rule_data.clone(),
298 reduction: red,
299 });
300 }
301 Err(_) => {
302 log::trace!(
303 "Rule attempted but not applied: {}, to expression: {} ({:?})",
304 rule_data.rule,
305 expression,
306 rule_data
307 );
308 stats.rewriter_rule_application_attempts =
309 Some(stats.rewriter_rule_application_attempts.unwrap() + 1);
310 continue;
311 }
312 }
313 }
314 results
315}
316
317/// Chooses the first applicable rule result from a list of rule applications.
318///
319/// Currently, applies the rule with the highest priority.
320/// If multiple rules have the same priority, logs an error message and panics.
321///
322/// # Parameters
323/// - `results`: A slice of [`RuleResult`]s to consider.
324/// - `initial_expression`: [`Expression`] before the rule application.
325///
326/// # Returns
327/// - `Some(<Reduction>)`: If there is at least one successful rule application, returns a [`Reduction`] to apply.
328/// - `None`: If there are no successful rule applications (i.e. `results` is empty).
329///
330/// # Example
331///
332/// let rule_results = vec![rule1_result, rule2_result];
333/// if let Some(reduction) = choose_rewrite(&rule_results) {
334/// // Process the chosen reduction
335/// }
336///
337fn choose_rewrite<'a>(
338 results: &[RuleResult<'a>],
339 initial_expression: &Expression,
340) -> Option<RuleResult<'a>> {
341 //in the case where multiple rules are applicable
342 if !results.is_empty() {
343 let mut rewrite_options: Vec<RuleResult> = Vec::new();
344 for (priority, group) in &results.iter().chunk_by(|result| result.rule_data.priority) {
345 let options: Vec<&RuleResult> = group.collect();
346 if options.len() > 1 {
347 // Multiple rules with the same priority
348 let mut message = format!(
349 "Found multiple rules of the same priority {} applicable to expression: {}\n",
350 priority, initial_expression
351 );
352 for option in options {
353 message.push_str(&format!(
354 "- Rule: {} (from {})\n",
355 option.rule_data.rule.name, option.rule_data.rule_set.name
356 ));
357 }
358 bug!("{}", message);
359 } else {
360 // Only one rule with this priority, add it to the list
361 rewrite_options.push(options[0].clone());
362 }
363 }
364
365 if rewrite_options.len() > 1 {
366 // Keep old behaviour: log a message and apply the highest priority rule
367 let mut message = format!(
368 "Found multiple rules of different priorities applicable to expression: {}\n",
369 initial_expression
370 );
371 for option in &rewrite_options {
372 message.push_str(&format!(
373 "- Rule: {} (priority {}, from {})\n",
374 option.rule_data.rule.name,
375 option.rule_data.priority,
376 option.rule_data.rule_set.name
377 ));
378 }
379 log::warn!("{}", message);
380 }
381
382 return Some(rewrite_options[0].clone());
383 }
384
385 None
386}