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, 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; 5] = ["rule", "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        self.keyword("lower")?;
155        let pattern = self.term()?;
156        self.close("lower")?;
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 { pattern, guard, replacement, spec, bounded, line, column })
188    }
189
190    /// The prose in a `(bounded ...)` clause.
191    fn string(&mut self) -> Result<String, Error> {
192        match self.peek() {
193            Some(Token::Str(text)) if !text.trim().is_empty() => {
194                let text = (*text).to_owned();
195                self.at += 1;
196                Ok(text)
197            }
198            Some(Token::Str(_)) => {
199                Err(self.error("a bounded proof needs a reason somebody signed for".to_owned()))
200            }
201            _ => Err(self.error("expected a reason, in quotation marks".to_owned())),
202        }
203    }
204
205    fn term(&mut self) -> Result<Term, Error> {
206        let Some(token) = self.tokens.get(self.at) else {
207            return Err(self.error("expected a term and the file ended".to_owned()));
208        };
209        let (line, column) = (token.line, token.column);
210        match &token.token {
211            Token::Int(value) => {
212                self.at += 1;
213                Ok(Term { kind: TermKind::Int(*value), line, column })
214            }
215            // A bare name is a variable and a parenthesised one is an application. That is the
216            // whole of the distinction, which is why a constructor that takes nothing is still
217            // written `(result)`: without the parentheses there would be no way to tell it from
218            // a variable nobody bound.
219            Token::Atom(name) => {
220                self.at += 1;
221                Ok(Term { kind: TermKind::Var((*name).to_owned()), line, column })
222            }
223            Token::Close => Err(self.error("expected a term and found a `)`".to_owned())),
224            Token::Str(_) => Err(self.error(
225                "a string is prose for a person and is not something a term can be".to_owned(),
226            )),
227            Token::Open => {
228                self.at += 1;
229                let head = match self.peek() {
230                    Some(Token::Atom(head)) => {
231                        let head = *head;
232                        self.at += 1;
233                        head
234                    }
235                    _ => return Err(self.error("expected a name after the `(`".to_owned())),
236                };
237                if RESERVED.contains(&head) {
238                    // Reported at the parenthesis rather than at the name, because what is
239                    // actually missing is a parenthesis somewhere above and this is the first
240                    // place that is visible.
241                    let message =
242                        format!("`{head}` belongs to a rule's own shape, not inside a term");
243                    self.at -= 2;
244                    return Err(self.error(message));
245                }
246                let mut args = Vec::new();
247                while !matches!(self.peek(), Some(Token::Close)) {
248                    if self.peek().is_none() {
249                        return Err(self.error(format!("`({head}` was never closed")));
250                    }
251                    args.push(self.term()?);
252                }
253                self.at += 1;
254                Ok(Term { kind: TermKind::App { head: head.to_owned(), args }, line, column })
255            }
256        }
257    }
258
259    /// Skip to the next thing that looks like the start of a rule, so that one bad rule costs
260    /// one error rather than every error after it.
261    fn resync(&mut self) {
262        self.at += 1;
263        while self.at < self.tokens.len() && !self.at_clause("rule") {
264            self.at += 1;
265        }
266    }
267}
268
269/// The checks that every consumer of a rule would otherwise have to make for itself.
270fn check(path: &str, rule: &Rule, errors: &mut Vec<Error>) {
271    let mut found = Vec::new();
272
273    if !matches!(rule.pattern.kind, TermKind::App { .. }) {
274        found.push((&rule.pattern, "a pattern has to name something to match".to_owned()));
275    }
276
277    // Bound at the first occurrence in the pattern, and only there. A name that occurs twice in
278    // one pattern is asking for the two places to be equal, which the matcher has no test for,
279    // so it is refused rather than silently read as two independent holes.
280    let mut bound: HashSet<&str> = HashSet::new();
281    let mut twice = Vec::new();
282    let mut in_pattern = Vec::new();
283    rule.pattern.walk(&mut |term| match &term.kind {
284        TermKind::Var(name) => {
285            if !bound.insert(name.as_str()) {
286                twice.push((term, format!("`{name}` is bound twice in one pattern")));
287            }
288        }
289        TermKind::App { head, .. } if head == RESULT => {
290            let said = "`(result)` is what the replacement produces, so it means nothing here";
291            in_pattern.push((term, said.to_owned()));
292        }
293        _ => {}
294    });
295    found.extend(twice);
296    found.extend(in_pattern);
297
298    let mut clauses =
299        vec![("the replacement", &rule.replacement), ("the specification", &rule.spec)];
300    if let Some(guard) = &rule.guard {
301        clauses.insert(0, ("the guard", guard));
302    }
303    let mut loose = Vec::new();
304    for (what, term) in clauses {
305        term.walk(&mut |term| match &term.kind {
306            TermKind::Var(name) if !bound.contains(name.as_str()) => {
307                let said = format!("`{name}` is used in {what} and the pattern never bound it");
308                loose.push((term, said));
309            }
310            // The specification is the one place that can talk about what the rule produced,
311            // because it is the only clause written after the fact rather than to make it.
312            TermKind::App { head, .. } if head == RESULT && what != "the specification" => {
313                let said = format!("`(result)` belongs in the specification, not in {what}");
314                loose.push((term, said));
315            }
316            _ => {}
317        });
318    }
319    found.extend(loose);
320
321    for (term, message) in found {
322        errors.push(Error { path: path.to_owned(), line: term.line, column: term.column, message });
323    }
324}