1use 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}