1use std::ops::Range;
45
46pub type Span = Range<usize>;
48
49#[derive(Debug, Clone)]
50pub struct Spanned<T> {
51 pub value: T,
52 pub span: Span,
53}
54
55#[derive(Debug, Clone)]
56pub enum Expr {
57 Literal(Literal),
58 Path(Vec<String>),
61 Not(Box<Spanned<Expr>>),
62 BinOp(BinOp, Box<Spanned<Expr>>, Box<Spanned<Expr>>),
63 Ternary(Box<Spanned<Expr>>, Box<Spanned<Expr>>, Box<Spanned<Expr>>),
64 Call(String, Vec<Spanned<Expr>>),
68 Assign(Vec<String>, Box<Spanned<Expr>>),
72 Seq(Vec<Spanned<Expr>>),
76}
77
78#[derive(Debug, Clone)]
79pub enum Literal {
80 Null,
81 Bool(bool),
82 Number(f64),
83 String(String),
84}
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum BinOp {
88 And,
89 Or,
90 Eq,
91 Ne,
92 Lt,
93 Le,
94 Gt,
95 Ge,
96 Plus,
101}
102
103#[derive(Debug, Clone)]
104pub struct ParseError {
105 pub message: String,
106 pub span: Span,
107 pub hint: Option<String>,
108}
109
110#[derive(Debug, Clone, PartialEq)]
113enum Tok {
114 LParen,
115 RParen,
116 Bang,
117 AndAnd,
118 OrOr,
119 EqEq,
120 BangEq,
121 Lt,
122 Le,
123 Gt,
124 Ge,
125 Plus,
126 Question,
127 Colon,
128 Dot,
129 Comma,
131 Semi,
133 Eq,
136 Ident(String),
137 StringLit(String),
138 NumberLit(f64),
139 True,
140 False,
141 Null,
142 Eof,
143}
144
145struct Lexer<'a> {
146 src: &'a [u8],
147 pos: usize,
148}
149
150impl<'a> Lexer<'a> {
151 fn new(src: &'a str) -> Self {
152 Self {
153 src: src.as_bytes(),
154 pos: 0,
155 }
156 }
157
158 fn peek(&self, offset: usize) -> Option<u8> {
159 self.src.get(self.pos + offset).copied()
160 }
161
162 fn skip_whitespace(&mut self) {
163 while let Some(c) = self.peek(0) {
164 if c.is_ascii_whitespace() {
165 self.pos += 1;
166 } else {
167 break;
168 }
169 }
170 }
171
172 fn next(&mut self) -> Result<(Tok, Span), ParseError> {
173 self.skip_whitespace();
174 let start = self.pos;
175 let Some(c) = self.peek(0) else {
176 return Ok((Tok::Eof, start..start));
177 };
178 let tok = match c {
179 b'(' => {
180 self.pos += 1;
181 Tok::LParen
182 }
183 b')' => {
184 self.pos += 1;
185 Tok::RParen
186 }
187 b',' => {
188 self.pos += 1;
189 Tok::Comma
190 }
191 b';' => {
192 self.pos += 1;
193 Tok::Semi
194 }
195 b'+' => {
196 self.pos += 1;
197 Tok::Plus
198 }
199 b'*' | b'/' | b'%' => {
203 return Err(self.err(
204 start..start + 1,
205 &format!(
206 "arithmetic operator `{}` is not supported in pine-expr",
207 c as char
208 ),
209 Some(
210 "compute Rust-side as a `#[computed]` field and bind by name — see docs/guides/poco/04-expressions.md",
211 ),
212 ));
213 }
214 b'-' if !matches!(self.peek(1), Some(b'0'..=b'9')) => {
220 return Err(self.err(
221 start..start + 1,
222 "arithmetic subtraction is not supported in pine-expr",
223 Some(
224 "compute Rust-side as a `#[computed]` field and bind by name — see docs/guides/poco/04-expressions.md",
225 ),
226 ));
227 }
228 b'?' => {
229 self.pos += 1;
230 if self.peek(0) == Some(b'?') {
231 return Err(self.err(
232 start..self.pos + 1,
233 "nullish coalescing `??` is not supported in pine-expr",
234 Some("use a ternary (`x == null ? fallback : x`) or compute Rust-side"),
235 ));
236 }
237 if self.peek(0) == Some(b'.') {
238 return Err(self.err(
239 start..self.pos + 1,
240 "optional chaining `?.` is not supported in pine-expr",
241 Some(
242 "compute Rust-side as a `#[computed]` field that handles the null case",
243 ),
244 ));
245 }
246 Tok::Question
247 }
248 b':' => {
249 self.pos += 1;
250 Tok::Colon
251 }
252 b'.' => {
253 self.pos += 1;
254 if self.peek(0) == Some(b'.') {
255 return Err(self.err(
256 start..self.pos + 1,
257 "spread `...` is not supported in pine-expr",
258 Some("pass arguments explicitly or compute Rust-side"),
259 ));
260 }
261 Tok::Dot
262 }
263 b'!' => {
264 self.pos += 1;
265 if self.peek(0) == Some(b'=') {
266 self.pos += 1;
267 if self.peek(0) == Some(b'=') {
268 return Err(self.err(
269 start..self.pos + 1,
270 "`!==` is not supported in pine-expr",
271 Some("use `!=` (pine-expr uses Rust-style equality)"),
272 ));
273 }
274 Tok::BangEq
275 } else {
276 Tok::Bang
277 }
278 }
279 b'=' => {
280 if self.peek(1) == Some(b'=') {
286 if self.peek(2) == Some(b'=') {
287 return Err(self.err(
288 start..self.pos + 3,
289 "`===` is not supported in pine-expr",
290 Some("use `==` (pine-expr uses Rust-style equality)"),
291 ));
292 }
293 self.pos += 2;
294 Tok::EqEq
295 } else if self.peek(1) == Some(b'>') {
296 return Err(self.err(
297 start..self.pos + 2,
298 "arrow functions are not supported in pine-expr",
299 Some(
300 "define a handler method on the component — see docs/guides/poco/04-expressions.md",
301 ),
302 ));
303 } else {
304 self.pos += 1;
305 Tok::Eq
306 }
307 }
308 b'&' => {
309 if self.peek(1) != Some(b'&') {
310 return Err(self.err(start..start + 1, "expected `&&`", None));
311 }
312 self.pos += 2;
313 Tok::AndAnd
314 }
315 b'|' => {
316 if self.peek(1) != Some(b'|') {
317 return Err(self.err(start..start + 1, "expected `||`", None));
318 }
319 self.pos += 2;
320 Tok::OrOr
321 }
322 b'<' => {
323 self.pos += 1;
324 if self.peek(0) == Some(b'=') {
325 self.pos += 1;
326 Tok::Le
327 } else {
328 Tok::Lt
329 }
330 }
331 b'>' => {
332 self.pos += 1;
333 if self.peek(0) == Some(b'=') {
334 self.pos += 1;
335 Tok::Ge
336 } else {
337 Tok::Gt
338 }
339 }
340 b'"' | b'\'' => self.string(c, start)?,
341 b'0'..=b'9' => self.number(start)?,
342 b'-' if matches!(self.peek(1), Some(b'0'..=b'9')) => self.number(start)?,
343 c if is_ident_start(c) => self.ident(start),
344 _ => {
345 return Err(self.err(
346 start..start + 1,
347 &format!("unexpected character {:?}", c as char),
348 None,
349 ));
350 }
351 };
352 Ok((tok, start..self.pos))
353 }
354
355 fn string(&mut self, quote: u8, start: usize) -> Result<Tok, ParseError> {
356 self.pos += 1; let content_start = self.pos;
358 while let Some(c) = self.peek(0) {
359 if c == quote {
360 let s = std::str::from_utf8(&self.src[content_start..self.pos])
361 .unwrap_or_default()
362 .to_string();
363 self.pos += 1; return Ok(Tok::StringLit(s));
365 }
366 self.pos += 1;
367 }
368 Err(self.err(start..self.pos, "unterminated string literal", None))
369 }
370
371 fn number(&mut self, start: usize) -> Result<Tok, ParseError> {
372 if self.peek(0) == Some(b'-') {
373 self.pos += 1;
374 }
375 while matches!(self.peek(0), Some(b'0'..=b'9')) {
376 self.pos += 1;
377 }
378 if self.peek(0) == Some(b'.') && matches!(self.peek(1), Some(b'0'..=b'9')) {
379 self.pos += 1; while matches!(self.peek(0), Some(b'0'..=b'9')) {
381 self.pos += 1;
382 }
383 }
384 let s = std::str::from_utf8(&self.src[start..self.pos]).unwrap_or_default();
385 let n = s.parse::<f64>().map_err(|_| ParseError {
386 message: format!("invalid number literal {s:?}"),
387 span: start..self.pos,
388 hint: None,
389 })?;
390 Ok(Tok::NumberLit(n))
391 }
392
393 fn ident(&mut self, start: usize) -> Tok {
394 while matches!(self.peek(0), Some(c) if is_ident_continue(c)) {
395 self.pos += 1;
396 }
397 let s = std::str::from_utf8(&self.src[start..self.pos]).unwrap_or_default();
398 match s {
399 "true" => Tok::True,
400 "false" => Tok::False,
401 "null" => Tok::Null,
402 _ => Tok::Ident(s.to_string()),
403 }
404 }
405
406 fn err(&self, span: Span, msg: &str, hint: Option<&str>) -> ParseError {
407 ParseError {
408 message: msg.to_string(),
409 span,
410 hint: hint.map(|s| s.to_string()),
411 }
412 }
413}
414
415fn is_ident_start(c: u8) -> bool {
416 c.is_ascii_alphabetic() || c == b'_' || c == b'$'
417}
418fn is_ident_continue(c: u8) -> bool {
419 c.is_ascii_alphanumeric() || c == b'_' || c == b'$'
420}
421
422struct Parser {
425 toks: Vec<(Tok, Span)>,
426 pos: usize,
427}
428
429impl Parser {
430 fn new(src: &str) -> Result<Self, ParseError> {
431 let mut lex = Lexer::new(src);
432 let mut toks = Vec::new();
433 loop {
434 let (tok, span) = lex.next()?;
435 let is_eof = tok == Tok::Eof;
436 toks.push((tok, span));
437 if is_eof {
438 break;
439 }
440 }
441 Ok(Self { toks, pos: 0 })
442 }
443
444 fn peek(&self) -> &(Tok, Span) {
445 &self.toks[self.pos]
446 }
447
448 fn eat(&mut self, want: &Tok) -> bool {
449 if std::mem::discriminant(&self.peek().0) == std::mem::discriminant(want) {
450 self.pos += 1;
451 true
452 } else {
453 false
454 }
455 }
456
457 fn expect(&mut self, want: &Tok, msg: &str) -> Result<Span, ParseError> {
458 let (tok, span) = self.peek().clone();
459 if std::mem::discriminant(&tok) == std::mem::discriminant(want) {
460 self.pos += 1;
461 Ok(span)
462 } else {
463 Err(ParseError {
464 message: msg.to_string(),
465 span,
466 hint: None,
467 })
468 }
469 }
470
471 fn parse_expr(&mut self) -> Result<Spanned<Expr>, ParseError> {
472 let e = self.parse_stmt_seq()?;
473 if self.peek().0 != Tok::Eof {
474 let span = self.peek().1.clone();
475 return Err(ParseError {
476 message: "unexpected trailing tokens".to_string(),
477 span,
478 hint: None,
479 });
480 }
481 Ok(e)
482 }
483
484 fn parse_stmt_seq(&mut self) -> Result<Spanned<Expr>, ParseError> {
489 let first = self.parse_stmt()?;
490 if !matches!(self.peek().0, Tok::Semi) {
491 return Ok(first);
492 }
493 let mut stmts = vec![first];
494 while self.eat(&Tok::Semi) {
495 if matches!(self.peek().0, Tok::Eof) {
496 break; }
498 stmts.push(self.parse_stmt()?);
499 }
500 let start = stmts.first().map(|s| s.span.start).unwrap_or(0);
501 let end = stmts.last().map(|s| s.span.end).unwrap_or(0);
502 Ok(Spanned {
503 value: Expr::Seq(stmts),
504 span: start..end,
505 })
506 }
507
508 fn parse_stmt(&mut self) -> Result<Spanned<Expr>, ParseError> {
513 let lhs = self.parse_expr_top()?;
514 if !matches!(self.peek().0, Tok::Eq) {
515 return Ok(lhs);
516 }
517 let Expr::Path(segments) = lhs.value else {
519 let span = self.peek().1.clone();
520 return Err(ParseError {
521 message: "left side of `=` must be a path".to_string(),
522 span,
523 hint: Some(
524 "only dotted identifiers like `foo` or `foo.bar` are assignable".to_string(),
525 ),
526 });
527 };
528 self.pos += 1; let rhs = self.parse_expr_top()?;
530 let span = lhs.span.start..rhs.span.end;
531 Ok(Spanned {
532 value: Expr::Assign(segments, Box::new(rhs)),
533 span,
534 })
535 }
536
537 fn parse_expr_top(&mut self) -> Result<Spanned<Expr>, ParseError> {
538 self.parse_ternary()
539 }
540
541 fn parse_ternary(&mut self) -> Result<Spanned<Expr>, ParseError> {
542 let cond = self.parse_or()?;
543 if self.eat(&Tok::Question) {
544 let then_e = self.parse_expr_top()?;
545 self.expect(&Tok::Colon, "expected `:` in ternary expression")?;
546 let else_e = self.parse_expr_top()?;
547 let span = cond.span.start..else_e.span.end;
548 return Ok(Spanned {
549 value: Expr::Ternary(Box::new(cond), Box::new(then_e), Box::new(else_e)),
550 span,
551 });
552 }
553 Ok(cond)
554 }
555
556 fn parse_or(&mut self) -> Result<Spanned<Expr>, ParseError> {
557 let mut lhs = self.parse_and()?;
558 while self.eat(&Tok::OrOr) {
559 let rhs = self.parse_and()?;
560 let span = lhs.span.start..rhs.span.end;
561 lhs = Spanned {
562 value: Expr::BinOp(BinOp::Or, Box::new(lhs), Box::new(rhs)),
563 span,
564 };
565 }
566 Ok(lhs)
567 }
568
569 fn parse_and(&mut self) -> Result<Spanned<Expr>, ParseError> {
570 let mut lhs = self.parse_equality()?;
571 while self.eat(&Tok::AndAnd) {
572 let rhs = self.parse_equality()?;
573 let span = lhs.span.start..rhs.span.end;
574 lhs = Spanned {
575 value: Expr::BinOp(BinOp::And, Box::new(lhs), Box::new(rhs)),
576 span,
577 };
578 }
579 Ok(lhs)
580 }
581
582 fn parse_equality(&mut self) -> Result<Spanned<Expr>, ParseError> {
583 let mut lhs = self.parse_relation()?;
584 loop {
585 let op = match self.peek().0 {
586 Tok::EqEq => BinOp::Eq,
587 Tok::BangEq => BinOp::Ne,
588 _ => break,
589 };
590 self.pos += 1;
591 let rhs = self.parse_relation()?;
592 let span = lhs.span.start..rhs.span.end;
593 lhs = Spanned {
594 value: Expr::BinOp(op, Box::new(lhs), Box::new(rhs)),
595 span,
596 };
597 }
598 Ok(lhs)
599 }
600
601 fn parse_relation(&mut self) -> Result<Spanned<Expr>, ParseError> {
602 let mut lhs = self.parse_additive()?;
603 loop {
604 let op = match self.peek().0 {
605 Tok::Le => BinOp::Le,
606 Tok::Lt => BinOp::Lt,
607 Tok::Ge => BinOp::Ge,
608 Tok::Gt => BinOp::Gt,
609 _ => break,
610 };
611 self.pos += 1;
612 let rhs = self.parse_additive()?;
613 let span = lhs.span.start..rhs.span.end;
614 lhs = Spanned {
615 value: Expr::BinOp(op, Box::new(lhs), Box::new(rhs)),
616 span,
617 };
618 }
619 Ok(lhs)
620 }
621
622 fn parse_additive(&mut self) -> Result<Spanned<Expr>, ParseError> {
623 let mut lhs = self.parse_unary()?;
624 while self.eat(&Tok::Plus) {
625 let rhs = self.parse_unary()?;
626 let span = lhs.span.start..rhs.span.end;
627 lhs = Spanned {
628 value: Expr::BinOp(BinOp::Plus, Box::new(lhs), Box::new(rhs)),
629 span,
630 };
631 }
632 Ok(lhs)
633 }
634
635 fn parse_unary(&mut self) -> Result<Spanned<Expr>, ParseError> {
636 let (tok, span) = self.peek().clone();
637 if tok == Tok::Bang {
638 self.pos += 1;
639 let inner = self.parse_unary()?;
640 let outer = span.start..inner.span.end;
641 return Ok(Spanned {
642 value: Expr::Not(Box::new(inner)),
643 span: outer,
644 });
645 }
646 self.parse_primary()
647 }
648
649 fn parse_primary(&mut self) -> Result<Spanned<Expr>, ParseError> {
650 let (tok, span) = self.peek().clone();
651 match tok {
652 Tok::LParen => {
653 self.pos += 1;
654 let e = self.parse_expr_top()?;
655 self.expect(&Tok::RParen, "expected `)`")?;
656 Ok(e)
657 }
658 Tok::StringLit(s) => {
659 self.pos += 1;
660 Ok(Spanned {
661 value: Expr::Literal(Literal::String(s)),
662 span,
663 })
664 }
665 Tok::NumberLit(n) => {
666 self.pos += 1;
667 Ok(Spanned {
668 value: Expr::Literal(Literal::Number(n)),
669 span,
670 })
671 }
672 Tok::True => {
673 self.pos += 1;
674 Ok(Spanned {
675 value: Expr::Literal(Literal::Bool(true)),
676 span,
677 })
678 }
679 Tok::False => {
680 self.pos += 1;
681 Ok(Spanned {
682 value: Expr::Literal(Literal::Bool(false)),
683 span,
684 })
685 }
686 Tok::Null => {
687 self.pos += 1;
688 Ok(Spanned {
689 value: Expr::Literal(Literal::Null),
690 span,
691 })
692 }
693 Tok::Ident(first) => {
694 self.pos += 1;
695 let start = span.start;
696 if matches!(self.peek().0, Tok::LParen) {
701 self.pos += 1; let mut args = Vec::new();
703 if !matches!(self.peek().0, Tok::RParen) {
704 loop {
705 args.push(self.parse_expr_top()?);
706 if self.eat(&Tok::Comma) {
707 continue;
708 }
709 break;
710 }
711 }
712 let end = self.peek().1.end;
713 self.expect(&Tok::RParen, "expected `)` closing call arguments")?;
714 return Ok(Spanned {
715 value: Expr::Call(first, args),
716 span: start..end,
717 });
718 }
719 let mut segments = vec![first];
720 let mut end = span.end;
721 while self.eat(&Tok::Dot) {
722 let (tok, s) = self.peek().clone();
723 match tok {
724 Tok::Ident(seg) => {
725 self.pos += 1;
726 end = s.end;
727 segments.push(seg);
728 }
729 _ => {
730 return Err(ParseError {
731 message: "expected identifier after `.`".to_string(),
732 span: s,
733 hint: None,
734 });
735 }
736 }
737 }
738 if matches!(self.peek().0, Tok::LParen) && segments.len() > 1 {
744 return Err(ParseError {
745 message: "method calls on objects are not supported in pine-expr"
746 .to_string(),
747 span: self.peek().1.clone(),
748 hint: Some(
749 "define a plain identifier handler, or a `#[computed]` field, that takes the object as an argument — see docs/guides/poco/04-expressions.md"
750 .to_string(),
751 ),
752 });
753 }
754 Ok(Spanned {
755 value: Expr::Path(segments),
756 span: start..end,
757 })
758 }
759 _ => Err(ParseError {
760 message: "expected expression".to_string(),
761 span,
762 hint: None,
763 }),
764 }
765 }
766}
767
768pub fn parse(src: &str) -> Result<Spanned<Expr>, ParseError> {
772 if src.trim().is_empty() {
773 return Err(ParseError {
774 message: "empty expression".to_string(),
775 span: 0..src.len(),
776 hint: None,
777 });
778 }
779 let mut p = Parser::new(src)?;
780 p.parse_expr()
781}
782
783#[cfg(test)]
786mod tests {
787 use super::*;
788
789 fn parse_ok(src: &str) -> Spanned<Expr> {
790 parse(src).unwrap_or_else(|e| panic!("parse failed for {src:?}: {:?}", e))
791 }
792
793 fn parse_err(src: &str) {
794 assert!(parse(src).is_err(), "expected parse error for {src:?}");
795 }
796
797 #[test]
798 fn literals() {
799 matches!(parse_ok("true").value, Expr::Literal(Literal::Bool(true)));
800 matches!(parse_ok("false").value, Expr::Literal(Literal::Bool(false)));
801 matches!(parse_ok("null").value, Expr::Literal(Literal::Null));
802 matches!(parse_ok("42").value, Expr::Literal(Literal::Number(_)));
803 matches!(parse_ok("\"hi\"").value, Expr::Literal(Literal::String(_)));
804 }
805
806 #[test]
807 fn path_segments() {
808 let e = parse_ok("foo.bar.baz");
809 match e.value {
810 Expr::Path(segs) => assert_eq!(segs, vec!["foo", "bar", "baz"]),
811 other => panic!("expected Path, got {other:?}"),
812 }
813 }
814
815 #[test]
816 fn precedence() {
817 let e = parse_ok("a && b || c");
819 match e.value {
820 Expr::BinOp(BinOp::Or, lhs, rhs) => {
821 assert!(matches!(lhs.value, Expr::BinOp(BinOp::And, _, _)));
822 assert!(matches!(rhs.value, Expr::Path(_)));
823 }
824 other => panic!("expected OR at top, got {other:?}"),
825 }
826 }
827
828 #[test]
829 fn not_prefix_and_comparison() {
830 let e = parse_ok("!(a == b)");
831 assert!(matches!(e.value, Expr::Not(_)));
832 }
833
834 #[test]
835 fn ternary_right_associative() {
836 let e = parse_ok("a ? b : c ? d : e");
838 match e.value {
839 Expr::Ternary(_, _, else_e) => {
840 assert!(matches!(else_e.value, Expr::Ternary(_, _, _)));
841 }
842 other => panic!("expected Ternary, got {other:?}"),
843 }
844 }
845
846 #[test]
847 fn string_literals_both_quotes() {
848 matches!(parse_ok("'hello'").value, Expr::Literal(Literal::String(_)));
849 matches!(
850 parse_ok("\"world\"").value,
851 Expr::Literal(Literal::String(_))
852 );
853 }
854
855 #[test]
856 fn errors_carry_spans() {
857 match parse("a ||") {
858 Err(ParseError { span, .. }) => assert!(span.start >= 3),
859 Ok(_) => panic!("expected error"),
860 }
861 }
862
863 #[test]
865 fn parses_assignment() {
866 let e = parse_ok("open = true");
867 match e.value {
868 Expr::Assign(ref path, ref rhs) => {
869 assert_eq!(path, &vec!["open".to_string()]);
870 assert!(matches!(rhs.value, Expr::Literal(Literal::Bool(true))));
871 }
872 other => panic!("expected Assign, got {other:?}"),
873 }
874 }
875
876 #[test]
877 fn parses_call_zero_args() {
878 let e = parse_ok("close()");
879 match e.value {
880 Expr::Call(name, args) => {
881 assert_eq!(name, "close");
882 assert!(args.is_empty());
883 }
884 other => panic!("expected Call, got {other:?}"),
885 }
886 }
887
888 #[test]
889 fn parses_call_with_args() {
890 let e = parse_ok("select(item.value, 42)");
891 match e.value {
892 Expr::Call(name, args) => {
893 assert_eq!(name, "select");
894 assert_eq!(args.len(), 2);
895 assert!(matches!(args[0].value, Expr::Path(_)));
896 assert!(matches!(args[1].value, Expr::Literal(Literal::Number(_))));
897 }
898 other => panic!("expected Call, got {other:?}"),
899 }
900 }
901
902 #[test]
903 fn parses_statement_sequence() {
904 let e = parse_ok("copy($event); close()");
905 match e.value {
906 Expr::Seq(ref stmts) => {
907 assert_eq!(stmts.len(), 2);
908 assert!(matches!(stmts[0].value, Expr::Call(_, _)));
909 assert!(matches!(stmts[1].value, Expr::Call(_, _)));
910 }
911 other => panic!("expected Seq, got {other:?}"),
912 }
913 }
914
915 #[test]
916 fn assignment_rhs_can_be_expression() {
917 let e = parse_ok("open = !open");
919 match e.value {
920 Expr::Assign(path, rhs) => {
921 assert_eq!(path, vec!["open".to_string()]);
922 assert!(matches!(rhs.value, Expr::Not(_)));
923 }
924 other => panic!("expected Assign, got {other:?}"),
925 }
926 }
927
928 #[test]
929 fn rejects_trailing_garbage() {
930 parse_err("a b");
931 }
932
933 #[test]
938 fn and_is_left_associative() {
939 let e = parse_ok("a && b && c");
941 match e.value {
942 Expr::BinOp(BinOp::And, lhs, rhs) => {
943 assert!(
944 matches!(lhs.value, Expr::BinOp(BinOp::And, _, _)),
945 "left arm should be another AND",
946 );
947 assert!(
948 matches!(rhs.value, Expr::Path(_)),
949 "right arm should be a leaf path",
950 );
951 }
952 other => panic!("expected top-level AND, got {other:?}"),
953 }
954 }
955
956 #[test]
957 fn or_is_left_associative() {
958 let e = parse_ok("a || b || c");
959 match e.value {
960 Expr::BinOp(BinOp::Or, lhs, rhs) => {
961 assert!(matches!(lhs.value, Expr::BinOp(BinOp::Or, _, _)));
962 assert!(matches!(rhs.value, Expr::Path(_)));
963 }
964 other => panic!("expected top-level OR, got {other:?}"),
965 }
966 }
967
968 #[test]
969 fn equality_is_left_associative() {
970 let e = parse_ok("a == b == c");
972 match e.value {
973 Expr::BinOp(BinOp::Eq, lhs, rhs) => {
974 assert!(matches!(lhs.value, Expr::BinOp(BinOp::Eq, _, _)));
975 assert!(matches!(rhs.value, Expr::Path(_)));
976 }
977 other => panic!("expected EQ at top, got {other:?}"),
978 }
979 }
980
981 #[test]
982 fn not_or_and_mixed_precedence() {
983 let e = parse_ok("!a || b && c");
986 match e.value {
987 Expr::BinOp(BinOp::Or, lhs, rhs) => {
988 assert!(matches!(lhs.value, Expr::Not(_)), "left is `!a`");
989 assert!(
990 matches!(rhs.value, Expr::BinOp(BinOp::And, _, _)),
991 "right is `b && c`",
992 );
993 }
994 other => panic!("expected OR at top, got {other:?}"),
995 }
996 }
997
998 #[test]
999 fn relation_tighter_than_equality_tighter_than_and() {
1000 let e = parse_ok("a < b == c && d");
1002 match e.value {
1003 Expr::BinOp(BinOp::And, lhs, rhs) => {
1004 match lhs.value {
1005 Expr::BinOp(BinOp::Eq, eq_l, _) => {
1006 assert!(matches!(eq_l.value, Expr::BinOp(BinOp::Lt, _, _)));
1007 }
1008 other => panic!("expected EQ inside AND's left, got {other:?}"),
1009 }
1010 assert!(matches!(rhs.value, Expr::Path(_)));
1011 }
1012 other => panic!("expected AND at top, got {other:?}"),
1013 }
1014 }
1015
1016 #[test]
1017 fn parens_override_precedence() {
1018 let e = parse_ok("a && (b || c)");
1020 match e.value {
1021 Expr::BinOp(BinOp::And, _, rhs) => {
1022 assert!(matches!(rhs.value, Expr::BinOp(BinOp::Or, _, _)));
1023 }
1024 other => panic!("expected AND at top after parens, got {other:?}"),
1025 }
1026 }
1027
1028 #[test]
1029 fn deeply_nested_parens() {
1030 let e = parse_ok("((a || b) && (c || d))");
1033 match e.value {
1034 Expr::BinOp(BinOp::And, lhs, rhs) => {
1035 assert!(matches!(lhs.value, Expr::BinOp(BinOp::Or, _, _)));
1036 assert!(matches!(rhs.value, Expr::BinOp(BinOp::Or, _, _)));
1037 }
1038 other => panic!("expected AND between two OR groups, got {other:?}"),
1039 }
1040 }
1041
1042 #[test]
1043 fn nested_path_many_segments() {
1044 let e = parse_ok("a.b.c.d.e");
1045 match e.value {
1046 Expr::Path(segs) => assert_eq!(segs, vec!["a", "b", "c", "d", "e"]),
1047 other => panic!("expected Path, got {other:?}"),
1048 }
1049 }
1050
1051 #[test]
1052 fn ternary_with_complex_condition() {
1053 let e = parse_ok("a && b ? c : d");
1056 match e.value {
1057 Expr::Ternary(cond, _, _) => {
1058 assert!(matches!(cond.value, Expr::BinOp(BinOp::And, _, _)));
1059 }
1060 other => panic!("expected Ternary, got {other:?}"),
1061 }
1062 }
1063
1064 #[test]
1065 fn ternary_in_ternary_branches() {
1066 let e = parse_ok("a ? (b ? c : d) : (e ? f : g)");
1068 match e.value {
1069 Expr::Ternary(_, then_e, else_e) => {
1070 assert!(matches!(then_e.value, Expr::Ternary(_, _, _)));
1071 assert!(matches!(else_e.value, Expr::Ternary(_, _, _)));
1072 }
1073 other => panic!("expected Ternary, got {other:?}"),
1074 }
1075 }
1076
1077 #[test]
1078 fn unary_not_stacks() {
1079 let e = parse_ok("!!a");
1082 match e.value {
1083 Expr::Not(inner) => assert!(matches!(inner.value, Expr::Not(_))),
1084 other => panic!("expected Not(Not), got {other:?}"),
1085 }
1086 }
1087
1088 #[test]
1089 fn comparison_with_string_literal() {
1090 let e = parse_ok("role == 'admin' || role == \"editor\"");
1091 assert!(matches!(e.value, Expr::BinOp(BinOp::Or, _, _)));
1092 }
1093
1094 #[test]
1097 fn plus_chains_left_to_right() {
1098 let e = parse_ok("a + b + c");
1099 match e.value {
1101 Expr::BinOp(BinOp::Plus, lhs, rhs) => {
1102 assert!(matches!(lhs.value, Expr::BinOp(BinOp::Plus, _, _)));
1103 assert!(matches!(rhs.value, Expr::Path(_)));
1104 }
1105 other => panic!("expected BinOp Plus, got {other:?}"),
1106 }
1107 }
1108
1109 #[test]
1110 fn plus_binds_tighter_than_comparison() {
1111 let e = parse_ok("a + b == 'foo-bar'");
1112 match e.value {
1113 Expr::BinOp(BinOp::Eq, lhs, _) => {
1114 assert!(matches!(lhs.value, Expr::BinOp(BinOp::Plus, _, _)));
1115 }
1116 other => panic!("expected equality, got {other:?}"),
1117 }
1118 }
1119
1120 #[test]
1121 fn plus_binds_looser_than_unary_not() {
1122 let e = parse_ok("!a + !b");
1125 match e.value {
1126 Expr::BinOp(BinOp::Plus, lhs, rhs) => {
1127 assert!(matches!(lhs.value, Expr::Not(_)));
1128 assert!(matches!(rhs.value, Expr::Not(_)));
1129 }
1130 other => panic!("expected plus at root, got {other:?}"),
1131 }
1132 }
1133
1134 #[test]
1135 fn plus_with_string_literal_parses() {
1136 let e = parse_ok("$id + '-title'");
1137 assert!(matches!(e.value, Expr::BinOp(BinOp::Plus, _, _)));
1138 }
1139
1140 fn assert_err_says(src: &str, needle: &str) {
1147 let err = parse(src).expect_err(&format!("expected parse error for {src:?}"));
1148 assert!(
1149 err.message.contains(needle) || err.hint.as_deref().unwrap_or("").contains(needle),
1150 "error for {src:?} should mention {needle:?}; got message={:?} hint={:?}",
1151 err.message,
1152 err.hint,
1153 );
1154 }
1155
1156 #[test]
1157 fn reject_arithmetic_star() {
1158 assert_err_says("progress * 100", "arithmetic operator");
1159 assert_err_says("progress * 100", "#[computed]");
1160 }
1161
1162 #[test]
1163 fn reject_arithmetic_slash() {
1164 assert_err_says("total / count", "arithmetic operator");
1165 }
1166
1167 #[test]
1168 fn reject_arithmetic_percent() {
1169 assert_err_says("idx % 2", "arithmetic operator");
1170 }
1171
1172 #[test]
1173 fn reject_arithmetic_minus() {
1174 assert_err_says("count - 1", "subtraction");
1175 assert_err_says("count - 1", "#[computed]");
1176 }
1177
1178 #[test]
1179 fn reject_triple_equals() {
1180 assert_err_says("status === 'queued'", "`===`");
1181 assert_err_says("status === 'queued'", "use `==`");
1182 }
1183
1184 #[test]
1185 fn reject_strict_inequality() {
1186 assert_err_says("status !== 'queued'", "`!==`");
1187 assert_err_says("status !== 'queued'", "use `!=`");
1188 }
1189
1190 #[test]
1191 fn reject_arrow_function() {
1192 assert_err_says("x => x", "arrow functions");
1193 assert_err_says("x => x", "handler method");
1194 }
1195
1196 #[test]
1197 fn reject_nullish_coalescing() {
1198 assert_err_says("a ?? b", "nullish coalescing");
1199 }
1200
1201 #[test]
1202 fn reject_optional_chaining() {
1203 assert_err_says("user?.name", "optional chaining");
1204 }
1205
1206 #[test]
1207 fn reject_spread() {
1208 assert_err_says("...rest", "spread");
1209 }
1210
1211 #[test]
1212 fn reject_method_call_on_path() {
1213 assert_err_says("files.filter(f)", "method calls on objects");
1216 assert_err_says("files.filter(f)", "#[computed]");
1217 }
1218
1219 #[test]
1220 fn negative_number_literal_still_parses() {
1221 let e = parse_ok("-42");
1223 assert!(matches!(
1224 e.value,
1225 Expr::Literal(Literal::Number(n)) if n == -42.0
1226 ));
1227 }
1228
1229 #[test]
1230 fn plain_identifier_call_still_parses() {
1231 let e = parse_ok("filter_done(files)");
1234 assert!(matches!(e.value, Expr::Call(_, _)));
1235 }
1236}