rucc_rules/matcher.rs
1//! Rules to the automaton that matches them.
2//!
3//! A pattern is a tree and the subject is a tree, and the obvious way to match one against the
4//! other is a chain of conditionals per rule. That is what `spec/10-backend.md` says not to
5//! build: with several hundred rules per target it re-tests the same opcode hundreds of times,
6//! and it puts the order the rules are tried in beyond anybody's control.
7//!
8//! What is built instead is a trie over the patterns, flattened. Every pattern becomes a
9//! sequence of steps read in pre-order, and patterns that begin the same way share the steps
10//! they agree on, so testing that a term is an `add.i64` happens once no matter how many rules
11//! begin with one. Matching walks the subject in the same pre-order, which is what makes the
12//! sequence well defined: at any node of the trie, every rule that reaches it has consumed the
13//! same shape of subject, so there is one stack of remaining subterms rather than one per rule.
14//!
15//! Specificity falls out of the shape rather than being sorted for. At each node the concrete
16//! tests are tried before the wildcard, so a rule that names an operand is always tried before
17//! a rule that takes anything there, which is the maximal munch that document asks for. Among
18//! rules that are equally specific the first one written wins, which is what `-O0` wants and is
19//! what the single-pass mode in section 10.3 is defined to do.
20
21use std::fmt;
22
23use crate::ast::{Rule, Term, TermKind};
24use crate::error::Error;
25
26/// One step of a flattened pattern.
27#[derive(Debug, Clone, PartialEq, Eq)]
28enum Step {
29 /// The subterm here must be this head applied to this many arguments.
30 App { head: String, arity: usize },
31 /// The subterm here must be this literal.
32 Int(i128),
33 /// Anything goes here, and it is remembered under this name.
34 Bind(String),
35}
36
37/// A test on one subterm. This is [`Step`] without the wildcard, because a wildcard is not a
38/// test: it is the branch taken when no test matched.
39#[derive(Debug, Clone, PartialEq, Eq)]
40enum Test {
41 App { head: String, arity: usize },
42 Int(i128),
43}
44
45/// One node of the trie.
46#[derive(Debug, Default)]
47struct Node {
48 /// The concrete tests, in the order they were first written, tried before the wildcard.
49 tests: Vec<(Test, usize)>,
50 /// The branch that takes anything, and the name it binds it under.
51 wildcard: Option<(String, usize)>,
52 /// The rule that ends here, if one does.
53 accept: Option<usize>,
54}
55
56/// The automaton a rule set compiles into.
57#[derive(Debug)]
58pub struct Matcher {
59 nodes: Vec<Node>,
60}
61
62/// What a successful match found.
63#[derive(Debug, Clone, PartialEq, Eq)]
64pub struct Match<'t> {
65 /// The index into the rule set of the rule that fired.
66 pub rule: usize,
67 /// What the pattern's variables were bound to, in the order the pattern binds them.
68 pub bindings: Vec<(String, &'t Term)>,
69}
70
71impl<'t> Match<'t> {
72 /// What one name was bound to, or nothing if the pattern never bound it.
73 #[must_use]
74 pub fn get(&self, name: &str) -> Option<&'t Term> {
75 self.bindings.iter().find(|(bound, _)| bound == name).map(|(_, term)| *term)
76 }
77}
78
79impl Matcher {
80 /// Compile a rule set.
81 ///
82 /// # Errors
83 ///
84 /// A rule whose pattern is one an earlier rule already has can never fire, and that is
85 /// reported rather than silently dropped. It is always a mistake: either the second rule was
86 /// meant to say something else, or one of the two should not be there.
87 pub fn build(path: &str, rules: &[Rule]) -> Result<Matcher, Vec<Error>> {
88 let mut matcher = Matcher { nodes: vec![Node::default()] };
89 let mut errors = Vec::new();
90
91 for (index, rule) in rules.iter().enumerate() {
92 let mut at = 0;
93 for step in flatten(&rule.pattern) {
94 at = matcher.follow(at, step);
95 }
96 match matcher.nodes[at].accept {
97 Some(first) => errors.push(Error {
98 path: path.to_owned(),
99 line: rule.line,
100 column: rule.column,
101 message: format!(
102 "this rule can never fire, because the rule on line {} matches everything it does",
103 rules[first].line
104 ),
105 }),
106 None => matcher.nodes[at].accept = Some(index),
107 }
108 }
109
110 if errors.is_empty() { Ok(matcher) } else { Err(errors) }
111 }
112
113 /// Add one step at one node, reusing the branch if it is already there.
114 fn follow(&mut self, at: usize, step: Step) -> usize {
115 let test = match step {
116 Step::App { head, arity } => Test::App { head, arity },
117 Step::Int(value) => Test::Int(value),
118 Step::Bind(name) => {
119 if let Some((_, next)) = &self.nodes[at].wildcard {
120 // The name is the first one written. Two rules that put different names in
121 // the same hole are the same automaton, and the binding is reported back
122 // under the name of the rule that fired rather than under this one.
123 return *next;
124 }
125 let next = self.push();
126 self.nodes[at].wildcard = Some((name, next));
127 return next;
128 }
129 };
130 if let Some((_, next)) = self.nodes[at].tests.iter().find(|(have, _)| *have == test) {
131 return *next;
132 }
133 let next = self.push();
134 self.nodes[at].tests.push((test, next));
135 next
136 }
137
138 fn push(&mut self) -> usize {
139 self.nodes.push(Node::default());
140 self.nodes.len() - 1
141 }
142
143 /// Match one term against the whole rule set, returning the rule that fires.
144 ///
145 /// The term is matched as a whole. Finding the subterms of a function worth matching is the
146 /// selector's job and not this one's.
147 #[must_use]
148 pub fn find<'t>(&self, term: &'t Term) -> Option<Match<'t>> {
149 let mut bindings = Vec::new();
150 let rule = self.run(0, vec![term], &mut bindings)?;
151 Some(Match { rule, bindings })
152 }
153
154 /// Walk the trie and the subject together.
155 ///
156 /// `left` is the subterms still to be matched, innermost last, so that popping gives the
157 /// pre-order the patterns were flattened in.
158 fn run<'t>(
159 &self,
160 at: usize,
161 mut left: Vec<&'t Term>,
162 bindings: &mut Vec<(String, &'t Term)>,
163 ) -> Option<usize> {
164 let Some(subject) = left.pop() else {
165 return self.nodes[at].accept;
166 };
167 let node = &self.nodes[at];
168
169 for (test, next) in &node.tests {
170 let matched = match (test, &subject.kind) {
171 (Test::Int(want), TermKind::Int(have)) => want == have,
172 (Test::App { head, arity }, TermKind::App { head: name, args }) => {
173 head == name && *arity == args.len()
174 }
175 _ => false,
176 };
177 if !matched {
178 continue;
179 }
180 let mut deeper = left.clone();
181 if let TermKind::App { args, .. } = &subject.kind {
182 deeper.extend(args.iter().rev());
183 }
184 let depth = bindings.len();
185 if let Some(rule) = self.run(*next, deeper, bindings) {
186 return Some(rule);
187 }
188 bindings.truncate(depth);
189 }
190
191 // The wildcard is last, which is the whole of what "specificity order" means here.
192 let (name, next) = node.wildcard.as_ref()?;
193 let depth = bindings.len();
194 bindings.push((name.clone(), subject));
195 if let Some(rule) = self.run(*next, left, bindings) {
196 return Some(rule);
197 }
198 bindings.truncate(depth);
199 None
200 }
201
202 /// How many nodes the trie has, which is what a rule set costs to match against.
203 #[must_use]
204 pub fn len(&self) -> usize {
205 self.nodes.len()
206 }
207
208 /// Whether the rule set was empty.
209 #[must_use]
210 pub fn is_empty(&self) -> bool {
211 self.nodes.len() <= 1
212 }
213}
214
215/// Flatten a pattern into the steps that match it, in the pre-order the matcher walks.
216fn flatten(pattern: &Term) -> Vec<Step> {
217 let mut out = Vec::new();
218 push_steps(pattern, &mut out);
219 out
220}
221
222fn push_steps(term: &Term, out: &mut Vec<Step>) {
223 match &term.kind {
224 TermKind::Var(name) => out.push(Step::Bind(name.clone())),
225 TermKind::Int(value) => out.push(Step::Int(*value)),
226 TermKind::App { head, args } => {
227 out.push(Step::App { head: head.clone(), arity: args.len() });
228 for arg in args {
229 push_steps(arg, out);
230 }
231 }
232 }
233}
234
235impl fmt::Display for Matcher {
236 /// Prints the trie, one branch to a line, indented by depth. This is what makes a rule set's
237 /// shape reviewable: two rules that share a prefix share a line, and a rule that can only be
238 /// reached through a wildcard is visibly the last thing tried.
239 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
240 self.show(f, 0, 0)
241 }
242}
243
244impl Matcher {
245 fn show(&self, f: &mut fmt::Formatter<'_>, at: usize, depth: usize) -> fmt::Result {
246 let pad = " ".repeat(depth);
247 let node = &self.nodes[at];
248 if let Some(rule) = node.accept {
249 writeln!(f, "{pad}=> rule {rule}")?;
250 }
251 for (test, next) in &node.tests {
252 match test {
253 Test::App { head, arity } => writeln!(f, "{pad}{head}/{arity}")?,
254 Test::Int(value) => writeln!(f, "{pad}{value}")?,
255 }
256 self.show(f, *next, depth + 1)?;
257 }
258 if let Some((name, next)) = &node.wildcard {
259 writeln!(f, "{pad}bind {name}")?;
260 self.show(f, *next, depth + 1)?;
261 }
262 Ok(())
263 }
264}