runmat_parser/parser/
mod.rs1mod 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 if matches!(
53 t.token,
54 Token::Ellipsis | Token::Section | Token::LineComment | Token::BlockComment
55 ) {
56 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}