1
use std::fmt::{self, Display, Formatter};
2
use std::hash::Hash;
3

            
4
use thiserror::Error;
5

            
6
use crate::ast::{Expression, SymbolTable};
7
use crate::metadata::Metadata;
8
use crate::model::Model;
9

            
10
#[derive(Debug, Error)]
11
pub enum ApplicationError {
12
    #[error("Rule is not applicable")]
13
    RuleNotApplicable,
14

            
15
    #[error("Could not calculate the expression domain")]
16
    DomainError,
17
}
18

            
19
/// Represents the result of applying a rule to an expression within a model.
20
///
21
/// A `Reduction` encapsulates the changes made to a model during a rule application.
22
/// It includes a new expression to replace the original one, an optional top-level constraint
23
/// to be added to the model, and any updates to the model's symbol table.
24
///
25
/// This struct allows for representing side-effects of rule applications, ensuring that
26
/// all modifications, including symbol table expansions and additional constraints, are
27
/// accounted for and can be applied to the model consistently.
28
///
29
/// # Fields
30
/// - `new_expression`: The updated [`Expression`] that replaces the original one after applying the rule.
31
/// - `new_top`: An additional top-level [`Expression`] constraint that should be added to the model. If no top-level
32
///   constraint is needed, this field can be set to `Expression::Nothing`.
33
/// - `symbols`: A [`SymbolTable`] containing any new symbol definitions or modifications to be added to the model's
34
///   symbol table. If no symbols are modified, this field can be set to an empty symbol table.
35
///
36
/// # Usage
37
/// A `Reduction` can be created using one of the provided constructors:
38
/// - [`Reduction::new`]: Creates a reduction with a new expression, top-level constraint, and symbol modifications.
39
/// - [`Reduction::pure`]: Creates a reduction with only a new expression and no side-effects on the symbol table or constraints.
40
/// - [`Reduction::with_symbols`]: Creates a reduction with a new expression and symbol table modifications, but no top-level constraint.
41
/// - [`Reduction::with_top`]: Creates a reduction with a new expression and a top-level constraint, but no symbol table modifications.
42
///
43
/// The `apply` method allows for applying the changes represented by the `Reduction` to a [`Model`].
44
///
45
/// # Example
46
/// ```
47
1
/// // Need to add an example
48
1
/// ```
49
1
///
50
/// # See Also
51
/// - [`ApplicationResult`]: Represents the result of applying a rule, which may either be a `Reduction` or an `ApplicationError`.
52
/// - [`Model`]: The structure to which the `Reduction` changes are applied.
53
#[non_exhaustive]
54
#[derive(Clone, Debug)]
55
pub struct Reduction {
56
    pub new_expression: Expression,
57
    pub new_top: Expression,
58
    pub symbols: SymbolTable,
59
}
60

            
61
/// The result of applying a rule to an expression.
62
/// Contains either a set of reduction instructions or an error.
63
pub type ApplicationResult = Result<Reduction, ApplicationError>;
64

            
65
impl Reduction {
66
23817
    pub fn new(new_expression: Expression, new_top: Expression, symbols: SymbolTable) -> Self {
67
23817
        Self {
68
23817
            new_expression,
69
23817
            new_top,
70
23817
            symbols,
71
23817
        }
72
23817
    }
73

            
74
    /// Represents a reduction with no side effects on the model.
75
19992
    pub fn pure(new_expression: Expression) -> Self {
76
19992
        Self {
77
19992
            new_expression,
78
19992
            new_top: Expression::And(Metadata::new(), Vec::new()),
79
19992
            symbols: SymbolTable::new(),
80
19992
        }
81
19992
    }
82

            
83
    /// Represents a reduction that also modifies the symbol table.
84
    pub fn with_symbols(new_expression: Expression, symbols: SymbolTable) -> Self {
85
        Self {
86
            new_expression,
87
            new_top: Expression::And(Metadata::new(), Vec::new()),
88
            symbols,
89
        }
90
    }
91

            
92
    /// Represents a reduction that also adds a top-level constraint to the model.
93
    pub fn with_top(new_expression: Expression, new_top: Expression) -> Self {
94
        Self {
95
            new_expression,
96
            new_top,
97
            symbols: SymbolTable::new(),
98
        }
99
    }
100

            
101
    // Apply side-effects (e.g. symbol table updates
102
19448
    pub fn apply(self, model: &mut Model) {
103
19448
        model.variables.extend(self.symbols); // Add new assignments to the symbol table
104
                                              // TODO: (yb33) Remove it when we change constraints to a vector
105
19448
        if let Expression::And(_, exprs) = &self.new_top {
106
19448
            if exprs.is_empty() {
107
19278
                model.constraints = self.new_expression.clone();
108
19278
                return;
109
170
            }
110
        }
111

            
112
170
        model.constraints = match self.new_expression {
113
51
            Expression::And(metadata, mut exprs) => {
114
51
                // Avoid creating a nested conjunction
115
51
                exprs.push(self.new_top.clone());
116
51
                Expression::And(metadata.clone_dirty(), exprs)
117
            }
118
119
            _ => Expression::And(
119
119
                Metadata::new(),
120
119
                vec![self.new_expression.clone(), self.new_top],
121
119
            ),
122
        };
123
19448
    }
124
}
125

            
126
/**
127
 * A rule with a name, application function, and rule sets.
128
 *
129
 * # Fields
130
 * - `name` The name of the rule.
131
 * - `application` The function to apply the rule.
132
 * - `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.
133
 */
134
#[derive(Clone, Debug)]
135
pub struct Rule<'a> {
136
    pub name: &'a str,
137
    pub application: fn(&Expression, &Model) -> ApplicationResult,
138
    pub rule_sets: &'a [(&'a str, u16)], // (name, priority). At runtime, we add the rule to rulesets
139
}
140

            
141
impl<'a> Rule<'a> {
142
    pub const fn new(
143
        name: &'a str,
144
        application: fn(&Expression, &Model) -> ApplicationResult,
145
        rule_sets: &'a [(&'static str, u16)],
146
    ) -> Self {
147
        Self {
148
            name,
149
            application,
150
            rule_sets,
151
        }
152
    }
153

            
154
271435430
    pub fn apply(&self, expr: &Expression, mdl: &Model) -> ApplicationResult {
155
271435430
        (self.application)(expr, mdl)
156
271435430
    }
157
}
158

            
159
impl Display for Rule<'_> {
160
    fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
161
        write!(f, "{}", self.name)
162
    }
163
}
164

            
165
impl PartialEq for Rule<'_> {
166
325754
    fn eq(&self, other: &Self) -> bool {
167
325754
        self.name == other.name
168
325754
    }
169
}
170

            
171
impl Eq for Rule<'_> {}
172

            
173
impl Hash for Rule<'_> {
174
482035
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
175
482035
        self.name.hash(state);
176
482035
    }
177
}