Skip to main content

rucc_rules/
parse.rs

1//! Tokens to rules, and the checks that belong in the reading rather than after it.
2
3use std::collections::HashSet;
4
5use crate::ast::{Rule, RuleKind, Term, TermKind};
6use crate::error::Error;
7use crate::lex::{Spanned, Token, tokens};
8
9/// What a specification calls the value the replacement computes.
10const RESULT: &str = "result";
11
12/// The names that mean something at the top of a rule and nowhere else. Refusing them as heads
13/// inside a term is what turns a missing parenthesis into a message about the missing
14/// parenthesis rather than a rule that parses and means something nobody wrote.
15const RESERVED: [&str; 6] = ["rule", "simplify", "lower", "if", "spec", "bounded"];
16
17/// Read every rule in one file.
18///
19/// # Errors
20///
21/// Returns every error found rather than the first. After a malformed rule the reader skips to
22/// the next `(rule`, so one missing parenthesis does not turn the rest of the file into noise.
23pub fn parse(path: &str, text: &str) -> Result<Vec<Rule>, Vec<Error>> {
24    let tokens = match tokens(path, text) {
25        Ok(tokens) => tokens,
26        Err(error) => return Err(vec![error]),
27    };
28    let mut reader = Reader { path, tokens: &tokens, at: 0, end: end_of(text) };
29    let mut rules = Vec::new();
30    let mut errors = Vec::new();
31
32    while reader.at < reader.tokens.len() {
33        match reader.rule() {
34            Ok(rule) => {
35                check(path, &rule, &mut errors);
36                rules.push(rule);
37            }
38            Err(error) => {
39                errors.push(error);
40                reader.resync();
41            }
42        }
43    }
44
45    if errors.is_empty() { Ok(rules) } else { Err(errors) }
46}
47
48/// Read a file of bare terms rather than of rules.
49///
50/// The machine model is written in the same language as the rules and is not a rule, so this is
51/// how it is read. Keeping one reader for both is the point: a model written in a second syntax
52/// would be a second thing to get wrong.
53///
54/// # Errors
55///
56/// The first malformed term, since a model file has no rule boundaries to resynchronise on.
57pub fn parse_terms(path: &str, text: &str) -> Result<Vec<Term>, Vec<Error>> {
58    let tokens = match tokens(path, text) {
59        Ok(tokens) => tokens,
60        Err(error) => return Err(vec![error]),
61    };
62    let mut reader = Reader { path, tokens: &tokens, at: 0, end: end_of(text) };
63    let mut out = Vec::new();
64    while reader.at < reader.tokens.len() {
65        match reader.term() {
66            Ok(term) => out.push(term),
67            Err(error) => return Err(vec![error]),
68        }
69    }
70    Ok(out)
71}
72
73/// Where the end of the file is, so that running out of tokens can be reported somewhere real.
74fn end_of(text: &str) -> (u32, u32) {
75    let line = 1 + u32::try_from(text.matches('\n').count()).unwrap_or(u32::MAX);
76    let column = 1 + u32::try_from(text.rsplit('\n').next().unwrap_or_default().chars().count())
77        .unwrap_or(u32::MAX);
78    (line, column)
79}
80
81/// One pass over the tokens of one file.
82#[derive(Debug)]
83struct Reader<'a> {
84    path: &'a str,
85    tokens: &'a [Spanned<'a>],
86    at: usize,
87    end: (u32, u32),
88}
89
90impl<'a> Reader<'a> {
91    fn error(&self, message: String) -> Error {
92        let (line, column) = match self.tokens.get(self.at) {
93            Some(token) => (token.line, token.column),
94            None => self.end,
95        };
96        Error { path: self.path.to_owned(), line, column, message }
97    }
98
99    fn peek(&self) -> Option<&'a Token<'a>> {
100        self.tokens.get(self.at).map(|t| &t.token)
101    }
102
103    /// Whether a clause of the given name starts here. A guard is optional and this is how its
104    /// absence is told from a replacement that happens to be an application.
105    fn at_clause(&self, name: &str) -> bool {
106        matches!(self.peek(), Some(Token::Open))
107            && matches!(self.tokens.get(self.at + 1).map(|t| &t.token), Some(Token::Atom(a)) if *a == name)
108    }
109
110    fn open(&mut self) -> Result<(), Error> {
111        match self.peek() {
112            Some(Token::Open) => {
113                self.at += 1;
114                Ok(())
115            }
116            _ => Err(self.error("expected a `(`".to_owned())),
117        }
118    }
119
120    /// A closing parenthesis, named after what it closes. When the file simply ran out, saying
121    /// which thing is still open is the difference between a message that locates the missing
122    /// parenthesis and one that only reports where the reader gave up.
123    fn close(&mut self, what: &str) -> Result<(), Error> {
124        match self.peek() {
125            Some(Token::Close) => {
126                self.at += 1;
127                Ok(())
128            }
129            None => Err(self.error(format!("`({what}` was never closed"))),
130            _ => Err(self.error("expected a `)`".to_owned())),
131        }
132    }
133
134    fn keyword(&mut self, name: &str) -> Result<(), Error> {
135        match self.peek() {
136            Some(Token::Atom(a)) if *a == name => {
137                self.at += 1;
138                Ok(())
139            }
140            _ => Err(self.error(format!("expected `{name}`"))),
141        }
142    }
143
144    /// A whole rule, from its `(` to its `)`.
145    fn rule(&mut self) -> Result<Rule, Error> {
146        let (line, column) = match self.tokens.get(self.at) {
147            Some(token) => (token.line, token.column),
148            None => self.end,
149        };
150        self.open()?;
151        self.keyword("rule")?;
152
153        self.open()?;
154        let kind = self.rule_kind()?;
155        let pattern = self.term()?;
156        self.close(kind.as_str())?;
157
158        let guard = if self.at_clause("if") {
159            self.at += 1;
160            self.at += 1;
161            let guard = self.term()?;
162            self.close("if")?;
163            Some(guard)
164        } else {
165            None
166        };
167
168        let replacement = self.term()?;
169
170        self.open()?;
171        self.keyword("spec")?;
172        let spec = self.term()?;
173        self.close("spec")?;
174
175        // Last, because it is about what happens to the claim rather than part of it, and
176        // optional, because most rules have no reason to expect the solver to struggle.
177        let bounded = if self.at_clause("bounded") {
178            self.at += 2;
179            let why = self.string()?;
180            self.close("bounded")?;
181            Some(why)
182        } else {
183            None
184        };
185
186        self.close("rule")?;
187        Ok(Rule { kind, pattern, guard, replacement, spec, bounded, line, column })
188    }
189
190    /// Which of the two keywords opened the pattern.
191    ///
192    /// Named in the error rather than left to `keyword`, because a rule that says neither is
193    /// usually a rule somebody wrote a third word in, and the message should say what the two
194    /// are rather than only that one of them is missing.
195    fn rule_kind(&mut self) -> Result<RuleKind, Error> {
196        match self.peek() {
197            Some(Token::Atom(a)) if *a == RuleKind::Simplify.as_str() => {
198                self.at += 1;
199                Ok(RuleKind::Simplify)
200            }
201            Some(Token::Atom(a)) if *a == RuleKind::Lower.as_str() => {
202                self.at += 1;
203                Ok(RuleKind::Lower)
204            }
205            _ => Err(self.error("expected `simplify` or `lower`".to_owned())),
206        }
207    }
208
209    /// The prose in a `(bounded ...)` clause.
210    fn string(&mut self) -> Result<String, Error> {
211        match self.peek() {
212            Some(Token::Str(text)) if !text.trim().is_empty() => {
213                let text = (*text).to_owned();
214                self.at += 1;
215                Ok(text)
216            }
217            Some(Token::Str(_)) => {
218                Err(self.error("a bounded proof needs a reason somebody signed for".to_owned()))
219            }
220            _ => Err(self.error("expected a reason, in quotation marks".to_owned())),
221        }
222    }
223
224    fn term(&mut self) -> Result<Term, Error> {
225        let Some(token) = self.tokens.get(self.at) else {
226            return Err(self.error("expected a term and the file ended".to_owned()));
227        };
228        let (line, column) = (token.line, token.column);
229        match &token.token {
230            Token::Int(value) => {
231                self.at += 1;
232                Ok(Term { kind: TermKind::Int(*value), line, column })
233            }
234            // A bare name is a variable and a parenthesised one is an application. That is the
235            // whole of the distinction, which is why a constructor that takes nothing is still
236            // written `(result)`: without the parentheses there would be no way to tell it from
237            // a variable nobody bound.
238            Token::Atom(name) => {
239                self.at += 1;
240                Ok(Term { kind: TermKind::Var((*name).to_owned()), line, column })
241            }
242            Token::Close => Err(self.error("expected a term and found a `)`".to_owned())),
243            Token::Str(_) => Err(self.error(
244                "a string is prose for a person and is not something a term can be".to_owned(),
245            )),
246            Token::Open => {
247                self.at += 1;
248                let head = match self.peek() {
249                    Some(Token::Atom(head)) => {
250                        let head = *head;
251                        self.at += 1;
252                        head
253                    }
254                    _ => return Err(self.error("expected a name after the `(`".to_owned())),
255                };
256                if RESERVED.contains(&head) {
257                    // Reported at the parenthesis rather than at the name, because what is
258                    // actually missing is a parenthesis somewhere above and this is the first
259                    // place that is visible.
260                    let message =
261                        format!("`{head}` belongs to a rule's own shape, not inside a term");
262                    self.at -= 2;
263                    return Err(self.error(message));
264                }
265                let mut args = Vec::new();
266                while !matches!(self.peek(), Some(Token::Close)) {
267                    if self.peek().is_none() {
268                        return Err(self.error(format!("`({head}` was never closed")));
269                    }
270                    args.push(self.term()?);
271                }
272                self.at += 1;
273                Ok(Term { kind: TermKind::App { head: head.to_owned(), args }, line, column })
274            }
275        }
276    }
277
278    /// Skip to the next thing that looks like the start of a rule, so that one bad rule costs
279    /// one error rather than every error after it.
280    fn resync(&mut self) {
281        self.at += 1;
282        while self.at < self.tokens.len() && !self.at_clause("rule") {
283            self.at += 1;
284        }
285    }
286}
287
288/// The checks that every consumer of a rule would otherwise have to make for itself.
289fn check(path: &str, rule: &Rule, errors: &mut Vec<Error>) {
290    let mut found = Vec::new();
291
292    if !matches!(rule.pattern.kind, TermKind::App { .. }) {
293        found.push((&rule.pattern, "a pattern has to name something to match".to_owned()));
294    }
295
296    // Bound at the first occurrence in the pattern. A name written again is not a second hole:
297    // it is a claim that the two places hold the same thing, which is how `x & x` is said and
298    // which the matcher turns into a test rather than a binding.
299    let mut bound: HashSet<&str> = HashSet::new();
300    let mut in_pattern = Vec::new();
301    rule.pattern.walk(&mut |term| match &term.kind {
302        TermKind::Var(name) => {
303            bound.insert(name.as_str());
304        }
305        TermKind::App { head, .. } if head == RESULT => {
306            let said = "`(result)` is what the replacement produces, so it means nothing here";
307            in_pattern.push((term, said.to_owned()));
308        }
309        _ => {}
310    });
311    found.extend(in_pattern);
312
313    let mut clauses =
314        vec![("the replacement", &rule.replacement), ("the specification", &rule.spec)];
315    if let Some(guard) = &rule.guard {
316        clauses.insert(0, ("the guard", guard));
317    }
318    let mut loose = Vec::new();
319    for (what, term) in clauses {
320        term.walk(&mut |term| match &term.kind {
321            TermKind::Var(name) if !bound.contains(name.as_str()) => {
322                let said = format!("`{name}` is used in {what} and the pattern never bound it");
323                loose.push((term, said));
324            }
325            // The specification is the one place that can talk about what the rule produced,
326            // because it is the only clause written after the fact rather than to make it.
327            TermKind::App { head, .. } if head == RESULT && what != "the specification" => {
328                let said = format!("`(result)` belongs in the specification, not in {what}");
329                loose.push((term, said));
330            }
331            _ => {}
332        });
333    }
334    found.extend(loose);
335
336    for (term, message) in found {
337        errors.push(Error { path: path.to_owned(), line: term.line, column: term.column, message });
338    }
339}