1use crate::ast::{BinaryOp, Expr, UnaryOp};
6use crate::lexer::{tokenize, Token};
7
8pub const MAX_PARSE_DEPTH: usize = 256;
13
14pub const MAX_PARSE_NODES: usize = 4096;
19
20pub fn parse(src: &str) -> Result<Expr, String> {
23 let tokens = tokenize(src);
24 let mut p = Parser {
25 tokens,
26 pos: 0,
27 depth: 0,
28 nodes: 0,
29 };
30 let expr = p.expr(0)?;
31 if !matches!(p.peek(), Token::Eof) {
32 return Err(format!("unexpected trailing token: {:?}", p.peek()));
33 }
34 Ok(expr)
35}
36
37struct Parser {
38 tokens: Vec<Token>,
39 pos: usize,
40 depth: usize,
42 nodes: usize,
44}
45
46fn infix_bp(tok: &Token) -> Option<(BinaryOp, u8, u8)> {
49 Some(match tok {
50 Token::OrOr => (BinaryOp::Or, 1, 2),
51 Token::AndAnd => (BinaryOp::And, 3, 4),
52 Token::EqEq => (BinaryOp::Eq, 5, 6),
53 Token::NotEq => (BinaryOp::NotEq, 5, 6),
54 Token::Lt => (BinaryOp::Lt, 7, 8),
55 Token::Gt => (BinaryOp::Gt, 7, 8),
56 Token::LtEq => (BinaryOp::LtEq, 7, 8),
57 Token::GtEq => (BinaryOp::GtEq, 7, 8),
58 Token::Plus => (BinaryOp::Add, 9, 10),
59 Token::Minus => (BinaryOp::Sub, 9, 10),
60 Token::Star => (BinaryOp::Mul, 11, 12),
61 Token::Slash => (BinaryOp::Div, 11, 12),
62 Token::Percent => (BinaryOp::Mod, 11, 12),
63 _ => return None,
64 })
65}
66
67impl Parser {
68 fn peek(&self) -> &Token {
69 self.tokens.get(self.pos).unwrap_or(&Token::Eof)
70 }
71
72 fn next(&mut self) -> Token {
73 let t = self.tokens.get(self.pos).cloned().unwrap_or(Token::Eof);
74 self.pos += 1;
75 t
76 }
77
78 fn eat(&mut self, want: &Token) -> Result<(), String> {
79 if self.peek() == want {
80 self.pos += 1;
81 Ok(())
82 } else {
83 Err(format!("expected {:?}, found {:?}", want, self.peek()))
84 }
85 }
86
87 fn expr(&mut self, min_bp: u8) -> Result<Expr, String> {
90 self.depth += 1;
91 if self.depth > MAX_PARSE_DEPTH {
92 self.depth -= 1;
93 return Err("expression nesting too deep".to_string());
94 }
95 let result = self.expr_inner(min_bp);
96 self.depth -= 1;
97 result
98 }
99
100 fn bump(&mut self) -> Result<(), String> {
103 self.nodes += 1;
104 if self.nodes > MAX_PARSE_NODES {
105 return Err("expression too large".to_string());
106 }
107 Ok(())
108 }
109
110 fn expr_inner(&mut self, min_bp: u8) -> Result<Expr, String> {
111 let mut lhs = self.prefix()?;
112 loop {
113 match self.peek() {
115 Token::Dot => {
116 self.bump()?;
117 self.pos += 1;
118 let name = match self.next() {
119 Token::Ident(n) => n,
120 Token::True => "true".to_string(),
122 Token::False => "false".to_string(),
123 Token::Null => "null".to_string(),
124 other => return Err(format!("expected member name, found {other:?}")),
125 };
126 lhs = Expr::Member(Box::new(lhs), name);
127 continue;
128 }
129 Token::LBracket => {
130 self.bump()?;
131 self.pos += 1;
132 let idx = self.expr(0)?;
133 self.eat(&Token::RBracket)?;
134 lhs = Expr::Index(Box::new(lhs), Box::new(idx));
135 continue;
136 }
137 Token::LParen => {
138 self.bump()?;
139 self.pos += 1;
140 let args = self.args()?;
141 self.eat(&Token::RParen)?;
142 lhs = Expr::Call(Box::new(lhs), args);
143 continue;
144 }
145 _ => {}
146 }
147 let Some((op, lbp, rbp)) = infix_bp(self.peek()) else {
149 break;
150 };
151 if lbp < min_bp {
152 break;
153 }
154 self.bump()?;
155 self.pos += 1;
156 let rhs = self.expr(rbp)?;
157 lhs = Expr::Binary(op, Box::new(lhs), Box::new(rhs));
158 }
159 Ok(lhs)
160 }
161
162 fn args(&mut self) -> Result<Vec<Expr>, String> {
163 let mut out = Vec::new();
164 if matches!(self.peek(), Token::RParen | Token::RBracket) {
166 return Ok(out);
167 }
168 loop {
169 out.push(self.expr(0)?);
170 match self.peek() {
171 Token::Comma => {
172 self.pos += 1;
173 if matches!(self.peek(), Token::RParen | Token::RBracket) {
175 break;
176 }
177 }
178 _ => break,
179 }
180 }
181 Ok(out)
182 }
183
184 fn prefix(&mut self) -> Result<Expr, String> {
185 match self.next() {
186 Token::Number(n) => Ok(Expr::Number(n)),
187 Token::Str(s) => Ok(Expr::Str(s)),
188 Token::Regex(p, f) => Ok(Expr::Regex(p, f)),
189 Token::Ident(name) => Ok(Expr::Ident(name)),
190 Token::True => Ok(Expr::Bool(true)),
191 Token::False => Ok(Expr::Bool(false)),
192 Token::Null => Ok(Expr::Null),
193 Token::Bang => Ok(Expr::Unary(UnaryOp::Not, Box::new(self.expr(13)?))),
194 Token::Minus => Ok(Expr::Unary(UnaryOp::Neg, Box::new(self.expr(13)?))),
195 Token::LParen => {
196 let e = self.expr(0)?;
197 self.eat(&Token::RParen)?;
198 Ok(e)
199 }
200 Token::LBracket => {
201 let items = self.args()?;
203 self.eat(&Token::RBracket)?;
204 Ok(Expr::Call(Box::new(Expr::Ident("__list".into())), items))
206 }
207 other => Err(format!("unexpected token: {other:?}")),
208 }
209 }
210}
211
212#[cfg(test)]
213mod tests {
214 use super::*;
215 use crate::ast::BinaryOp;
216
217 #[test]
218 fn precedence_mul_over_add() {
219 let e = parse("1 + 2 * 3").unwrap();
220 assert_eq!(
222 e,
223 Expr::Binary(
224 BinaryOp::Add,
225 Box::new(Expr::Number(1.0)),
226 Box::new(Expr::Binary(
227 BinaryOp::Mul,
228 Box::new(Expr::Number(2.0)),
229 Box::new(Expr::Number(3.0)),
230 )),
231 )
232 );
233 }
234
235 #[test]
236 fn comparison_below_arithmetic() {
237 let e = parse("a + 1 == b").unwrap();
238 assert!(matches!(e, Expr::Binary(BinaryOp::Eq, _, _)));
240 }
241
242 #[test]
243 fn and_below_or_binding() {
244 let e = parse("a || b && c").unwrap();
246 match e {
247 Expr::Binary(BinaryOp::Or, l, r) => {
248 assert_eq!(*l, Expr::Ident("a".into()));
249 assert!(matches!(*r, Expr::Binary(BinaryOp::And, _, _)));
250 }
251 other => panic!("expected Or at top, got {other:?}"),
252 }
253 }
254
255 #[test]
256 fn member_call_chain() {
257 let e = parse("file.hasTag(\"x\")").unwrap();
258 match e {
259 Expr::Call(callee, args) => {
260 assert_eq!(args.len(), 1);
261 assert_eq!(
262 *callee,
263 Expr::Member(Box::new(Expr::Ident("file".into())), "hasTag".into())
264 );
265 }
266 other => panic!("expected call, got {other:?}"),
267 }
268 }
269
270 #[test]
271 fn method_on_call_result() {
272 let e = parse("link(\"a\").linksTo(x)").unwrap();
274 assert!(matches!(e, Expr::Call(_, _)));
275 }
276
277 #[test]
278 fn not_and_neg() {
279 assert!(matches!(parse("!a").unwrap(), Expr::Unary(UnaryOp::Not, _)));
280 assert!(matches!(parse("-5").unwrap(), Expr::Unary(UnaryOp::Neg, _)));
281 }
282
283 #[test]
284 fn index_access() {
285 let e = parse("categories[0]").unwrap();
286 assert!(matches!(e, Expr::Index(_, _)));
287 }
288
289 #[test]
290 fn list_literal_becomes_list_call() {
291 let e = parse("[1, 2, 3]").unwrap();
292 match e {
293 Expr::Call(callee, args) => {
294 assert_eq!(*callee, Expr::Ident("__list".into()));
295 assert_eq!(args.len(), 3);
296 }
297 other => panic!("expected list call, got {other:?}"),
298 }
299 }
300
301 #[test]
302 fn grouping() {
303 let e = parse("(1 + 2) * 3").unwrap();
304 assert!(matches!(e, Expr::Binary(BinaryOp::Mul, _, _)));
305 }
306
307 #[test]
308 fn trailing_token_is_error() {
309 assert!(parse("1 2").is_err());
310 }
311
312 #[test]
313 fn negation_of_call() {
314 let e = parse("!file.inFolder(\"Misc\")").unwrap();
316 assert!(matches!(e, Expr::Unary(UnaryOp::Not, _)));
317 }
318
319 #[test]
320 fn deeply_nested_input_errors_instead_of_overflowing() {
321 let parens = "(".repeat(100_000);
323 assert!(parse(&parens).is_err());
324 let bangs = format!("{}x", "!".repeat(100_000));
325 assert!(parse(&bangs).is_err());
326 let negs = format!("{}x", "-".repeat(100_000));
327 assert!(parse(&negs).is_err());
328 }
329
330 #[test]
331 fn long_postfix_chain_errors_on_node_budget() {
332 let chain = format!("a{}", ".a".repeat(100_000));
335 assert!(parse(&chain).is_err());
336 }
337
338 #[test]
339 fn normal_expressions_stay_under_the_budget() {
340 assert!(parse(
342 r#"categories.contains(link("Categories/Books", "Books")) && !file.inFolder("Misc")"#
343 )
344 .is_ok());
345 }
346}