Skip to main content

hawdb_cascades/
rule.rs

1// Copyright 2026 Nowledge
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use crate::search::{RuleEvent, RuleOutcome};
16use std::str::FromStr;
17
18#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
19pub enum RuleKind {
20    Implementation,
21    Exploration,
22    Transformation,
23    Enforcer,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
27pub struct RuleId {
28    name: &'static str,
29    kind: RuleKind,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq, Eq)]
33pub struct RulePromise {
34    priority: u16,
35}
36
37#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct RuleApplication<E> {
39    expression: E,
40    detail: String,
41}
42
43#[derive(Debug, Clone, PartialEq, Eq)]
44pub struct AppliedRule<E> {
45    id: RuleId,
46    application: RuleApplication<E>,
47}
48
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct RuleBatch<E> {
51    expressions: Vec<AppliedRule<E>>,
52    events: Vec<RuleEvent>,
53}
54
55pub trait OptimizerRule<E> {
56    fn id(&self) -> RuleId;
57
58    fn promise(&self, expression: &E) -> RulePromise;
59
60    fn apply(&self, expression: &E) -> Option<RuleApplication<E>>;
61}
62
63impl RuleKind {
64    pub fn all() -> &'static [Self] {
65        const ALL: &[RuleKind] = &[
66            RuleKind::Implementation,
67            RuleKind::Exploration,
68            RuleKind::Transformation,
69            RuleKind::Enforcer,
70        ];
71        ALL
72    }
73
74    pub fn as_str(self) -> &'static str {
75        match self {
76            RuleKind::Implementation => "implementation",
77            RuleKind::Exploration => "exploration",
78            RuleKind::Transformation => "transformation",
79            RuleKind::Enforcer => "enforcer",
80        }
81    }
82}
83
84impl FromStr for RuleKind {
85    type Err = &'static str;
86
87    fn from_str(value: &str) -> Result<Self, Self::Err> {
88        Self::all()
89            .iter()
90            .copied()
91            .find(|kind| kind.as_str() == value)
92            .ok_or("unknown optimizer rule kind")
93    }
94}
95
96impl RuleId {
97    pub const fn new(name: &'static str, kind: RuleKind) -> Self {
98        Self { name, kind }
99    }
100
101    pub fn name(self) -> &'static str {
102        self.name
103    }
104
105    pub fn kind(self) -> RuleKind {
106        self.kind
107    }
108
109    pub fn stable_name(self) -> String {
110        format!("{}:{}", self.kind.as_str(), self.name)
111    }
112}
113
114impl RulePromise {
115    pub const NEVER: Self = Self { priority: 0 };
116
117    pub const fn new(priority: u16) -> Self {
118        Self { priority }
119    }
120
121    pub fn priority(self) -> u16 {
122        self.priority
123    }
124
125    pub fn should_apply(self) -> bool {
126        self.priority > 0
127    }
128}
129
130impl<E> RuleApplication<E> {
131    pub fn new(expression: E, detail: impl Into<String>) -> Self {
132        Self {
133            expression,
134            detail: detail.into(),
135        }
136    }
137
138    pub fn expression(&self) -> &E {
139        &self.expression
140    }
141
142    pub fn into_expression(self) -> E {
143        self.expression
144    }
145
146    pub fn detail(&self) -> &str {
147        &self.detail
148    }
149}
150
151impl<E> AppliedRule<E> {
152    pub fn id(&self) -> RuleId {
153        self.id
154    }
155
156    pub fn application(&self) -> &RuleApplication<E> {
157        &self.application
158    }
159
160    pub fn into_application(self) -> RuleApplication<E> {
161        self.application
162    }
163}
164
165impl<E> RuleBatch<E> {
166    pub fn expressions(&self) -> &[AppliedRule<E>] {
167        &self.expressions
168    }
169
170    pub fn events(&self) -> &[RuleEvent] {
171        &self.events
172    }
173
174    pub fn into_parts(self) -> (Vec<AppliedRule<E>>, Vec<RuleEvent>) {
175        (self.expressions, self.events)
176    }
177}
178
179pub fn apply_rule_batch<E>(expression: &E, rules: &[&dyn OptimizerRule<E>]) -> RuleBatch<E> {
180    let mut candidates = rules
181        .iter()
182        .enumerate()
183        .filter_map(|(index, rule)| {
184            let promise = rule.promise(expression);
185            promise
186                .should_apply()
187                .then(|| (index, rule.id(), promise, *rule))
188        })
189        .collect::<Vec<_>>();
190
191    candidates.sort_by(
192        |(left_index, left_id, left_promise, _), (right_index, right_id, right_promise, _)| {
193            right_promise
194                .priority()
195                .cmp(&left_promise.priority())
196                .then_with(|| left_id.stable_name().cmp(&right_id.stable_name()))
197                .then_with(|| left_index.cmp(right_index))
198        },
199    );
200
201    let mut expressions = Vec::new();
202    let mut events = Vec::new();
203
204    for (_, id, promise, rule) in candidates {
205        match rule.apply(expression) {
206            Some(application) => {
207                events.push(RuleEvent::new(
208                    id.stable_name(),
209                    RuleOutcome::Applied,
210                    format!("priority={} {}", promise.priority(), application.detail()),
211                ));
212                expressions.push(AppliedRule { id, application });
213            }
214            None => events.push(RuleEvent::skipped(
215                id.stable_name(),
216                format!("priority={} no expression produced", promise.priority()),
217            )),
218        }
219    }
220
221    RuleBatch {
222        expressions,
223        events,
224    }
225}
226
227#[cfg(test)]
228mod tests {
229    use super::{
230        apply_rule_batch, OptimizerRule, RuleApplication, RuleId, RuleKind, RuleOutcome,
231        RulePromise,
232    };
233
234    #[derive(Debug, Clone, PartialEq, Eq)]
235    struct TestExpr(&'static str);
236
237    struct RenameRule {
238        id: RuleId,
239        priority: u16,
240        input: &'static str,
241        output: Option<&'static str>,
242    }
243
244    impl OptimizerRule<TestExpr> for RenameRule {
245        fn id(&self) -> RuleId {
246            self.id
247        }
248
249        fn promise(&self, expression: &TestExpr) -> RulePromise {
250            if expression.0 == self.input {
251                RulePromise::new(self.priority)
252            } else {
253                RulePromise::NEVER
254            }
255        }
256
257        fn apply(&self, _expression: &TestExpr) -> Option<RuleApplication<TestExpr>> {
258            self.output
259                .map(|output| RuleApplication::new(TestExpr(output), format!("to={output}")))
260        }
261    }
262
263    #[test]
264    fn rule_identity_exposes_stable_names() {
265        let id = RuleId::new("scan_to_seek", RuleKind::Implementation);
266
267        assert_eq!(id.name(), "scan_to_seek");
268        assert_eq!(id.kind(), RuleKind::Implementation);
269        assert_eq!(id.stable_name(), "implementation:scan_to_seek");
270    }
271
272    #[test]
273    fn rule_kind_strings_round_trip_for_diagnostics() {
274        for kind in RuleKind::all() {
275            assert_eq!(kind.as_str().parse::<RuleKind>(), Ok(*kind));
276        }
277        assert!("unknown".parse::<RuleKind>().is_err());
278    }
279
280    #[test]
281    fn rule_promise_zero_means_never_apply() {
282        assert!(!RulePromise::NEVER.should_apply());
283        assert!(RulePromise::new(1).should_apply());
284        assert_eq!(RulePromise::new(7).priority(), 7);
285    }
286
287    #[test]
288    fn rule_batch_orders_by_promise_then_stable_rule_name() {
289        let low = RenameRule {
290            id: RuleId::new("low", RuleKind::Transformation),
291            priority: 1,
292            input: "scan",
293            output: Some("low"),
294        };
295        let high_b = RenameRule {
296            id: RuleId::new("b", RuleKind::Implementation),
297            priority: 9,
298            input: "scan",
299            output: Some("high_b"),
300        };
301        let high_a = RenameRule {
302            id: RuleId::new("a", RuleKind::Implementation),
303            priority: 9,
304            input: "scan",
305            output: Some("high_a"),
306        };
307        let batch = apply_rule_batch(&TestExpr("scan"), &[&low, &high_b, &high_a]);
308
309        let expressions = batch.expressions();
310        assert_eq!(expressions.len(), 3);
311        assert_eq!(expressions[0].id().stable_name(), "implementation:a");
312        assert_eq!(expressions[1].id().stable_name(), "implementation:b");
313        assert_eq!(expressions[2].id().stable_name(), "transformation:low");
314        assert_eq!(
315            expressions[0].application().expression(),
316            &TestExpr("high_a")
317        );
318        assert_eq!(batch.events()[0].outcome(), RuleOutcome::Applied);
319        assert_eq!(batch.events()[0].detail(), "priority=9 to=high_a");
320    }
321
322    #[test]
323    fn rule_batch_records_skipped_promising_rule_without_output() {
324        let skipped = RenameRule {
325            id: RuleId::new("no_output", RuleKind::Exploration),
326            priority: 3,
327            input: "scan",
328            output: None,
329        };
330        let unrelated = RenameRule {
331            id: RuleId::new("unrelated", RuleKind::Exploration),
332            priority: 8,
333            input: "filter",
334            output: Some("should_not_apply"),
335        };
336        let batch = apply_rule_batch(&TestExpr("scan"), &[&skipped, &unrelated]);
337
338        assert!(batch.expressions().is_empty());
339        assert_eq!(batch.events().len(), 1);
340        assert_eq!(batch.events()[0].rule(), "exploration:no_output");
341        assert_eq!(batch.events()[0].outcome(), RuleOutcome::Skipped);
342        assert_eq!(
343            batch.events()[0].detail(),
344            "priority=3 no expression produced"
345        );
346    }
347}