Skip to main content

jia_parse/jia_lang/parser/
mod.rs

1//! Parser for the CP scheduling modelling language.
2//!
3//! Top-level grammar:
4//! ```text
5//! model       = "model" IDENT sections* objective?
6//! sections    = variables_block | domains_block | constraints_block
7//! ```
8//!
9//! Public entry point: [`parse_model`]
10
11pub mod constraint;
12pub mod decl;
13pub mod domain;
14pub mod expr;
15
16use crate::error::{ParseError, Span};
17use crate::jia_lang::ast::{Expr, JiaModel, ModelType, Objective, OptDirection};
18use crate::jia_lang::lexer::{Token, TokenKind};
19
20/// Parser state wrapping a token slice with a cursor.
21pub struct Parser<'a> {
22    tokens: &'a [Token],
23    pos: usize,
24}
25
26impl<'a> Parser<'a> {
27    /// Create a new parser over the given token slice.
28    pub fn new(tokens: &'a [Token]) -> Self {
29        Self { tokens, pos: 0 }
30    }
31
32    /// Peek at the current token's kind without consuming it.
33    pub fn peek_kind(&self) -> Option<TokenKind> {
34        self.tokens.get(self.pos).map(|t| t.kind.clone())
35    }
36
37    /// Advance the cursor by one token.
38    pub fn advance(&mut self) {
39        self.pos += 1;
40    }
41
42    /// Get the span of the current token (or a default EOF span).
43    pub fn current_span(&self) -> Span {
44        self.tokens
45            .get(self.pos)
46            .map(|t| t.span)
47            .unwrap_or(Span::new(0, 1, 1))
48    }
49
50    /// Return true if there are no more tokens.
51    pub fn is_eof(&self) -> bool {
52        self.pos >= self.tokens.len()
53    }
54
55    /// Expect and consume a specific token kind.
56    pub fn expect_token(&mut self, expected: TokenKind) -> Result<(), ParseError> {
57        let span = self.current_span();
58        match self.peek_kind() {
59            Some(ref kind) if kind == &expected => {
60                self.advance();
61                Ok(())
62            }
63            Some(ref kind) => Err(ParseError::new(
64                format!("expected {expected:?}, got {kind:?}"),
65                span,
66            )),
67            None => Err(ParseError::new(
68                format!("expected {expected:?}, got end of input"),
69                span,
70            )),
71        }
72    }
73
74    /// Expect and consume an identifier, returning its name.
75    pub fn expect_ident(&mut self) -> Result<String, ParseError> {
76        let span = self.current_span();
77        match self.peek_kind() {
78            Some(TokenKind::Ident(name)) => {
79                self.advance();
80                Ok(name)
81            }
82            Some(kind) => Err(ParseError::new(
83                format!("expected identifier, got {kind:?}"),
84                span,
85            )),
86            None => Err(ParseError::new(
87                "expected identifier, got end of input",
88                span,
89            )),
90        }
91    }
92
93    /// Expect and consume a specific identifier.
94    pub fn expect_ident_matching(&mut self, expected: &str) -> Result<(), ParseError> {
95        let span = self.current_span();
96        let name = self.expect_ident()?;
97        if name != expected {
98            return Err(ParseError::new(
99                format!("expected '{expected}', got '{name}'"),
100                span,
101            ));
102        }
103        Ok(())
104    }
105
106    /// Expect and consume a number literal.
107    pub fn expect_number(&mut self) -> Result<i64, ParseError> {
108        let span = self.current_span();
109        match self.peek_kind() {
110            Some(TokenKind::Number(n)) => {
111                self.advance();
112                Ok(n)
113            }
114            Some(kind) => Err(ParseError::new(
115                format!("expected number, got {kind:?}"),
116                span,
117            )),
118            None => Err(ParseError::new("expected number, got end of input", span)),
119        }
120    }
121}
122
123/// Parse a Jia model from a token slice.
124pub fn parse_model(tokens: &[Token]) -> Result<JiaModel, ParseError> {
125    let mut parser = Parser::new(tokens);
126
127    // Optional @model tag: @model lp | @model cp
128    let model_type = if matches!(parser.peek_kind(), Some(TokenKind::At)) {
129        parser.advance(); // consume @
130        parser.expect_ident_matching("model")?;
131        let span = parser.current_span();
132        let type_name = parser.expect_ident()?;
133        match type_name.as_str() {
134            "lp" => Some(ModelType::Lp),
135            "cp" => Some(ModelType::Cp),
136            other => {
137                return Err(ParseError::new(
138                    format!("unknown model type '{other}', expected 'lp' or 'cp'"),
139                    span,
140                ));
141            }
142        }
143    } else {
144        None
145    };
146
147    // model <name>
148    parser.expect_ident_matching("model")?;
149    let name = parser.expect_ident()?;
150
151    let mut variables = Vec::new();
152    let mut domains = Vec::new();
153    let mut constraints = Vec::new();
154    let mut objective = None;
155
156    // Parse sections in any order
157    while !parser.is_eof() {
158        match parser.peek_kind() {
159            Some(TokenKind::Ident(kw)) => match kw.as_str() {
160                "variables" => {
161                    variables = parser.parse_variables_block()?;
162                }
163                "domains" => {
164                    domains = parser.parse_domains_block()?;
165                }
166                "constraints" => {
167                    constraints = parser.parse_constraints_block()?;
168                }
169                "minimize" => {
170                    parser.advance();
171                    let expr = parser.parse_expr()?;
172                    objective = Some(Objective {
173                        direction: OptDirection::Minimize,
174                        expr,
175                    });
176                }
177                "maximize" => {
178                    parser.advance();
179                    let expr = parser.parse_expr()?;
180                    objective = Some(Objective {
181                        direction: OptDirection::Maximize,
182                        expr,
183                    });
184                }
185                other => {
186                    return Err(ParseError::new(
187                        format!("unexpected keyword '{other}', expected section or objective"),
188                        parser.current_span(),
189                    ));
190                }
191            },
192            Some(kind) => {
193                return Err(ParseError::new(
194                    format!("unexpected token {kind:?}"),
195                    parser.current_span(),
196                ));
197            }
198            None => break,
199        }
200    }
201
202    Ok(JiaModel {
203        model_type,
204        name,
205        variables,
206        domains,
207        constraints,
208        objective,
209    })
210}
211
212/// Parse a CP model from a source string.
213///
214/// This is the main public entry point. It tokenizes the input and then parses
215/// the token stream into a [`JiaModel`].
216pub fn parse_model_str(input: &str) -> Result<JiaModel, ParseError> {
217    let tokens = crate::jia_lang::lexer::tokenize(input)?;
218    parse_model(&tokens)
219}
220
221/// Parse an expression from a source string (for testing).
222pub fn parse_expr_str(input: &str) -> Result<Expr, ParseError> {
223    let tokens = crate::jia_lang::lexer::tokenize(input)?;
224    let mut parser = Parser::new(&tokens);
225    parser.parse_expr()
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::jia_lang::ast::*;
232
233    #[test]
234    fn test_minimal_model() {
235        let model = parse_model_str("model test").unwrap();
236        assert_eq!(model.name, "test");
237        assert!(model.variables.is_empty());
238        assert!(model.domains.is_empty());
239        assert!(model.constraints.is_empty());
240        assert!(model.objective.is_none());
241    }
242
243    #[test]
244    fn test_model_with_objective() {
245        let model = parse_model_str(
246            "model test\nvariables { Integer: x }\ndomains { x in 0..10 }\nminimize x",
247        )
248        .unwrap();
249        assert_eq!(model.name, "test");
250        assert!(model.objective.is_some());
251        let obj = model.objective.unwrap();
252        assert_eq!(obj.direction, OptDirection::Minimize);
253        assert_eq!(obj.expr, Expr::Var("x".to_string()));
254    }
255
256    #[test]
257    fn test_job_shop_model() {
258        let input = r#"
259model job_shop
260
261variables {
262  Interval: j1_op1, j1_op2
263  Integer: makespan
264  Set[Interval]: machine1
265}
266
267domains {
268  duration(j1_op1) = 3
269  duration(j1_op2) = 2
270  makespan in 0..100
271  machine1 = {j1_op1, j1_op2}
272}
273
274constraints {
275  end_of(j1_op1) <= start_of(j1_op2)
276  no_overlap(machine1)
277  end_of(j1_op2) <= makespan
278}
279
280minimize makespan
281"#;
282        let model = parse_model_str(input).unwrap();
283        assert_eq!(model.name, "job_shop");
284        assert_eq!(model.variables.len(), 3);
285        assert_eq!(model.constraints.len(), 3);
286        assert!(model.objective.is_some());
287    }
288
289    #[test]
290    fn test_unknown_section() {
291        let err = parse_model_str("model test\nfoobar { }").unwrap_err();
292        assert!(err.message.contains("unexpected keyword"));
293    }
294
295    #[test]
296    fn test_model_tag_lp() {
297        let model = parse_model_str("@model lp\nmodel test").unwrap();
298        assert_eq!(model.model_type, Some(ModelType::Lp));
299        assert_eq!(model.name, "test");
300    }
301
302    #[test]
303    fn test_model_tag_cp() {
304        let model = parse_model_str("@model cp\nmodel test").unwrap();
305        assert_eq!(model.model_type, Some(ModelType::Cp));
306    }
307
308    #[test]
309    fn test_model_no_tag() {
310        let model = parse_model_str("model test").unwrap();
311        assert_eq!(model.model_type, None);
312    }
313
314    #[test]
315    fn test_empty_token_stream_errors_cleanly() {
316        let tokens = Vec::new();
317        assert!(parse_model(&tokens).is_err());
318    }
319
320    #[test]
321    fn test_model_tag_unknown() {
322        let err = parse_model_str("@model foo\nmodel test").unwrap_err();
323        assert!(err.message.contains("unknown model type"));
324    }
325
326    #[test]
327    fn test_real_variables() {
328        let model = parse_model_str("@model lp\nmodel test\nvariables { Real: x, y }").unwrap();
329        assert_eq!(model.variables.len(), 1);
330        assert_eq!(model.variables[0].var_type, VarType::Real);
331        assert_eq!(model.variables[0].names, vec!["x", "y"]);
332    }
333
334    #[test]
335    fn test_float_in_expression() {
336        let expr = parse_expr_str("3.25 + x").unwrap();
337        assert_eq!(
338            expr,
339            Expr::BinaryOp {
340                op: ArithOp::Add,
341                left: Box::new(Expr::Float(3.25)),
342                right: Box::new(Expr::Var("x".to_string())),
343            }
344        );
345    }
346
347    #[test]
348    fn test_division_in_expression() {
349        let expr = parse_expr_str("x / 2").unwrap();
350        assert!(matches!(
351            expr,
352            Expr::BinaryOp {
353                op: ArithOp::Div,
354                ..
355            }
356        ));
357    }
358
359    #[test]
360    fn test_real_domain_with_inf() {
361        let model = parse_model_str(
362            "@model lp\nmodel test\nvariables { Real: x }\ndomains { x in 0..inf }",
363        )
364        .unwrap();
365        assert_eq!(model.domains.len(), 1);
366    }
367
368    #[test]
369    fn test_full_lp_model() {
370        let input = r#"
371@model lp
372model simple
373
374variables {
375  Real: x, y
376}
377
378domains {
379  x in 0..inf
380  y in 0..inf
381}
382
383constraints {
384  x + y >= 10
385  x <= 8
386}
387
388minimize 2 * x + 3 * y
389"#;
390        let model = parse_model_str(input).unwrap();
391        assert_eq!(model.model_type, Some(ModelType::Lp));
392        assert_eq!(model.name, "simple");
393        assert_eq!(model.variables.len(), 1);
394        assert_eq!(model.variables[0].var_type, VarType::Real);
395        assert_eq!(model.constraints.len(), 2);
396        assert!(model.objective.is_some());
397    }
398
399    #[test]
400    fn test_equality_with_single_eq() {
401        // In constraints, `=` should work as equality (alongside `==`)
402        let model = parse_model_str(
403            "model test\nvariables { Integer: x }\ndomains { x in 0..10 }\nconstraints { x = 5 }",
404        )
405        .unwrap();
406        assert_eq!(model.constraints.len(), 1);
407        assert!(matches!(
408            &model.constraints[0],
409            Constraint::Comparison { op, .. } if *op == CmpOp::Eq
410        ));
411    }
412
413    #[test]
414    fn parser_expect_helpers_report_errors() {
415        let tokens = crate::jia_lang::lexer::tokenize("name").unwrap();
416        let mut parser = Parser::new(&tokens);
417        assert!(parser.expect_token(TokenKind::LBrace).is_err());
418
419        let tokens = Vec::new();
420        let mut parser = Parser::new(&tokens);
421        assert!(parser.expect_token(TokenKind::LBrace).is_err());
422        assert!(parser.expect_ident().is_err());
423        assert!(parser.expect_number().is_err());
424
425        let tokens = crate::jia_lang::lexer::tokenize("123").unwrap();
426        let mut parser = Parser::new(&tokens);
427        assert!(parser.expect_ident().is_err());
428
429        let tokens = crate::jia_lang::lexer::tokenize("actual").unwrap();
430        let mut parser = Parser::new(&tokens);
431        assert!(parser.expect_ident_matching("expected").is_err());
432
433        let tokens = crate::jia_lang::lexer::tokenize("name").unwrap();
434        let mut parser = Parser::new(&tokens);
435        assert!(parser.expect_number().is_err());
436    }
437
438    #[test]
439    fn parse_model_reports_unexpected_non_identifier_token() {
440        assert!(parse_model_str("model test\n{").is_err());
441    }
442}