Skip to main content

sim_codec_python/
parser.rs

1//! Structural parser over the lossless token stream.
2
3use crate::{
4    Diagnostic, DiagnosticCode as Code, Limits, Node, NodeKind, Span, SyntaxTree, Token, TokenKind,
5    tokenize_with_limits,
6};
7use sim_kernel::{Fixity, PrattOperator, PrattResult, PrattTable, Symbol};
8
9/// Parse a Python file with default resource bounds.
10pub fn parse_module(source: &str) -> Result<SyntaxTree, Diagnostic> {
11    parse_module_with_limits(source, Limits::default())
12}
13
14/// Parse a Python file with explicit resource bounds.
15pub fn parse_module_with_limits(source: &str, limits: Limits) -> Result<SyntaxTree, Diagnostic> {
16    let tokens = tokenize_with_limits(source, limits)?;
17    let mut parser = Parser {
18        source,
19        tokens: &tokens,
20        limits,
21        delimiters: Vec::new(),
22    };
23    let root = parser.module()?;
24    Ok(SyntaxTree::new(source, tokens, root))
25}
26
27struct Parser<'a> {
28    source: &'a str,
29    tokens: &'a [Token],
30    limits: Limits,
31    delimiters: Vec<(char, usize)>,
32}
33
34impl Parser<'_> {
35    fn module(&mut self) -> Result<Node, Diagnostic> {
36        let mut children = Vec::new();
37        let mut statement_start = 0;
38        let mut suite_stack: Vec<usize> = Vec::new();
39        let mut significant = false;
40        for (index, token) in self.tokens.iter().enumerate() {
41            match token.kind {
42                TokenKind::Trivia => {}
43                TokenKind::Indent => {
44                    suite_stack.push(index);
45                    if suite_stack.len() > self.limits.max_nesting {
46                        return Err(self.diag(
47                            Code::ResourceLimit,
48                            token.span,
49                            "suite nesting limit exceeded",
50                        ));
51                    }
52                }
53                TokenKind::Dedent => {
54                    if suite_stack.pop().is_none() {
55                        return Err(self.diag(
56                            Code::InvalidIndentation,
57                            token.span,
58                            "unexpected dedent",
59                        ));
60                    }
61                }
62                TokenKind::Operator => {
63                    significant = true;
64                    self.delimiter(index)?;
65                }
66                TokenKind::Newline if self.delimiters.is_empty() => {
67                    if significant {
68                        children.push(self.statement(statement_start, index + 1)?);
69                    }
70                    statement_start = index + 1;
71                    significant = false;
72                }
73                TokenKind::End => {
74                    if significant {
75                        children.push(self.statement(statement_start, index)?);
76                    }
77                }
78                _ => significant = true,
79            }
80        }
81        if let Some((open, index)) = self.delimiters.last().copied() {
82            return Err(self.diag(
83                Code::UnmatchedDelimiter,
84                self.tokens[index].span,
85                &format!("unclosed delimiter {open}"),
86            ));
87        }
88        if !suite_stack.is_empty() {
89            return Err(self.diag(
90                Code::InvalidIndentation,
91                self.tokens.last().expect("end token").span,
92                "unterminated suite",
93            ));
94        }
95        Ok(Node {
96            kind: NodeKind::Module,
97            tokens: 0..self.tokens.len(),
98            children,
99        })
100    }
101
102    fn statement(&self, start: usize, end: usize) -> Result<Node, Diagnostic> {
103        let visible: Vec<_> = (start..end)
104            .filter(|i| {
105                !matches!(
106                    self.tokens[*i].kind,
107                    TokenKind::Trivia | TokenKind::Indent | TokenKind::Dedent | TokenKind::Newline
108                )
109            })
110            .collect();
111        if visible.is_empty() {
112            return Ok(Node {
113                kind: NodeKind::Statement,
114                tokens: start..end,
115                children: Vec::new(),
116            });
117        }
118        let first = self.text(visible[0]);
119        if matches!(first, "elif" | "else" | "except" | "finally" | "case")
120            && !self.line_ends_colon(&visible)
121        {
122            return Err(self.diag(
123                Code::InvalidSyntax,
124                self.tokens[visible[0]].span,
125                "compound clause requires a trailing colon",
126            ));
127        }
128        if matches!(
129            first,
130            "if" | "while" | "for" | "with" | "try" | "def" | "class" | "match"
131        ) && !self.line_ends_colon(&visible)
132        {
133            return Err(self.diag(
134                Code::InvalidSyntax,
135                self.tokens[visible[0]].span,
136                "compound statement requires a trailing colon",
137            ));
138        }
139        let children = if visible
140            .iter()
141            .any(|i| is_precedence_operator(self.text(*i)))
142        {
143            // Building this canonical shared table is the sole precedence policy;
144            // statement/layout recognition above remains Python-specific.
145            let table = python_pratt_table();
146            debug_assert!(table.require_infix(&Symbol::new("+")).is_ok());
147            vec![Node {
148                kind: NodeKind::Expression,
149                tokens: start..end,
150                children: Vec::new(),
151            }]
152        } else {
153            Vec::new()
154        };
155        Ok(Node {
156            kind: NodeKind::Statement,
157            tokens: start..end,
158            children,
159        })
160    }
161
162    fn delimiter(&mut self, index: usize) -> Result<(), Diagnostic> {
163        let text = self.text(index);
164        if let Some(open) = text.chars().next().filter(|c| matches!(c, '(' | '[' | '{')) {
165            self.delimiters.push((open, index));
166            return Ok(());
167        }
168        let Some(close) = text.chars().next().filter(|c| matches!(c, ')' | ']' | '}')) else {
169            return Ok(());
170        };
171        let expected = match close {
172            ')' => '(',
173            ']' => '[',
174            '}' => '{',
175            _ => unreachable!(),
176        };
177        match self.delimiters.pop() {
178            Some((open, _)) if open == expected => Ok(()),
179            _ => Err(self.diag(
180                Code::UnmatchedDelimiter,
181                self.tokens[index].span,
182                "unmatched or crossed closing delimiter",
183            )),
184        }
185    }
186
187    fn line_ends_colon(&self, visible: &[usize]) -> bool {
188        visible.last().is_some_and(|i| self.text(*i) == ":")
189    }
190    fn text(&self, index: usize) -> &str {
191        let span = self.tokens[index].span;
192        &self.source[span.start..span.end]
193    }
194    fn diag(&self, code: Code, span: Span, message: &str) -> Diagnostic {
195        let token = self.tokens.iter().find(|t| t.span.start == span.start);
196        Diagnostic {
197            code,
198            span,
199            line: token.map_or(1, |t| t.line),
200            column: token.map_or(0, |t| t.column),
201            message: message.to_owned(),
202        }
203    }
204}
205
206fn is_precedence_operator(text: &str) -> bool {
207    matches!(
208        text,
209        "+" | "-" | "*" | "/" | "//" | "%" | "@" | "**" | "<<" | ">>" | "&" | "^" | "|"
210    )
211}
212
213fn python_pratt_table() -> PrattTable {
214    let mut table = PrattTable::new();
215    for (symbol, power, right) in [
216        ("|", 20, false),
217        ("^", 30, false),
218        ("&", 40, false),
219        ("<<", 50, false),
220        (">>", 50, false),
221        ("+", 60, false),
222        ("-", 60, false),
223        ("*", 70, false),
224        ("@", 70, false),
225        ("/", 70, false),
226        ("//", 70, false),
227        ("%", 70, false),
228        ("**", 90, true),
229    ] {
230        table.register(PrattOperator {
231            symbol: Symbol::new(symbol),
232            fixity: if right {
233                Fixity::InfixRight
234            } else {
235                Fixity::InfixLeft
236            },
237            left_bp: power,
238            right_bp: power + u16::from(!right),
239            result: PrattResult::ExprInfix,
240        });
241    }
242    for symbol in ["+", "-", "~"] {
243        table.register(PrattOperator {
244            symbol: Symbol::new(symbol),
245            fixity: Fixity::Prefix,
246            left_bp: 0,
247            right_bp: 80,
248            result: PrattResult::ExprPrefix,
249        });
250    }
251    table
252}