Skip to main content

implicit_predicates_gen/point/
parse.rs

1//! Reads point type declarations.
2//!
3//! ```text
4//! decl   := 'point' NAME '(' params? ')' '=' expr ('where' cmps)? ';'
5//! params := param (',' param)* ','?        param := NAME ':' ('F64x3' | 'f64')
6//! cmps   := cmp (',' cmp)*                 cmp   := sum '<=' sum
7//! sum    := (NAME | INTEGER) ('+' (NAME | INTEGER))*
8//! expr   := term (('+' | '-') term)*
9//! term   := unary ('*' unary)*
10//! unary  := '-'* atom
11//! atom   := INTEGER | NAME | '(' expr ')'
12//! ```
13//!
14//! Operations on `F64x3` are per coordinate. A product is an `f64` by an
15//! `F64x3` (in either order) or an `f64` by an `f64`; the sum, the difference
16//! and the negation keep the type; an integer is an `f64`. The value of the
17//! whole expression is an `F64x3`. In a comparison, names are `f64`
18//! parameters.
19//!
20//! Like the expressions of predicates, chains are read in loops and only
21//! parenthesized expressions recurse, within the limit on the depth of
22//! brackets.
23
24use proc_macro2::{Delimiter, LexError, Span, TokenStream, TokenTree};
25
26use super::expr::{Scope, Value, operator_span, parse_expr};
27use super::{
28    Constraint, LinearSum, ParamType, PointBuilder, PointGraph, PointNode, PointParam, PointType,
29};
30use crate::cursor::{Cursor, is_punct};
31use crate::error::{Diag, Error, Res, diag};
32use crate::expr::int_literal;
33use crate::limits;
34use crate::parse::{Position, can_name_a_module};
35
36/// Reads every point type declared in `source`.
37///
38/// `file` is only used to label errors. Points come in the order of their
39/// declarations. A file without points gives an empty list. The functions of
40/// the file are skipped without being read, since [`crate::parse`] reads them
41/// and reports their errors; but what is skipped must have the shape
42/// `#[..]* fn NAME (..) -> Sign {..}`, or it is an error at its position, so
43/// no declaration is lost.
44///
45/// # Limits
46///
47/// No input makes this function panic or overflow the stack of the calling
48/// thread. The only limit is the nesting of brackets, at most 32 deep, counting
49/// the parentheses of the expression of a point; sums, products and runs of
50/// unary minus are read in loops and may be of any length.
51///
52/// # Errors
53///
54/// Returns the first problem found, at the offending token: a token that the
55/// grammar does not allow, a type other than `F64x3` and `f64`, a name that is
56/// a Rust keyword or is defined twice, an unused parameter, a name that is not
57/// a parameter, a product of two `F64x3`, a sum of an `F64x3` and an `f64`, an
58/// expression that is not an `F64x3`, or a `where` clause over an `F64x3`.
59pub fn parse_points(source: &str, file: &str) -> Result<Vec<PointType>, Error> {
60    parse_source(source).map_err(|d| Error {
61        file: file.to_string(),
62        line: d.line,
63        column: d.column,
64        message: d.message,
65    })
66}
67
68fn parse_source(source: &str) -> Res<Vec<PointType>> {
69    limits::check_text(source)?;
70    let tokens: TokenStream = source
71        .parse()
72        .map_err(|e: LexError| diag(e.span(), e.to_string()))?;
73    let mut cur = Cursor::new(tokens, None);
74    let mut points: Vec<PointType> = Vec::new();
75    while !cur.at_end() {
76        if cur.peek_word("point") {
77            read_declaration(&mut cur, &mut points)?;
78        } else {
79            skip_function(&mut cur)?;
80        }
81    }
82    Ok(points)
83}
84
85/// Reads the declaration at `cur` and adds it to `points`, which holds the
86/// ones read before it (their names cannot be repeated).
87pub(crate) fn read_declaration(cur: &mut Cursor, points: &mut Vec<PointType>) -> Res<()> {
88    let point = parse_declaration(cur, points)?;
89    points.push(point);
90    Ok(())
91}
92
93/// Moves past a function of a predicate without reading it: [`crate::parse`]
94/// does. Only the shape `#[..]* fn NAME (..) -> Sign {..}` is skipped; anything
95/// else is an error at its position, so a declaration cannot be lost behind a
96/// malformed function or a stray token.
97fn skip_function(cur: &mut Cursor) -> Res<()> {
98    while cur.peek_punct('#') {
99        cur.bump();
100        if !matches!(cur.peek(), Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Bracket) {
101            return Err(cur.expected("`[`"));
102        }
103        cur.bump();
104    }
105    if !cur.peek_word("fn") {
106        return Err(cur.expected("`point` or `fn`"));
107    }
108    cur.bump();
109    if !matches!(cur.peek(), Some(TokenTree::Ident(_))) {
110        return Err(cur.expected("a function name"));
111    }
112    cur.bump();
113    expect_group(cur, Delimiter::Parenthesis, "`(`")?;
114    if !(cur.peek_punct('-') && is_punct(cur.peek_at(1), '>')) {
115        return Err(cur.expected("`->`"));
116    }
117    cur.bump();
118    cur.bump();
119    if !cur.peek_word("Sign") {
120        return Err(cur.expected("`Sign`"));
121    }
122    cur.bump();
123    expect_group(cur, Delimiter::Brace, "`{`")
124}
125
126/// Moves past a group with the delimiter `delimiter`.
127fn expect_group(cur: &mut Cursor, delimiter: Delimiter, what: &str) -> Res<()> {
128    if !matches!(cur.peek(), Some(TokenTree::Group(g)) if g.delimiter() == delimiter) {
129        return Err(cur.expected(what));
130    }
131    cur.bump();
132    Ok(())
133}
134
135fn parse_declaration(cur: &mut Cursor, seen: &[PointType]) -> Res<PointType> {
136    cur.bump();
137    let (name, name_span) = parse_name(cur, "a point name", "a point")?;
138    if seen.iter().any(|p| p.name == name) {
139        return Err(diag(name_span, format!("`{name}` is already defined")));
140    }
141    let params = parse_params(cur)?;
142    if !cur.peek_punct('=') {
143        return Err(cur.expected("`=`"));
144    }
145    cur.bump();
146    let start = cur.peek().map_or_else(|| cur.close(), TokenTree::span);
147    let mut scope = Scope {
148        builder: PointBuilder::default(),
149        params: &params,
150        env: params
151            .iter()
152            .enumerate()
153            .map(|(i, p)| (p.name.as_str(), i))
154            .collect(),
155    };
156    let value = parse_expr(cur, &mut scope, 0)?;
157    let Value::Vector(roots) = value else {
158        return Err(diag(
159            start,
160            "the value of a point must be an `F64x3`, not an `f64`",
161        ));
162    };
163    let constraints = if cur.peek_word("where") {
164        cur.bump();
165        parse_constraints(cur, &scope)?
166    } else {
167        Vec::new()
168    };
169    if !cur.peek_punct(';') {
170        return Err(cur.expected("an operator, `where` or `;`"));
171    }
172    cur.bump();
173    let graph = scope.builder.finish(roots);
174    if let Some(param) = params
175        .iter()
176        .enumerate()
177        .find_map(|(i, p)| (!is_used(&graph, i)).then_some(p))
178    {
179        return Err(Diag {
180            line: param.position.line,
181            column: param.position.column,
182            message: format!(
183                "unused parameter `{}`: the expression does not use it",
184                param.name
185            ),
186        });
187    }
188    Ok(PointType {
189        name,
190        position: Position::of(name_span),
191        params,
192        graph,
193        constraints,
194    })
195}
196
197/// Tells whether the graph reads parameter `index`.
198fn is_used(graph: &PointGraph, index: usize) -> bool {
199    graph.nodes.iter().any(|n| {
200        matches!(n, PointNode::Coord { param, .. } | PointNode::Scalar(param) if *param == index)
201    })
202}
203
204/// Reads a name that is not a Rust keyword. `what` is for the message when
205/// there is none, `of` for the one when it is reserved.
206fn parse_name(cur: &mut Cursor, what: &str, of: &str) -> Res<(String, Span)> {
207    let Some(TokenTree::Ident(id)) = cur.peek() else {
208        return Err(cur.expected(what));
209    };
210    let (name, span) = (id.to_string(), id.span());
211    if !can_name_a_module(&name) {
212        return Err(diag(
213            span,
214            format!(
215                "`{name}` cannot be used as the name of {of}: \
216                 it is a Rust keyword or a raw identifier"
217            ),
218        ));
219    }
220    cur.bump();
221    Ok((name, span))
222}
223
224/// Reads the parameter list.
225fn parse_params(cur: &mut Cursor) -> Res<Vec<PointParam>> {
226    let (stream, close) = match cur.peek() {
227        Some(TokenTree::Group(g)) if g.delimiter() == Delimiter::Parenthesis => {
228            (g.stream(), g.span_close())
229        }
230        _ => return Err(cur.expected("`(`")),
231    };
232    cur.bump();
233    let mut inside = Cursor::new(stream, Some(close));
234    let mut params: Vec<PointParam> = Vec::new();
235    while !inside.at_end() {
236        let (name, span) = parse_name(&mut inside, "a parameter name", "a parameter")?;
237        if params.iter().any(|p| p.name == name) {
238            return Err(diag(span, format!("`{name}` redefines a name")));
239        }
240        if !inside.peek_punct(':') {
241            return Err(inside.expected("`:`"));
242        }
243        inside.bump();
244        let ty = match inside.peek() {
245            Some(TokenTree::Ident(id)) if id == "F64x3" => ParamType::F64x3,
246            Some(TokenTree::Ident(id)) if id == "f64" => ParamType::F64,
247            Some(token) => {
248                return Err(diag(
249                    token.span(),
250                    "a parameter must have type `F64x3` or `f64`",
251                ));
252            }
253            None => return Err(inside.expected("a type: `F64x3` or `f64`")),
254        };
255        inside.bump();
256        params.push(PointParam {
257            name,
258            ty,
259            position: Position::of(span),
260        });
261        if !inside.at_end() {
262            if !inside.peek_punct(',') {
263                return Err(inside.expected("`,`"));
264            }
265            inside.bump();
266        }
267    }
268    Ok(params)
269}
270
271/// Reads the comparisons after `where`, up to the `;`.
272fn parse_constraints(cur: &mut Cursor, scope: &Scope) -> Res<Vec<Constraint>> {
273    let mut constraints = Vec::new();
274    loop {
275        let start = operator_span(cur);
276        let lhs = parse_sum(cur, scope)?;
277        let is_le = cur.peek_punct('<')
278            && matches!(cur.peek(), Some(TokenTree::Punct(p)) if p.spacing() == proc_macro2::Spacing::Joint)
279            && cur
280                .peek_at(1)
281                .is_some_and(|t| crate::cursor::is_punct(Some(t), '='));
282        if !is_le {
283            return Err(cur.expected("`<=`: a `where` clause only has `<=` comparisons"));
284        }
285        cur.bump();
286        cur.bump();
287        let rhs = parse_sum(cur, scope)?;
288        if lhs.params.is_empty() && rhs.params.is_empty() {
289            return Err(diag(
290                start,
291                "this comparison names no parameter, so it constrains nothing: \
292                 a `where` clause compares `f64` parameters",
293            ));
294        }
295        constraints.push(Constraint {
296            lhs,
297            rhs,
298            position: Position::of(start),
299        });
300        if !cur.peek_punct(',') {
301            return Ok(constraints);
302        }
303        cur.bump();
304    }
305}
306
307/// Reads `a + b + 3`: `f64` parameters and integers joined by `+`.
308fn parse_sum(cur: &mut Cursor, scope: &Scope) -> Res<LinearSum> {
309    let mut sum = LinearSum {
310        params: Vec::new(),
311        constant: 0,
312    };
313    loop {
314        match cur.peek() {
315            Some(TokenTree::Ident(id)) => {
316                let (name, span) = (id.to_string(), id.span());
317                match scope.env.get(name.as_str()) {
318                    None => return Err(diag(span, format!("unknown name `{name}`"))),
319                    Some(&i) if scope.params[i].ty == ParamType::F64x3 => {
320                        return Err(diag(
321                            span,
322                            format!(
323                                "`{name}` is an `F64x3`: a `where` clause only uses `f64` parameters"
324                            ),
325                        ));
326                    }
327                    Some(&i) => sum.params.push(i),
328                }
329            }
330            Some(TokenTree::Literal(lit)) => {
331                let span = lit.span();
332                let value = int_literal(&lit.to_string(), span)?;
333                sum.constant = sum.constant.checked_add(value).ok_or_else(|| {
334                    diag(span, "the sum of the integers does not fit in an `i64`")
335                })?;
336            }
337            _ => return Err(cur.expected("an `f64` parameter or an integer")),
338        }
339        cur.bump();
340        if cur.peek_punct('-') || cur.peek_punct('*') {
341            return Err(diag(
342                operator_span(cur),
343                "only sums are supported in a `where` clause",
344            ));
345        }
346        if !cur.peek_punct('+') {
347            return Ok(sum);
348        }
349        cur.bump();
350    }
351}