Lines
67.74 %
Functions
7.58 %
use super::{RewriteError, RuleSet, resolve_rules::RuleData};
use crate::{
Model,
ast::Expression as Expr,
bug,
rule_engine::{
get_rules_grouped,
rewriter_common::{
RuleResult, VariableDeclarationSnapshot, log_rule_application,
snapshot_variable_declarations,
},
submodel_zipper::expression_ctx,
settings::{Rewriter, set_current_rewriter},
stats::RewriterStats,
};
use itertools::Itertools;
use std::{sync::Arc, time::Instant};
use tracing::trace;
// debug imports
#[cfg(debug_assertions)]
use {
crate::ast::assertions::debug_assert_model_well_formed,
tracing::{Level, span},
type VariableSnapshots = Option<(VariableDeclarationSnapshot, VariableDeclarationSnapshot)>;
type ApplicableRule<'a, CtxFnType> = (RuleResult<'a>, u16, Expr, CtxFnType, VariableSnapshots);
/// A naive, exhaustive rewriter for development purposes. Applies rules in priority order,
/// favouring expressions found earlier during preorder traversal of the tree.
pub fn rewrite_naive<'a>(
model: &Model,
rule_sets: &Vec<&'a RuleSet<'a>>,
prop_multiple_equally_applicable: bool,
) -> Result<Model, RewriteError> {
set_current_rewriter(Rewriter::Naive);
let rules_grouped = get_rules_grouped(rule_sets)
.unwrap_or_else(|_| bug!("get_rule_priorities() failed!"))
.into_iter()
.collect_vec();
let mut model = model.clone();
let mut done_something = true;
let mut rewriter_stats = RewriterStats::new();
rewriter_stats.is_optimization_enabled = Some(false);
let run_start = Instant::now();
trace!(
target: "rule_engine_human",
"Model before rewriting:\n\n{}\n--\n",
model
);
target: "rule_engine_human_verbose",
"elapsed_s,rule_level,rule_name,rule_set,status,expression"
// Rewrite until there are no more rules left to apply.
while done_something {
done_something = try_rewrite_model(
&mut model,
&rules_grouped,
prop_multiple_equally_applicable,
&mut rewriter_stats,
&run_start,
)
.is_some();
}
let run_end = Instant::now();
rewriter_stats.rewriter_run_time = Some(run_end - run_start);
.context
.write()
.unwrap()
.stats
.add_rewriter_run(rewriter_stats);
"Final model:\n\n{}",
Ok(model)
// Tries to do a single rewrite on the model.
//
// Returns None if no change was made.
fn try_rewrite_model(
submodel: &mut Model,
rules_grouped: &Vec<(u16, Vec<RuleData<'_>>)>,
stats: &mut RewriterStats,
run_start: &Instant,
) -> Option<()> {
type CtxFn = Arc<dyn Fn(Expr) -> Expr>;
let mut results: Vec<ApplicableRule<'_, CtxFn>> = vec![];
// Iterate over rules by priority in descending order.
'top: for (priority, rules) in rules_grouped.iter() {
// Rewrite within the current root expression tree.
for (expr, ctx) in expression_ctx(submodel.root().clone()) {
// Clone expr and ctx so they can be reused
let expr = expr.clone();
let ctx = ctx.clone();
for rd in rules {
// Count rule application attempts
stats.rewriter_rule_application_attempts =
Some(stats.rewriter_rule_application_attempts.unwrap_or(0) + 1);
let span = span!(Level::TRACE,"trying_rule_application",rule_name=rd.rule.name,rule_target_expression=%expr);
let _guard = span.enter();
tracing::trace!(rule_name = rd.rule.name, "Trying rule");
let before_variable_snapshot = matches!(expr, Expr::Root(_, _))
.then(|| snapshot_variable_declarations(&submodel.symbols()));
match (rd.rule.application)(&expr, &submodel.symbols()) {
Ok(red) => {
log_verbose_rule_attempt(
run_start,
priority,
rd.rule.name,
rd.rule_set.name,
"success",
&expr,
// Count successful rule applications
stats.rewriter_rule_applications =
Some(stats.rewriter_rule_applications.unwrap_or(0) + 1);
let after_variable_snapshot = before_variable_snapshot
.as_ref()
.map(|_| snapshot_variable_declarations(&red.symbols));
let variable_snapshots =
before_variable_snapshot.zip(after_variable_snapshot);
// Collect applicable rules
results.push((
RuleResult {
rule_data: rd.clone(),
reduction: red,
*priority,
expr.clone(),
ctx.clone(),
variable_snapshots,
));
Err(_) => {
"fail",
// when called a lot, this becomes very expensive!
tracing::trace!(
"Rule attempted but not applied: {} (priority {}, rule set {}), to expression: {}",
expr
// This expression has the highest rule priority so far, so this is what we want to
// rewrite.
if !results.is_empty() {
break 'top;
match results.as_slice() {
[] => {
return None;
} // no rules are applicable.
[(result, _priority, expr, ctx, variable_snapshots), ..] => {
if prop_multiple_equally_applicable {
assert_no_multiple_equally_applicable_rules(&results, rules_grouped);
// Extract the single applicable rule and apply it
log_rule_application(
result,
expr,
&submodel.symbols(),
variable_snapshots
.map(|(before, after)| (before, after)),
// Replace expr with new_expression
let new_root = ctx(result.reduction.new_expression.clone());
submodel.replace_root(new_root);
// Apply new symbols and top level
result.reduction.clone().apply(submodel);
{
let assertion_context = format!(
"naive rewriter after applying rule '{}'",
result.rule_data.rule.name
debug_assert_model_well_formed(submodel, &assertion_context);
Some(())
fn csv_escape(field: &str) -> String {
if field.contains([',', '"', '\n', '\r']) {
format!("\"{}\"", field.replace('"', "\"\""))
} else {
field.to_string()
fn log_verbose_rule_attempt(
priority: &u16,
rule_name: &str,
rule_set_name: &str,
status: &str,
expr: &Expr,
) {
let elapsed_seconds = run_start.elapsed().as_secs_f64();
let expr_str = expr.to_string();
"{:.3},{},{},{},{},{}",
elapsed_seconds,
csv_escape(rule_name),
csv_escape(rule_set_name),
status,
csv_escape(&expr_str)
// Exits with a bug if there are multiple equally applicable rules for an expression.
fn assert_no_multiple_equally_applicable_rules<CtxFnType>(
results: &Vec<ApplicableRule<'_, CtxFnType>>,
if results.len() <= 1 {
return;
let names: Vec<_> = results
.iter()
.map(|(result, _, _, _, _)| result.rule_data.rule.name)
.collect();
// Extract the expression from the first result
let expr = results[0].2.clone();
// Construct a single string to display the names of the rules grouped by priority
let mut rules_by_priority_string = String::new();
rules_by_priority_string.push_str("Rules grouped by priority:\n");
for (priority, rules) in rules_grouped.iter() {
rules_by_priority_string.push_str(&format!("Priority {priority}:\n"));
rules_by_priority_string.push_str(&format!(
" - {} (from {})\n",
rd.rule.name, rd.rule_set.name
bug!("Multiple equally applicable rules for {expr}: {names:#?}\n\n{rules_by_priority_string}");