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

            
5
use thiserror::Error;
6

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

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

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

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

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

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

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

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

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

            
102
    // Apply side-effects (e.g. symbol table updates
103
20570
    pub fn apply(self, model: &mut Model) {
104
20570
        model.extend_sym_table(self.symbols);
105

            
106
        // TODO: (yb33) Remove it when we change constraints to a vector
107
20570
        if let Expression::And(_, exprs) = &self.new_top {
108
20400
            if exprs.is_empty() {
109
20230
                model.constraints = self.new_expression.clone();
110
20230
                return;
111
170
            }
112
170
        }
113

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

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

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

            
156
288063198
    pub fn apply(&self, expr: &Expression, mdl: &Model) -> ApplicationResult {
157
288063198
        (self.application)(expr, mdl)
158
288063198
    }
159
}
160

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

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

            
173
impl Eq for Rule<'_> {}
174

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