Skip to main content

alba_syntax/
expr.rs

1//! [`Expr`], [`BinOp`], and the precedence-climbing expression parser.
2//!
3//! The parser methods here are inherent methods on
4//! [`crate::parser::Parser`] (an `impl` block for that type, physically
5//! located in this file rather than `parser.rs`): they share its token
6//! stream and cursor rather than duplicating that machinery. Only
7//! [`Parser::parse_expr`] is called from outside this module (by
8//! `parser.rs`'s `let` bindings and by `template.rs`'s interpolation
9//! bodies); the rest of the chain is private to keep the precedence levels
10//! an implementation detail.
11//!
12//! Precedence, low to high: `||`, `&&`, `==`/`!=`, `+`, atoms. `if cond
13//! then a else b` is parsed as an atom whose three parts each recurse back
14//! into the full expression grammar, so e.g. `else "no" + suffix` parses
15//! the whole `+` expression as the `else` branch.
16
17use crate::ast::Spanned;
18use crate::parser::{ParseError, Parser};
19use crate::template::{self, StringTemplate};
20use crate::token::TokenKind;
21
22/// An expression, as used in `let` bindings and interpolated `{expr}`
23/// segments of a [`StringTemplate`].
24#[derive(Debug, Clone, PartialEq)]
25pub enum Expr {
26    Str(StringTemplate),
27    Bool(bool),
28    Var(Spanned<String>),
29    Call {
30        name: Spanned<String>,
31        args: Vec<Expr>,
32    },
33    Binary {
34        op: BinOp,
35        lhs: Box<Expr>,
36        rhs: Box<Expr>,
37    },
38    If {
39        cond: Box<Expr>,
40        then: Box<Expr>,
41        otherwise: Box<Expr>,
42    },
43    /// `object.field`, member access. Only the `git` object has fields;
44    /// which ones is `alba-core`'s concern, not the grammar's.
45    Field {
46        object: Spanned<String>,
47        field: Spanned<String>,
48    },
49}
50
51/// A binary operator usable in an [`Expr::Binary`].
52#[derive(Debug, Clone, Copy, PartialEq, Eq)]
53pub enum BinOp {
54    Eq,
55    NotEq,
56    And,
57    Or,
58    /// `+`, string concatenation.
59    Concat,
60}
61
62impl<'a> Parser<'a> {
63    /// Parses a full expression: the entry point for `let` bindings,
64    /// function-call arguments, and interpolation bodies.
65    pub(crate) fn parse_expr(&mut self) -> Result<Expr, ParseError> {
66        self.parse_or()
67    }
68
69    /// `||`, left-associative, lowest precedence.
70    fn parse_or(&mut self) -> Result<Expr, ParseError> {
71        let mut lhs = self.parse_and()?;
72        while self.check(&TokenKind::OrOr) {
73            self.advance();
74            let rhs = self.parse_and()?;
75            lhs = Expr::Binary {
76                op: BinOp::Or,
77                lhs: Box::new(lhs),
78                rhs: Box::new(rhs),
79            };
80        }
81        Ok(lhs)
82    }
83
84    /// `&&`, left-associative, binds tighter than `||`.
85    fn parse_and(&mut self) -> Result<Expr, ParseError> {
86        let mut lhs = self.parse_equality()?;
87        while self.check(&TokenKind::AndAnd) {
88            self.advance();
89            let rhs = self.parse_equality()?;
90            lhs = Expr::Binary {
91                op: BinOp::And,
92                lhs: Box::new(lhs),
93                rhs: Box::new(rhs),
94            };
95        }
96        Ok(lhs)
97    }
98
99    /// `==`/`!=`, left-associative, binds tighter than `&&`.
100    fn parse_equality(&mut self) -> Result<Expr, ParseError> {
101        let mut lhs = self.parse_additive()?;
102        loop {
103            let op = if self.check(&TokenKind::EqEq) {
104                BinOp::Eq
105            } else if self.check(&TokenKind::NotEq) {
106                BinOp::NotEq
107            } else {
108                break;
109            };
110            self.advance();
111            let rhs = self.parse_additive()?;
112            lhs = Expr::Binary {
113                op,
114                lhs: Box::new(lhs),
115                rhs: Box::new(rhs),
116            };
117        }
118        Ok(lhs)
119    }
120
121    /// `+` (string concatenation), left-associative, binds tighter than
122    /// `==`/`!=`.
123    fn parse_additive(&mut self) -> Result<Expr, ParseError> {
124        let mut lhs = self.parse_atom()?;
125        while self.check(&TokenKind::Plus) {
126            self.advance();
127            let rhs = self.parse_atom()?;
128            lhs = Expr::Binary {
129                op: BinOp::Concat,
130                lhs: Box::new(lhs),
131                rhs: Box::new(rhs),
132            };
133        }
134        Ok(lhs)
135    }
136
137    /// A single atom: a string, boolean, variable, call, or `if/then/else`
138    /// expression. The highest-precedence level.
139    fn parse_atom(&mut self) -> Result<Expr, ParseError> {
140        match self.peek().kind.clone() {
141            TokenKind::KwIf => self.parse_if(),
142            TokenKind::Str(_) => {
143                let tok = self.advance();
144                let template = template::parse_template_at(self.source(), tok.span)?;
145                Ok(Expr::Str(template))
146            }
147            TokenKind::KwTrue => {
148                self.advance();
149                Ok(Expr::Bool(true))
150            }
151            TokenKind::KwFalse => {
152                self.advance();
153                Ok(Expr::Bool(false))
154            }
155            TokenKind::Ident(name) => {
156                let name_span = self.advance().span;
157                if self.check(&TokenKind::Dot) {
158                    self.advance();
159                    let field = self.eat_ident()?;
160                    return Ok(Expr::Field {
161                        object: Spanned::new(name, name_span),
162                        field,
163                    });
164                }
165                if self.check(&TokenKind::LParen) {
166                    self.advance();
167                    let mut args = Vec::new();
168                    if !self.check(&TokenKind::RParen) {
169                        loop {
170                            args.push(self.parse_expr()?);
171                            if self.check(&TokenKind::Comma) {
172                                self.advance();
173                            } else {
174                                break;
175                            }
176                        }
177                    }
178                    self.expect(TokenKind::RParen, "`)`")?;
179                    Ok(Expr::Call {
180                        name: Spanned::new(name, name_span),
181                        args,
182                    })
183                } else {
184                    Ok(Expr::Var(Spanned::new(name, name_span)))
185                }
186            }
187            _ => Err(self.unexpected("an expression")),
188        }
189    }
190
191    /// `if cond then a else b`. Each part recurses into the full
192    /// expression grammar.
193    fn parse_if(&mut self) -> Result<Expr, ParseError> {
194        self.expect(TokenKind::KwIf, "`if`")?;
195        let cond = self.parse_expr()?;
196        self.expect(TokenKind::KwThen, "`then`")?;
197        let then = self.parse_expr()?;
198        self.expect(TokenKind::KwElse, "`else`")?;
199        let otherwise = self.parse_expr()?;
200        Ok(Expr::If {
201            cond: Box::new(cond),
202            then: Box::new(then),
203            otherwise: Box::new(otherwise),
204        })
205    }
206}
207
208#[cfg(test)]
209mod tests {
210    use super::*;
211    use crate::parser::tokenize;
212
213    /// Parses `source` as a single standalone expression (no surrounding
214    /// beam/let syntax), asserting it consumes the whole input.
215    fn parse_expr_str(source: &str) -> Expr {
216        let tokens = tokenize(source).expect("tokenize failed");
217        let mut parser = Parser::new(tokens, source);
218        let expr = parser.parse_expr().expect("parse failed");
219        assert!(parser.is_eof(), "trailing tokens after expression");
220        expr
221    }
222
223    #[test]
224    fn parses_if_then_else_and_precedence() {
225        // && binds tighter than ||, == tighter than &&, + tighter than ==
226        let e = parse_expr_str(r#"if a == "x" && b then "yes" else "no" + suffix"#);
227        insta::assert_debug_snapshot!(e);
228    }
229}