conjure_core/rule_engine/
rule_set.rs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
use std::collections::{HashMap, HashSet};
use std::fmt::{Display, Formatter};
use std::hash::Hash;
use std::sync::OnceLock;

use log::warn;

use crate::rule_engine::{get_rule_set_by_name, get_rules, Rule};
use crate::solver::SolverFamily;

/// A structure representing a set of rules with a name, priority, and dependencies.
///
/// `RuleSet` is a way to group related rules together under a single name.
/// You can think of it like a list of rules that belong to the same category.
/// Each `RuleSet` can also have a number that tells it what order it should run in compared to other `RuleSet` instances.
/// Additionally, a `RuleSet` can depend on other `RuleSet` instances, meaning it needs them to run first.
///
/// To make things efficient, `RuleSet` only figures out its rules and dependencies the first time they're needed,
/// and then it remembers them so it doesn't have to do the work again.
///
/// # Fields
/// - `name`: The name of the rule set.
/// - `order`: A number that decides the order in which this `RuleSet` should be applied.
/// If two `RuleSet` instances have the same rule but with different priorities,
/// the one with the higher `order` number will be the one that is used.
/// - `rules`: A lazily initialized map of rules to their priorities.
/// - `dependency_rs_names`: The names of the rule sets that this rule set depends on.
/// - `dependencies`: A lazily initialized set of `RuleSet` dependencies.
/// - `solver_families`: The solver families that this rule set applies to.
#[derive(Clone, Debug)]
pub struct RuleSet<'a> {
    /// The name of the rule set.
    pub name: &'a str,
    /// Order of the RuleSet. Used to establish a consistent order of operations when resolving rules.
    /// If two RuleSets overlap (contain the same rule but with different priorities), the RuleSet with the higher order will be used as the source of truth.
    pub order: u16,
    /// A map of rules to their priorities. This will be lazily initialized at runtime.
    rules: OnceLock<HashMap<&'a Rule<'a>, u16>>,
    /// The names of the rule sets that this rule set depends on.
    dependency_rs_names: &'a [&'a str],
    dependencies: OnceLock<HashSet<&'a RuleSet<'a>>>,
    /// The solver families that this rule set applies to.
    pub solver_families: &'a [SolverFamily],
}

impl<'a> RuleSet<'a> {
    pub const fn new(
        name: &'a str,
        order: u16,
        dependencies: &'a [&'a str],
        solver_families: &'a [SolverFamily],
    ) -> Self {
        Self {
            name,
            order,
            dependency_rs_names: dependencies,
            solver_families,
            rules: OnceLock::new(),
            dependencies: OnceLock::new(),
        }
    }

    /// Get the rules of this rule set, evaluating them lazily if necessary
    /// Returns a `&HashMap<&Rule, u16>` where the key is the rule and the value is the priority of the rule.
    pub fn get_rules(&self) -> &HashMap<&'a Rule<'a>, u16> {
        match self.rules.get() {
            None => {
                let rules = self.resolve_rules();
                let _ = self.rules.set(rules); // Try to set the rules, but ignore if it fails.

                // At this point, the rules cell is guaranteed to be set, so we can unwrap safely.
                // see: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html#method.set
                #[allow(clippy::unwrap_used)]
                self.rules.get().unwrap()
            }
            Some(rules) => rules,
        }
    }

    /// Get the dependencies of this rule set, evaluating them lazily if necessary
    /// Returns a `&HashSet<&RuleSet>` of the rule sets that this rule set depends on.
    #[allow(clippy::mutable_key_type)] // RuleSet is 'static so it's fine
    pub fn get_dependencies(&self) -> &HashSet<&'static RuleSet> {
        match self.dependencies.get() {
            None => {
                let dependencies = self.resolve_dependencies();
                let _ = self.dependencies.set(dependencies); // Try to set the dependencies, but ignore if it fails.

                // At this point, the dependencies cell is guaranteed to be set, so we can unwrap safely.
                // see: https://doc.rust-lang.org/stable/std/sync/struct.OnceLock.html#method.set
                #[allow(clippy::unwrap_used)]
                self.dependencies.get().unwrap()
            }
            Some(dependencies) => dependencies,
        }
    }

    /// Get the dependencies of this rule set, including itself
    #[allow(clippy::mutable_key_type)] // RuleSet is 'static so it's fine
    pub fn with_dependencies(&self) -> HashSet<&'static RuleSet> {
        let mut deps = self.get_dependencies().clone();
        deps.insert(self);
        deps
    }

    /// Resolve the rules of this rule set ("reverse the arrows")
    fn resolve_rules(&self) -> HashMap<&'a Rule<'a>, u16> {
        let mut rules = HashMap::new();

        for rule in get_rules() {
            let mut found = false;
            let mut priority: u16 = 0;

            for (name, p) in rule.rule_sets {
                if *name == self.name {
                    found = true;
                    priority = *p;
                    break;
                }
            }

            if found {
                rules.insert(rule, priority);
            }
        }

        rules
    }

    /// Recursively resolve the dependencies of this rule set.
    #[allow(clippy::mutable_key_type)] // RuleSet is 'static so it's fine
    fn resolve_dependencies(&self) -> HashSet<&'static RuleSet> {
        let mut dependencies = HashSet::new();

        for dep in self.dependency_rs_names {
            match get_rule_set_by_name(dep) {
                None => {
                    warn!(
                        "Rule set {} depends on non-existent rule set {}",
                        &self.name, dep
                    );
                }
                Some(rule_set) => {
                    if !dependencies.contains(rule_set) {
                        // Prevent cycles
                        dependencies.insert(rule_set);
                        dependencies.extend(rule_set.resolve_dependencies());
                    }
                }
            }
        }

        dependencies
    }
}

impl PartialEq for RuleSet<'_> {
    fn eq(&self, other: &Self) -> bool {
        self.name == other.name
    }
}

impl Eq for RuleSet<'_> {}

impl Hash for RuleSet<'_> {
    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
        self.name.hash(state);
    }
}

impl Display for RuleSet<'_> {
    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
        let n_rules = self.get_rules().len();
        let solver_families = self
            .solver_families
            .iter()
            .map(|f| f.to_string())
            .collect::<Vec<String>>();

        write!(
            f,
            "RuleSet {{\n\
            \tname: {}\n\
            \torder: {}\n\
            \trules: {}\n\
            \tsolver_families: {:?}\n\
        }}",
            self.name, self.order, n_rules, solver_families
        )
    }
}