Skip to main content

runmat_parser/parser/
mod.rs

1mod assignment;
2mod classdef;
3mod command;
4mod cursor;
5mod expr;
6mod stmt;
7
8use runmat_lexer::Token;
9
10use crate::{ParserOptions, Program, ScriptSection, Span, Stmt, SyntaxError};
11
12#[derive(Clone)]
13struct TokenInfo {
14    token: Token,
15    lexeme: String,
16    position: usize,
17    end: usize,
18}
19
20struct Parser {
21    tokens: Vec<TokenInfo>,
22    pos: usize,
23    input: String,
24    options: ParserOptions,
25    in_matrix_expr: bool,
26    current_classdef_name: Option<String>,
27    sections: Vec<ScriptSection>,
28}
29
30pub fn parse(input: &str) -> Result<Program, SyntaxError> {
31    parse_with_options(input, ParserOptions::default())
32}
33
34pub fn parse_with_options(input: &str, options: ParserOptions) -> Result<Program, SyntaxError> {
35    use runmat_lexer::tokenize_detailed;
36
37    let toks = tokenize_detailed(input);
38    let sections = script_sections(&toks, input.len());
39    let mut tokens = Vec::new();
40    let mut skip_newlines = false;
41
42    for t in toks {
43        if matches!(t.token, Token::Error) {
44            return Err(SyntaxError {
45                message: format!("Invalid token: '{}'", t.lexeme),
46                position: t.start,
47                found_token: Some(t.lexeme),
48                expected: None,
49            });
50        }
51        // Skip layout-only tokens from lexing.
52        if matches!(
53            t.token,
54            Token::Ellipsis | Token::Section | Token::LineComment | Token::BlockComment
55        ) {
56            // After ellipsis, also drop any immediately following Newline tokens.
57            // The lexer callback already consumed the first \n after `...`; any
58            // additional blank lines should be treated as part of the continuation.
59            skip_newlines = matches!(t.token, Token::Ellipsis);
60            continue;
61        }
62        if skip_newlines && matches!(t.token, Token::Newline) {
63            continue;
64        }
65        skip_newlines = false;
66        tokens.push(TokenInfo {
67            token: t.token,
68            lexeme: t.lexeme,
69            position: t.start,
70            end: t.end,
71        });
72    }
73
74    let mut parser = Parser {
75        tokens,
76        pos: 0,
77        input: input.to_string(),
78        options,
79        in_matrix_expr: false,
80        current_classdef_name: None,
81        sections,
82    };
83    parser.parse_program()
84}
85
86impl Parser {
87    fn parse_program(&mut self) -> Result<Program, SyntaxError> {
88        let mut body = Vec::new();
89        while self.pos < self.tokens.len() {
90            if self.consume(&Token::Semicolon)
91                || self.consume(&Token::Comma)
92                || self.consume(&Token::Newline)
93            {
94                continue;
95            }
96            body.push(self.parse_stmt_with_semicolon()?);
97        }
98        Ok(Program {
99            body,
100            sections: std::mem::take(&mut self.sections),
101        })
102    }
103
104    fn finalize_stmt(&self, stmt: Stmt, is_semicolon_terminated: bool) -> Stmt {
105        match stmt {
106            Stmt::ExprStmt(expr, _, span) => Stmt::ExprStmt(expr, is_semicolon_terminated, span),
107            Stmt::Assign(name, expr, _, span) => {
108                Stmt::Assign(name, expr, is_semicolon_terminated, span)
109            }
110            Stmt::MultiAssign(names, expr, _, span) => {
111                Stmt::MultiAssign(names, expr, is_semicolon_terminated, span)
112            }
113            Stmt::AssignLValue(lv, expr, _, span) => {
114                Stmt::AssignLValue(lv, expr, is_semicolon_terminated, span)
115            }
116            other => other,
117        }
118    }
119}
120
121fn script_sections(tokens: &[runmat_lexer::SpannedToken], source_len: usize) -> Vec<ScriptSection> {
122    let markers: Vec<_> = tokens
123        .iter()
124        .filter(|token| matches!(token.token, Token::Section))
125        .collect();
126    markers
127        .iter()
128        .enumerate()
129        .map(|(index, marker)| {
130            let marker_text = marker.lexeme.trim_end_matches(['\r', '\n']);
131            ScriptSection {
132                ordinal: index as u32 + 1,
133                title: marker_text
134                    .strip_prefix("%%")
135                    .unwrap_or(marker_text)
136                    .trim()
137                    .to_owned(),
138                marker_span: Span {
139                    start: marker.start,
140                    end: marker.start + marker_text.len(),
141                },
142                body_span: Span {
143                    start: marker.end,
144                    end: markers.get(index + 1).map_or(source_len, |next| next.start),
145                },
146            }
147        })
148        .collect()
149}