Skip to main content

jia_parse/jia_lang/parser/
constraint.rs

1//! Constraints block parser for the CP language.
2//!
3//! Parses constraints like:
4//! ```text
5//! constraints {
6//!   no_overlap(machine1)
7//!   cumulative(res1, 4)
8//!   span(project, phases)
9//!   alternative(j1_o1, j1_o1_alts)
10//!   end_of(a) <= start_of(b)
11//! }
12//! ```
13
14use super::Parser;
15use crate::error::ParseError;
16use crate::jia_lang::ast::{CmpOp, Constraint};
17use crate::jia_lang::lexer::TokenKind;
18
19impl<'a> Parser<'a> {
20    /// Parse a `constraints { ... }` block.
21    pub fn parse_constraints_block(&mut self) -> Result<Vec<Constraint>, ParseError> {
22        self.expect_ident_matching("constraints")?;
23        self.expect_token(TokenKind::LBrace)?;
24
25        let mut constraints = Vec::new();
26        while self.peek_kind() != Some(TokenKind::RBrace) {
27            constraints.push(self.parse_constraint_stmt()?);
28        }
29        self.expect_token(TokenKind::RBrace)?;
30        Ok(constraints)
31    }
32
33    /// Parse a single constraint statement.
34    fn parse_constraint_stmt(&mut self) -> Result<Constraint, ParseError> {
35        // Dispatch on first ident
36        if let Some(TokenKind::Ident(name)) = self.peek_kind() {
37            match name.as_str() {
38                "no_overlap" => return self.parse_no_overlap(),
39                "cumulative" => return self.parse_cumulative(),
40                "span" => return self.parse_span(),
41                "alternative" => return self.parse_alternative(),
42                _ => {}
43            }
44        }
45
46        // Otherwise it's a comparison: expr op expr
47        self.parse_comparison()
48    }
49
50    /// Parse `no_overlap(name, ...)`
51    fn parse_no_overlap(&mut self) -> Result<Constraint, ParseError> {
52        self.expect_ident_matching("no_overlap")?;
53        self.expect_token(TokenKind::LParen)?;
54        let intervals = self.parse_ident_list()?;
55        self.expect_token(TokenKind::RParen)?;
56        Ok(Constraint::NoOverlap { intervals })
57    }
58
59    /// Parse `cumulative(set, capacity_expr)`
60    fn parse_cumulative(&mut self) -> Result<Constraint, ParseError> {
61        self.expect_ident_matching("cumulative")?;
62        self.expect_token(TokenKind::LParen)?;
63        let set = self.expect_ident()?;
64        self.expect_token(TokenKind::Comma)?;
65        let capacity = self.parse_expr()?;
66        self.expect_token(TokenKind::RParen)?;
67        Ok(Constraint::Cumulative { set, capacity })
68    }
69
70    /// Parse `span(parent, set)`
71    fn parse_span(&mut self) -> Result<Constraint, ParseError> {
72        self.expect_ident_matching("span")?;
73        self.expect_token(TokenKind::LParen)?;
74        let parent = self.expect_ident()?;
75        self.expect_token(TokenKind::Comma)?;
76        let set = self.expect_ident()?;
77        self.expect_token(TokenKind::RParen)?;
78        Ok(Constraint::Span { parent, set })
79    }
80
81    /// Parse `alternative(parent, set)`
82    fn parse_alternative(&mut self) -> Result<Constraint, ParseError> {
83        self.expect_ident_matching("alternative")?;
84        self.expect_token(TokenKind::LParen)?;
85        let parent = self.expect_ident()?;
86        self.expect_token(TokenKind::Comma)?;
87        let set = self.expect_ident()?;
88        self.expect_token(TokenKind::RParen)?;
89        Ok(Constraint::Alternative { parent, set })
90    }
91
92    /// Parse a comparison constraint: `expr op expr`
93    fn parse_comparison(&mut self) -> Result<Constraint, ParseError> {
94        let left = self.parse_expr()?;
95        let op = self.parse_cmp_op()?;
96        let right = self.parse_expr()?;
97        Ok(Constraint::Comparison { left, op, right })
98    }
99
100    /// Parse a comparison operator.
101    fn parse_cmp_op(&mut self) -> Result<CmpOp, ParseError> {
102        let span = self.current_span();
103        match self.peek_kind() {
104            Some(TokenKind::Le) => {
105                self.advance();
106                Ok(CmpOp::Le)
107            }
108            Some(TokenKind::Ge) => {
109                self.advance();
110                Ok(CmpOp::Ge)
111            }
112            Some(TokenKind::Lt) => {
113                self.advance();
114                Ok(CmpOp::Lt)
115            }
116            Some(TokenKind::Gt) => {
117                self.advance();
118                Ok(CmpOp::Gt)
119            }
120            Some(TokenKind::EqEq) | Some(TokenKind::Eq) => {
121                self.advance();
122                Ok(CmpOp::Eq)
123            }
124            Some(TokenKind::Ne) => {
125                self.advance();
126                Ok(CmpOp::Ne)
127            }
128            _ => Err(ParseError::new(
129                "expected comparison operator (<=, >=, <, >, ==, !=, =)",
130                span,
131            )),
132        }
133    }
134}
135
136#[cfg(test)]
137mod tests {
138    use crate::jia_lang::ast::{CmpOp, Constraint, Expr};
139    use crate::jia_lang::lexer::tokenize;
140    use crate::jia_lang::parser::Parser;
141
142    fn parse_constraints(input: &str) -> Vec<Constraint> {
143        let tokens = tokenize(input).unwrap();
144        let mut parser = Parser::new(&tokens);
145        parser.parse_constraints_block().unwrap()
146    }
147
148    #[test]
149    fn test_no_overlap() {
150        let cs = parse_constraints("constraints { no_overlap(machine1) }");
151        assert_eq!(
152            cs[0],
153            Constraint::NoOverlap {
154                intervals: vec!["machine1".to_string()]
155            }
156        );
157    }
158
159    #[test]
160    fn test_no_overlap_multiple() {
161        let cs = parse_constraints("constraints { no_overlap(a, b, c) }");
162        assert_eq!(
163            cs[0],
164            Constraint::NoOverlap {
165                intervals: vec!["a".to_string(), "b".to_string(), "c".to_string()]
166            }
167        );
168    }
169
170    #[test]
171    fn test_cumulative() {
172        let cs = parse_constraints("constraints { cumulative(res1, 4) }");
173        assert_eq!(
174            cs[0],
175            Constraint::Cumulative {
176                set: "res1".to_string(),
177                capacity: Expr::Number(4),
178            }
179        );
180    }
181
182    #[test]
183    fn test_span() {
184        let cs = parse_constraints("constraints { span(project, phases) }");
185        assert_eq!(
186            cs[0],
187            Constraint::Span {
188                parent: "project".to_string(),
189                set: "phases".to_string(),
190            }
191        );
192    }
193
194    #[test]
195    fn test_alternative() {
196        let cs = parse_constraints("constraints { alternative(j1_o1, j1_o1_alts) }");
197        assert_eq!(
198            cs[0],
199            Constraint::Alternative {
200                parent: "j1_o1".to_string(),
201                set: "j1_o1_alts".to_string(),
202            }
203        );
204    }
205
206    #[test]
207    fn test_comparison() {
208        let cs = parse_constraints("constraints { end_of(a) <= start_of(b) }");
209        assert_eq!(
210            cs[0],
211            Constraint::Comparison {
212                left: Expr::EndOf("a".to_string()),
213                op: CmpOp::Le,
214                right: Expr::StartOf("b".to_string()),
215            }
216        );
217    }
218
219    #[test]
220    fn test_comparison_with_arithmetic() {
221        let cs = parse_constraints("constraints { start_of(b) >= end_of(a) + gap }");
222        assert!(matches!(
223            &cs[0],
224            Constraint::Comparison { op, .. } if *op == CmpOp::Ge
225        ));
226    }
227
228    #[test]
229    fn test_lt_gt_ne_and_missing_comparator_errors() {
230        for (input, expected) in [
231            ("constraints { x < 1 }", CmpOp::Lt),
232            ("constraints { x > 1 }", CmpOp::Gt),
233            ("constraints { x != 1 }", CmpOp::Ne),
234        ] {
235            let cs = parse_constraints(input);
236            assert!(matches!(
237                &cs[0],
238                Constraint::Comparison { op, .. } if *op == expected
239            ));
240        }
241
242        let tokens = tokenize("constraints { x + 1 }").unwrap();
243        let mut parser = Parser::new(&tokens);
244        assert!(parser.parse_constraints_block().is_err());
245    }
246}