Skip to main content

sim_codec_algol/pratt/parser/
core.rs

1//! Core of the Pratt parser: `PrattParser` and the precedence-climbing driver
2//! that tokenizes infix source and parses it into located expression trees
3//! using operator binding powers from the table.
4
5use crate::parse::{
6    ParseCx, SpannedToken, extend_tree_trivia, raw_number_expr, tokenize_algol_spanned_with_budget,
7    tree_origin, with_origin_span,
8};
9use sim_codec::{DecodeBudget, DecodeLimits};
10use sim_kernel::{
11    Error, Expr, Fixity, LocatedExprTree, PrattOperator, PrattTable, PrattToken as Token, Result,
12    SourceId, parse_pratt_symbol as parse_symbol,
13};
14
15/// Precedence-climbing parser for the Algol surface: tokenizes infix source and
16/// parses it into [`LocatedExprTree`] forms using operator binding powers from
17/// its [`PrattTable`].
18pub struct PrattParser {
19    pub(crate) operators: PrattTable,
20}
21
22impl PrattParser {
23    /// Creates a parser driven by the given operator table. Use
24    /// [`crate::default_pratt_table`] for the standard arithmetic operators.
25    pub fn new(operators: PrattTable) -> Self {
26        Self { operators }
27    }
28
29    /// Parses `source` into a located expression tree under a default decode
30    /// budget. `source_id` names the input for origin tracking.
31    pub fn parse_text_tree(
32        &self,
33        codec: sim_kernel::CodecId,
34        source_id: impl Into<String>,
35        source: &str,
36    ) -> Result<LocatedExprTree> {
37        let mut budget = DecodeBudget::new(DecodeLimits::default());
38        self.parse_text_tree_with_budget(codec, source_id, source, &mut budget)
39    }
40
41    /// Parses `source` into a located expression tree under an explicit
42    /// `budget`, erroring if any tokens remain after a complete expression.
43    pub fn parse_text_tree_with_budget(
44        &self,
45        codec: sim_kernel::CodecId,
46        source_id: impl Into<String>,
47        source: &str,
48        budget: &mut DecodeBudget,
49    ) -> Result<LocatedExprTree> {
50        let tokens = tokenize_algol_spanned_with_budget(source, budget)?;
51        budget.check_tokens(codec, tokens.len())?;
52        let mut cx = ParseCx::new(tokens);
53        let expr = self.parse_expr_tree(
54            &mut cx,
55            codec,
56            &SourceId(source_id.into()),
57            source,
58            0,
59            budget,
60            0,
61        )?;
62        if !cx.is_empty() {
63            return Err(Error::Eval("trailing algol tokens".to_owned()));
64        }
65        Ok(expr)
66    }
67
68    #[allow(clippy::too_many_arguments)]
69    pub(super) fn parse_expr_tree(
70        &self,
71        cx: &mut ParseCx,
72        codec: sim_kernel::CodecId,
73        source_id: &SourceId,
74        source: &str,
75        min_bp: u16,
76        budget: &mut DecodeBudget,
77        depth: usize,
78    ) -> Result<LocatedExprTree> {
79        // Charge depth/node budget BEFORE recursing into nud/led so that
80        // unbalanced prefix-operator or open-paren chains (`- - - ...` or
81        // `((((...`) fail closed on the depth limit instead of overflowing the
82        // native stack: those paths recurse through `parse_expr_tree` without
83        // reaching a leaf arm, where the budget was previously the only check.
84        budget.enter_node(codec, depth)?;
85        let mut left = self.parse_nud_tree(cx, codec, source_id, source, budget, depth)?;
86
87        loop {
88            if matches!(
89                cx.peek(),
90                Some(SpannedToken {
91                    token: Token::OpenParen,
92                    ..
93                })
94            ) {
95                if 110 < min_bp {
96                    break;
97                }
98                let open = cx.next_required()?;
99                left = self.parse_call_tree(
100                    cx, codec, source_id, source, left, open.start, budget, depth,
101                )?;
102                continue;
103            }
104
105            let Some(token) = cx.peek().cloned() else {
106                break;
107            };
108
109            let Some(op) = self.operators.lookup_led(&token.token) else {
110                break;
111            };
112
113            if op.fixity == Fixity::Postfix {
114                if op.left_bp < min_bp {
115                    break;
116                }
117                cx.advance();
118                left = self.parse_postfix_tree(
119                    codec, source_id, source, left, op, token.end, budget, depth,
120                )?;
121                continue;
122            }
123
124            if op.left_bp < min_bp {
125                break;
126            }
127
128            cx.advance();
129            left = self.parse_led_tree(cx, codec, source_id, source, left, op, budget, depth)?;
130        }
131
132        Ok(left)
133    }
134
135    fn parse_nud_tree(
136        &self,
137        cx: &mut ParseCx,
138        codec: sim_kernel::CodecId,
139        source_id: &SourceId,
140        source: &str,
141        budget: &mut DecodeBudget,
142        depth: usize,
143    ) -> Result<LocatedExprTree> {
144        let token = cx.next_required()?;
145
146        if let Some(op) = self.operators.lookup_nud(&token.token) {
147            let right =
148                self.parse_expr_tree(cx, codec, source_id, source, op.right_bp, budget, depth + 1)?;
149            return self.build_prefix_tree(
150                codec,
151                source_id,
152                source,
153                op,
154                token.start,
155                right,
156                budget,
157                depth,
158            );
159        }
160
161        match token.token {
162            Token::Ident(name) => {
163                budget.enter_node(codec, depth)?;
164                let expr = match name.as_str() {
165                    "nil" => Expr::Nil,
166                    "true" => Expr::Bool(true),
167                    "false" => Expr::Bool(false),
168                    _ => Expr::Symbol(parse_symbol(&name)),
169                };
170                Ok(LocatedExprTree::without_children(
171                    expr,
172                    Some(tree_origin(
173                        codec,
174                        source_id.clone(),
175                        source,
176                        token.start,
177                        token.end,
178                        token.leading_trivia.clone(),
179                    )),
180                ))
181            }
182            Token::Number(number) => {
183                budget.enter_node(codec, depth)?;
184                Ok(LocatedExprTree::without_children(
185                    raw_number_expr(number),
186                    Some(tree_origin(
187                        codec,
188                        source_id.clone(),
189                        source,
190                        token.start,
191                        token.end,
192                        token.leading_trivia.clone(),
193                    )),
194                ))
195            }
196            Token::String(value) => {
197                budget.enter_node(codec, depth)?;
198                budget.check_string_bytes(codec, value.len())?;
199                Ok(LocatedExprTree::without_children(
200                    Expr::String(value),
201                    Some(tree_origin(
202                        codec,
203                        source_id.clone(),
204                        source,
205                        token.start,
206                        token.end,
207                        token.leading_trivia.clone(),
208                    )),
209                ))
210            }
211            Token::OpenParen => {
212                let mut expr =
213                    self.parse_expr_tree(cx, codec, source_id, source, 0, budget, depth + 1)?;
214                let close = cx.next_required()?;
215                if close.token != Token::CloseParen {
216                    return Err(Error::Eval(format!(
217                        "expected ')' in algol input, found {:?}",
218                        close
219                    )));
220                }
221                extend_tree_trivia(&mut expr, close.leading_trivia.clone());
222                Ok(with_origin_span(
223                    expr,
224                    tree_origin(codec, source_id.clone(), source, token.start, close.end, {
225                        let mut trivia = token.leading_trivia.clone();
226                        trivia.extend(close.leading_trivia.clone());
227                        trivia
228                    }),
229                ))
230            }
231            other => Err(Error::Eval(format!(
232                "unexpected algol token in nud {:?}",
233                other
234            ))),
235        }
236    }
237
238    #[allow(clippy::too_many_arguments)]
239    fn parse_led_tree(
240        &self,
241        cx: &mut ParseCx,
242        codec: sim_kernel::CodecId,
243        source_id: &SourceId,
244        source: &str,
245        left: LocatedExprTree,
246        op: PrattOperator,
247        budget: &mut DecodeBudget,
248        depth: usize,
249    ) -> Result<LocatedExprTree> {
250        match op.fixity {
251            Fixity::InfixLeft | Fixity::InfixRight => {
252                let right = self.parse_expr_tree(
253                    cx,
254                    codec,
255                    source_id,
256                    source,
257                    op.right_bp,
258                    budget,
259                    depth + 1,
260                )?;
261                self.build_infix_tree(codec, source_id, source, op, left, right, budget, depth)
262            }
263            _ => Err(Error::Eval("operator cannot be used here".to_owned())),
264        }
265    }
266}