1
use std::collections::BTreeMap;
2
use std::sync::{Arc, RwLock};
3
use std::{fs, vec};
4

            
5
use conjure_cp_core::Model;
6
use conjure_cp_core::ast::DeclarationPtr;
7
use conjure_cp_core::ast::assertions::debug_assert_model_well_formed;
8
use conjure_cp_core::context::Context;
9
#[allow(unused)]
10
use uniplate::Uniplate;
11

            
12
use super::ParseContext;
13
use super::dominance::parse_dominance_relation;
14
use super::find::{parse_find_statement, parse_given_statement};
15
use super::letting::parse_letting_statement;
16
use super::objective::parse_objective_statement;
17
use super::util::{TypecheckingContext, get_tree};
18
use crate::diagnostics::source_map::SourceMap;
19
use crate::errors::{FatalParseError, ParseErrorCollection, RecoverableParseError};
20
use crate::expression::parse_expression;
21
use crate::syntax_errors::detect_syntactic_errors;
22
use tree_sitter::Tree;
23

            
24
/// Parse an Essence file into a Model using the tree-sitter parser.
25
1138672
pub fn parse_essence_file_native(
26
1138672
    path: &str,
27
1138672
    context: Arc<RwLock<Context<'static>>>,
28
1138672
) -> Result<Model, Box<ParseErrorCollection>> {
29
1138672
    let source_code = fs::read_to_string(path).map_err(|source| {
30
4
        Box::new(ParseErrorCollection::fatal(FatalParseError::FileRead {
31
4
            path: path.to_string(),
32
4
            source,
33
4
        }))
34
4
    })?;
35

            
36
1138668
    let mut errors = vec![];
37
1138668
    let model = parse_essence_with_context(&source_code, context, &mut errors);
38

            
39
1138668
    match model {
40
        Ok(Some(m)) => {
41
1137128
            debug_assert_model_well_formed(&m, "tree-sitter");
42
1137128
            Ok(m)
43
        }
44
        Ok(None) => {
45
            // Recoverable errors were found, return them as a ParseErrorCollection
46
1540
            Err(Box::new(ParseErrorCollection::multiple(
47
1540
                errors,
48
1540
                Some(source_code),
49
1540
                Some(path.to_string()),
50
1540
            )))
51
        }
52
        Err(fatal) => {
53
            // Fatal error - wrap in ParseErrorCollection::Fatal
54
            Err(Box::new(ParseErrorCollection::fatal(fatal)))
55
        }
56
    }
57
1138672
}
58

            
59
1138668
pub fn parse_essence_with_context(
60
1138668
    src: &str,
61
1138668
    context: Arc<RwLock<Context<'static>>>,
62
1138668
    errors: &mut Vec<RecoverableParseError>,
63
1138668
) -> Result<Option<Model>, FatalParseError> {
64
1138668
    match parse_essence_with_context_and_map(src, context, errors, None)? {
65
1137128
        (Some(model), _source_map) => Ok(Some(model)),
66
1540
        (None, _source_map) => Ok(None),
67
    }
68
1138668
}
69

            
70
/*
71
    this function is used by both the file-based parser and the LSP parser (which needs the source map)
72
    the LSP parser can also optionally pass in a pre-parsed tree to avoid parsing twice (which is how caching is implemented)
73
    if the tree is not passed in, we will parse it from scratch (this is what the file-based parser does)
74
    when cache is dirty, LSP has to call parse_essence_with_context_and_map with None for the tree,
75
    which will cause it to re-parse the source code and update the cache (Model = ast, SorceMap = map)
76
*/
77
1139958
pub fn parse_essence_with_context_and_map(
78
1139958
    src: &str,
79
1139958
    context: Arc<RwLock<Context<'static>>>,
80
1139958
    errors: &mut Vec<RecoverableParseError>,
81
1139958
    tree: Option<&Tree>,
82
1139958
) -> Result<(Option<Model>, SourceMap), FatalParseError> {
83
1139958
    let (tree, source_code) = if let Some(tree) = tree {
84
888
        (tree.clone(), src.to_string())
85
    } else {
86
1139070
        match get_tree(src) {
87
1139070
            Some(tree) => tree,
88
            None => {
89
                return Err(FatalParseError::TreeSitterError(
90
                    "Failed to parse source code".to_string(),
91
                ));
92
            }
93
        }
94
    };
95

            
96
1139958
    let has_syntax_errors = tree.root_node().has_error();
97
1139958
    if has_syntax_errors {
98
1304
        detect_syntactic_errors(src, &tree, errors);
99
1138654
    }
100

            
101
    // don't detect semantic errors if there are syntactic errors, but still parse for source map.
102
1139958
    let mut suppressed_semantic_errors = Vec::new();
103
1139958
    let semantic_errors: &mut Vec<RecoverableParseError> = if has_syntax_errors {
104
1304
        &mut suppressed_semantic_errors
105
    } else {
106
1138654
        errors
107
    };
108

            
109
1139958
    let mut model = Model::new(context);
110
1139958
    let mut source_map = SourceMap::default();
111
1139958
    let mut declaration_spans = BTreeMap::new();
112
1139958
    let root_node = tree.root_node();
113

            
114
    // Create a ParseContext
115
1139958
    let mut ctx = ParseContext::new(
116
1139958
        &source_code,
117
1139958
        &root_node,
118
1139958
        Some(model.symbols_ptr_unchecked().clone()),
119
1139958
        semantic_errors,
120
1139958
        &mut source_map,
121
1139958
        &mut declaration_spans,
122
1139958
    );
123

            
124
1139958
    let mut cursor = root_node.walk();
125
1139958
    for statement in root_node.children(&mut cursor) {
126
7802212
        if !statement.is_named() || statement.is_error() || statement.kind() == "ERROR" {
127
1989600
            continue;
128
5812612
        }
129

            
130
5812612
        ctx.typechecking_context = TypecheckingContext::Unknown;
131
5812612
        ctx.inner_typechecking_context = TypecheckingContext::Unknown;
132

            
133
5812612
        match statement.kind() {
134
726132
            "single_line_comment" => {}
135
881094
            "language_declaration" => {}
136
            "find_statement" => {
137
1617870
                let parsed = parse_find_statement(&mut ctx, statement)?;
138
1617870
                for (name, domain) in parsed.declarations {
139
2263683
                    let decl = if parsed.auxiliary {
140
36
                        DeclarationPtr::new_find_auxiliary(name, domain)
141
                    } else {
142
2263647
                        DeclarationPtr::new_find(name, domain)
143
                    };
144
2263683
                    model.symbols_mut().insert(decl);
145
                }
146
            }
147
            "given_statement" => {
148
187566
                let var_hashmap = parse_given_statement(&mut ctx, statement)?;
149
187566
                for (name, domain) in var_hashmap {
150
201104
                    model
151
201104
                        .symbols_mut()
152
201104
                        .insert(DeclarationPtr::new_given(name, domain));
153
                }
154
            }
155
            // `arithmetic_expr` is not a valid constraint, but the grammar accepts it here so
156
            // that the type checker can report "expected bool, got int" against the expression
157
            // itself rather than the parser rejecting the whole line as malformed.
158
            "bool_expr" | "atom" | "comparison_expr" | "arithmetic_expr" => {
159
1968904
                ctx.typechecking_context = TypecheckingContext::Boolean;
160
1968904
                let Some(expr) = parse_expression(&mut ctx, statement)? else {
161
1056
                    continue;
162
                };
163
1967848
                model.add_constraint(expr);
164
            }
165
            "where_statement" => {
166
192
                ctx.typechecking_context = TypecheckingContext::Boolean;
167
192
                let mut cursor = statement.walk();
168
192
                for condition in statement.named_children(&mut cursor) {
169
192
                    let Some(expr) = parse_expression(&mut ctx, condition)? else {
170
                        continue;
171
                    };
172
192
                    model.add_instantiation_condition(expr);
173
                }
174
            }
175
            "language_label" => {}
176
            "letting_statement" => {
177
398354
                let Some(letting_vars) = parse_letting_statement(&mut ctx, statement)? else {
178
                    continue;
179
                };
180
398354
                model.symbols_mut().extend(letting_vars);
181
            }
182
            "dominance_relation" => {
183
14835
                let Some(dominance) = parse_dominance_relation(&mut ctx, &statement)? else {
184
                    continue;
185
                };
186
14835
                if model.dominance.is_some() {
187
                    ctx.record_error(RecoverableParseError::new(
188
                        "Duplicate dominance relation".to_string(),
189
                        None,
190
                    ));
191
                    continue;
192
14835
                }
193
14835
                model.dominance = Some(dominance);
194
            }
195
            "objective_statement" => {
196
17665
                let Some(objective) = parse_objective_statement(&mut ctx, &statement)? else {
197
                    continue;
198
                };
199
17665
                if model.objective.is_some() {
200
                    ctx.record_error(RecoverableParseError::new(
201
                        "Duplicate objective statement".to_string(),
202
                        None,
203
                    ));
204
                    continue;
205
17665
                }
206
17665
                model.objective = Some(objective);
207
            }
208
            _ => {
209
                ctx.record_error(RecoverableParseError::new(
210
                    format!("Unexpected top-level statement: {}", statement.kind()),
211
                    Some(statement.range()),
212
                ));
213
                continue;
214
            }
215
        }
216
    }
217

            
218
    // Check if there were any recoverable errors
219
1139958
    if !errors.is_empty() {
220
2380
        return Ok((None, source_map));
221
1137578
    }
222
    // otherwise return the model
223
1137578
    Ok((Some(model), source_map))
224
1139958
}
225

            
226
402
pub fn parse_essence(src: &str) -> Result<(Model, SourceMap), Box<ParseErrorCollection>> {
227
402
    let context = Arc::new(RwLock::new(Context::default()));
228
402
    let mut errors = vec![];
229
402
    match parse_essence_with_context_and_map(src, context, &mut errors, None) {
230
        Ok((Some(model), source_map)) => {
231
402
            debug_assert_model_well_formed(&model, "tree-sitter");
232
402
            Ok((model, source_map))
233
        }
234
        Ok((None, _source_map)) => {
235
            // Recoverable errors were found, return them as a ParseErrorCollection
236
            Err(Box::new(ParseErrorCollection::multiple(
237
                errors,
238
                Some(src.to_string()),
239
                None,
240
            )))
241
        }
242
        Err(fatal) => Err(Box::new(ParseErrorCollection::fatal(fatal))),
243
    }
244
402
}
245

            
246
mod test {
247
    #[allow(unused_imports)]
248
    use crate::parse_essence;
249
    #[allow(unused_imports)]
250
    use conjure_cp_core::ast::{
251
        Atom, DeclarationKind, Expression, Metadata, Moo, Name, ReturnType, Typeable,
252
    };
253
    #[allow(unused_imports)]
254
    use conjure_cp_core::{domain_int, matrix_expr, range};
255
    #[allow(unused_imports)]
256
    use std::ops::Deref;
257

            
258
    #[test]
259
1
    pub fn test_parse_xyz() {
260
1
        let src = "
261
1
        find x, y, z : int(1..4)
262
1
        such that x + y + z = 4
263
1
        such that x >= y
264
1
        ";
265

            
266
1
        let (model, _source_map) = parse_essence(src).unwrap();
267

            
268
1
        let st = model.symbols();
269
1
        let x = st.lookup(&Name::user("x")).unwrap();
270
1
        let y = st.lookup(&Name::user("y")).unwrap();
271
1
        let z = st.lookup(&Name::user("z")).unwrap();
272
1
        assert_eq!(x.domain(), Some(domain_int!(1..4)));
273
1
        assert_eq!(y.domain(), Some(domain_int!(1..4)));
274
1
        assert_eq!(z.domain(), Some(domain_int!(1..4)));
275

            
276
1
        let constraints = model.constraints();
277
1
        assert_eq!(constraints.len(), 2);
278

            
279
1
        let c1 = constraints[0].clone();
280
1
        let x_e = Expression::Atomic(Metadata::new(), Atom::new_ref(x));
281
1
        let y_e = Expression::Atomic(Metadata::new(), Atom::new_ref(y));
282
1
        let z_e = Expression::Atomic(Metadata::new(), Atom::new_ref(z));
283
1
        assert_eq!(
284
1
            c1,
285
1
            Expression::Eq(
286
1
                Metadata::new(),
287
1
                Moo::new(Expression::Sum(
288
1
                    Metadata::new(),
289
1
                    Moo::new(matrix_expr!(
290
1
                        Expression::Sum(
291
1
                            Metadata::new(),
292
1
                            Moo::new(matrix_expr!(x_e.clone(), y_e.clone()))
293
1
                        ),
294
1
                        z_e
295
1
                    ))
296
1
                )),
297
1
                Moo::new(Expression::Atomic(Metadata::new(), 4.into()))
298
1
            )
299
        );
300

            
301
1
        let c2 = constraints[1].clone();
302
1
        assert_eq!(
303
1
            c2,
304
1
            Expression::Geq(Metadata::new(), Moo::new(x_e), Moo::new(y_e))
305
        );
306
1
    }
307

            
308
    #[test]
309
1
    pub fn test_parse_bare_int_domain_is_unbounded() {
310
1
        let src = "given a : int";
311
1
        let (model, _source_map) = parse_essence(src).unwrap();
312

            
313
1
        let st = model.symbols();
314
1
        let a = st.lookup(&Name::user("a")).unwrap();
315
1
        assert_eq!(
316
1
            a.domain(),
317
1
            Some(conjure_cp_core::ast::Domain::int_ground(vec![
318
1
                conjure_cp_core::ast::Range::Unbounded
319
1
            ]))
320
        );
321
1
    }
322

            
323
    #[test]
324
1
    pub fn test_parse_empty_int_domain() {
325
1
        let src = "find x : int()";
326
1
        let (model, _source_map) = parse_essence(src).unwrap();
327

            
328
1
        let st = model.symbols();
329
1
        let x = st.lookup(&Name::user("x")).unwrap();
330
1
        assert_eq!(x.domain(), Some(domain_int!()));
331
1
    }
332

            
333
    #[test]
334
1
    pub fn test_pretty_int_domain_reference_bound_without_extra_parentheses() {
335
1
        let src = "
336
1
        given n : int
337
1
        find x : int(1..n)
338
1
        ";
339

            
340
1
        let (model, _source_map) = parse_essence(src).unwrap();
341

            
342
1
        assert!(model.to_string().contains("find x: int(1..n)\n"));
343
1
    }
344

            
345
    #[test]
346
1
    pub fn test_parse_letting_index() {
347
1
        let src = "
348
1
        letting a be [ [ 1,2,3 ; int(1,2,4) ], [ 1,3,2 ; int(1,2,4) ], [ 3,2,1 ; int(1,2,4) ] ; int(-2..0) ]
349
1
        find b: int(1..5)
350
1
        such that
351
1
        b < a[-2,2],
352
1
        allDiff(a[-2,..])
353
1
        ";
354

            
355
1
        let (model, _source_map) = parse_essence(src).unwrap();
356
1
        let st = model.symbols();
357
1
        let a_decl = st.lookup(&Name::user("a")).unwrap();
358
1
        let a = a_decl.as_value_letting().unwrap().deref().clone();
359
1
        assert_eq!(
360
1
            a,
361
1
            matrix_expr!(
362
1
                matrix_expr!(1.into(), 2.into(), 3.into() ; domain_int!(1, 2, 4)),
363
1
                matrix_expr!(1.into(), 3.into(), 2.into() ; domain_int!(1, 2, 4)),
364
1
                matrix_expr!(3.into(), 2.into(), 1.into() ; domain_int!(1, 2, 4));
365
1
                domain_int!(-2..0)
366
            )
367
        )
368
1
    }
369

            
370
    #[test]
371
1
    pub fn test_parse_chained_and_multi_index() {
372
1
        let src = "
373
1
        find x : (bool, (bool, int(1..4)))
374
1
        such that
375
1
            x[2][1] = true,
376
1
            x[2,1] = true
377
1
        ";
378

            
379
1
        let (model, _source_map) = parse_essence(src).unwrap();
380
1
        let constraints = model.constraints();
381
1
        assert_eq!(constraints.len(), 2);
382
1
        for constraint in constraints {
383
2
            let Expression::Eq(_, lhs, _) = constraint else {
384
                panic!("expected an equality constraint");
385
            };
386
2
            assert_eq!(lhs.return_type(), ReturnType::Bool);
387
        }
388
1
    }
389

            
390
    #[test]
391
1
    pub fn test_multi_dimensional_matrix_index_return_type() {
392
1
        let src = "
393
1
        find a : matrix indexed by [int(1..2), int(1..2)] of int(1..4)
394
1
        such that a[1,1] = 1
395
1
        ";
396

            
397
1
        let (model, _source_map) = parse_essence(src).unwrap();
398
1
        let constraints = model.constraints();
399
1
        let Expression::Eq(_, lhs, _) = &constraints[0] else {
400
            panic!("expected an equality constraint");
401
        };
402
1
        assert_eq!(lhs.return_type(), ReturnType::Int);
403
1
    }
404

            
405
    #[test]
406
1
    pub fn value_letting_retains_symbolic_integer_domain() {
407
1
        let src = "
408
1
        given v: int(1..)
409
1
        given b: int(1..)
410
1
        given r: int(1..)
411
1
        letting rv be r * v
412
1
        letting ceilrv be rv / b + toInt(rv % b != 0)
413
1
        ";
414

            
415
1
        let (model, _source_map) = parse_essence(src).unwrap();
416
1
        let symbols = model.symbols();
417

            
418
1
        for name in ["rv", "ceilrv"] {
419
2
            let declaration = symbols.lookup(&Name::user(name)).unwrap();
420
2
            assert!(
421
2
                declaration.domain().is_some(),
422
                "{name} should have a domain"
423
            );
424
2
            assert!(matches!(
425
2
                declaration.kind().deref(),
426
                DeclarationKind::ValueLetting(_, Some(_))
427
            ));
428
        }
429
1
        drop(symbols);
430

            
431
1
        let (params, _source_map) = parse_essence(
432
1
            "
433
1
            letting v be 8
434
1
            letting b be 28
435
1
            letting r be 14
436
1
            ",
437
1
        )
438
1
        .unwrap();
439
1
        let model = conjure_cp_core::instantiate::instantiate_model(model, params).unwrap();
440
1
        let symbols = model.symbols();
441
1
        let rv = symbols.lookup(&Name::user("rv")).unwrap();
442
1
        assert_eq!(
443
1
            rv.domain().unwrap().resolve().unwrap().as_ref(),
444
1
            domain_int!(112).resolve().unwrap().as_ref()
445
        );
446
1
    }
447

            
448
    #[test]
449
1
    pub fn test_parse_table_in_quantifier() {
450
1
        let src = "
451
1
        find x, y, z : int(1..3)
452
1
        such that forAll i : int(1..1) . table([x,y,z], [[1,2,3]])
453
1
        ";
454

            
455
1
        let (model, _source_map) = parse_essence(src).unwrap();
456
1
        let constraints = model.constraints();
457
1
        assert_eq!(constraints.len(), 1);
458

            
459
1
        let Expression::And(_, comprehension_expr) = &constraints[0] else {
460
            panic!("expected forAll to parse as an And over a comprehension");
461
        };
462
1
        let Expression::Comprehension(_, comprehension) = comprehension_expr.as_ref() else {
463
            panic!("expected forAll body to be a comprehension");
464
        };
465

            
466
1
        assert!(matches!(
467
1
            comprehension.return_expression,
468
            Expression::Table(_, _, _)
469
        ));
470
1
    }
471

            
472
    #[test]
473
1
    pub fn test_parse_objective_statement() {
474
1
        let src = "
475
1
        find cost : int(0..10)
476
1
        minimising cost
477
1
        such that cost = 5
478
1
        ";
479

            
480
1
        let (model, _source_map) = parse_essence(src).unwrap();
481
1
        assert!(matches!(
482
1
            model.objective.as_ref().unwrap().direction,
483
            conjure_cp_core::ast::OptimiseDirection::Minimising
484
        ));
485

            
486
1
        let st = model.symbols();
487
1
        let objective = model.objective.as_ref().unwrap();
488
1
        let cost = st.lookup(&Name::user("cost")).unwrap();
489
1
        assert_eq!(
490
1
            objective.expression,
491
1
            Expression::Atomic(Metadata::new(), Atom::new_ref(cost))
492
        );
493
1
    }
494

            
495
    #[test]
496
1
    pub fn test_parse_pareto_in_dominance_relation() {
497
1
        let src = "
498
1
        find x : int(0..3)
499
1

            
500
1
        dominance relation
501
1
            pareto(minimising x)
502
1
        ";
503

            
504
1
        let (model, _source_map) = parse_essence(src).unwrap();
505
1
        let st = model.symbols();
506
1
        let x = st.lookup(&Name::user("x")).unwrap();
507
1
        let x_e = Expression::Atomic(Metadata::new(), Atom::new_ref(x.clone()));
508
1
        let x_prev = Expression::FromSolution(Metadata::new(), Moo::new(Atom::new_ref(x)));
509

            
510
1
        assert_eq!(
511
1
            model.dominance,
512
1
            Some(Expression::DominanceRelation(
513
1
                Metadata::new(),
514
1
                Moo::new(Expression::And(
515
1
                    Metadata::new(),
516
1
                    Moo::new(matrix_expr!(
517
1
                        Expression::Leq(
518
1
                            Metadata::new(),
519
1
                            Moo::new(x_e.clone()),
520
1
                            Moo::new(x_prev.clone())
521
1
                        ),
522
1
                        Expression::Lt(Metadata::new(), Moo::new(x_e), Moo::new(x_prev))
523
1
                    ))
524
1
                ))
525
1
            ))
526
        );
527
1
    }
528

            
529
    #[test]
530
1
    pub fn test_parse_pareto_with_mixed_directions() {
531
1
        let src = "
532
1
        find x : int(0..3)
533
1
        find y : int(0..3)
534
1

            
535
1
        dominance relation
536
1
            pareto(minimising x, maximising y)
537
1
        ";
538

            
539
1
        let (model, _source_map) = parse_essence(src).unwrap();
540
1
        let st = model.symbols();
541
1
        let x = st.lookup(&Name::user("x")).unwrap();
542
1
        let y = st.lookup(&Name::user("y")).unwrap();
543
1
        let x_e = Expression::Atomic(Metadata::new(), Atom::new_ref(x.clone()));
544
1
        let y_e = Expression::Atomic(Metadata::new(), Atom::new_ref(y.clone()));
545
1
        let x_prev = Expression::FromSolution(Metadata::new(), Moo::new(Atom::new_ref(x)));
546
1
        let y_prev = Expression::FromSolution(Metadata::new(), Moo::new(Atom::new_ref(y)));
547

            
548
1
        assert_eq!(
549
1
            model.dominance,
550
1
            Some(Expression::DominanceRelation(
551
1
                Metadata::new(),
552
1
                Moo::new(Expression::And(
553
1
                    Metadata::new(),
554
1
                    Moo::new(matrix_expr!(
555
1
                        Expression::Leq(
556
1
                            Metadata::new(),
557
1
                            Moo::new(x_e.clone()),
558
1
                            Moo::new(x_prev.clone())
559
1
                        ),
560
1
                        Expression::Geq(
561
1
                            Metadata::new(),
562
1
                            Moo::new(y_e.clone()),
563
1
                            Moo::new(y_prev.clone())
564
1
                        ),
565
1
                        Expression::Or(
566
1
                            Metadata::new(),
567
1
                            Moo::new(matrix_expr!(
568
1
                                Expression::Lt(Metadata::new(), Moo::new(x_e), Moo::new(x_prev)),
569
1
                                Expression::Gt(Metadata::new(), Moo::new(y_e), Moo::new(y_prev))
570
1
                            ))
571
1
                        )
572
1
                    ))
573
1
                ))
574
1
            ))
575
        );
576
1
    }
577

            
578
    #[test]
579
1
    pub fn test_parse_pareto_over_expression_component() {
580
1
        let src = "
581
1
        find x : int(0..3)
582
1

            
583
1
        dominance relation
584
1
            pareto(minimising x + 1)
585
1
        ";
586

            
587
1
        let (model, _source_map) = parse_essence(src).unwrap();
588
1
        let st = model.symbols();
589
1
        let x = st.lookup(&Name::user("x")).unwrap();
590
1
        let x_e = Expression::Atomic(Metadata::new(), Atom::new_ref(x.clone()));
591
1
        let x_prev = Expression::FromSolution(Metadata::new(), Moo::new(Atom::new_ref(x)));
592
1
        let one = Expression::Atomic(Metadata::new(), 1.into());
593
1
        let current = Expression::Sum(
594
1
            Metadata::new(),
595
1
            Moo::new(matrix_expr!(x_e.clone(), one.clone())),
596
1
        );
597
1
        let previous = Expression::Sum(Metadata::new(), Moo::new(matrix_expr!(x_prev, one)));
598

            
599
1
        assert_eq!(
600
1
            model.dominance,
601
1
            Some(Expression::DominanceRelation(
602
1
                Metadata::new(),
603
1
                Moo::new(Expression::And(
604
1
                    Metadata::new(),
605
1
                    Moo::new(matrix_expr!(
606
1
                        Expression::Leq(
607
1
                            Metadata::new(),
608
1
                            Moo::new(current.clone()),
609
1
                            Moo::new(previous.clone())
610
1
                        ),
611
1
                        Expression::Lt(Metadata::new(), Moo::new(current), Moo::new(previous))
612
1
                    ))
613
1
                ))
614
1
            ))
615
        );
616
1
    }
617

            
618
    #[test]
619
1
    pub fn test_parse_permutation_domain_literal_and_operators() {
620
1
        let src = "
621
1
        find p : permutation (numMoved 3) of int(1..5)
622
1
        letting q be permutation((1,2,3),(4,5))
623
1
        find x, y : int(1..5)
624
1
        such that y = image(p, x)
625
1
        such that inverse(p, q)
626
1
        such that q = permInverse(p)
627
1
        such that y = image(compose(p, q), x)
628
1
        ";
629

            
630
1
        let (model, _source_map) = parse_essence(src).unwrap();
631

            
632
1
        let st = model.symbols();
633
1
        let p = st.lookup(&Name::user("p")).unwrap();
634
1
        let ground = p.domain().unwrap().resolve().unwrap();
635
1
        let conjure_cp_core::ast::GroundDomain::Permutation(attrs, inner) = ground.as_ref() else {
636
            panic!("expected a permutation domain, got {ground}");
637
        };
638
1
        assert_eq!(attrs.num_moved, range!(3));
639
1
        assert_eq!(**inner, *domain_int!(1..5).resolve().unwrap());
640

            
641
1
        let constraints = model.constraints();
642
1
        assert_eq!(constraints.len(), 4);
643
1
        assert!(matches!(constraints[0], Expression::Eq(_, _, _)));
644
1
        assert!(matches!(constraints[1], Expression::Inverse(_, _, _)));
645
1
        assert!(matches!(constraints[2], Expression::Eq(_, _, _)));
646
1
        let Expression::Eq(_, _, rhs) = &constraints[2] else {
647
            unreachable!()
648
        };
649
1
        assert!(matches!(rhs.as_ref(), Expression::PermInverse(_, _)));
650
1
        assert!(matches!(constraints[3], Expression::Eq(_, _, _)));
651
1
        let Expression::Eq(_, _, rhs) = &constraints[3] else {
652
            unreachable!()
653
        };
654
1
        let Expression::Image(_, compose_expr, _) = rhs.as_ref() else {
655
            unreachable!()
656
        };
657
1
        assert!(matches!(
658
1
            compose_expr.as_ref(),
659
            Expression::Compose(_, _, _)
660
        ));
661
1
    }
662

            
663
    #[test]
664
1
    pub fn test_parse_permutation_unattributed_domain_and_empty_literal() {
665
        // `letting q be permutation()`'s own domain would need type inference from its (empty)
666
        // literal, which -- like the equivalent empty-partition-literal case -- is not yet
667
        // implemented (`GroundDomain::from_literal_vec`'s `AbstractLiteral::Permutation` arm is a
668
        // deliberate `todo!()`, mirroring Partition's own pre-existing gap); so this test only
669
        // inspects the parsed literal's AST shape directly, without triggering that inference.
670
1
        let src = "
671
1
        find p : permutation of int(1..3)
672
1
        letting q be permutation()
673
1
        find x : int(1..3)
674
1
        such that x = image(p, 1)
675
1
        ";
676

            
677
1
        let (model, _source_map) = parse_essence(src).unwrap();
678

            
679
1
        let st = model.symbols();
680
1
        let p = st.lookup(&Name::user("p")).unwrap();
681
1
        let ground = p.domain().unwrap().resolve().unwrap();
682
1
        let conjure_cp_core::ast::GroundDomain::Permutation(attrs, _) = ground.as_ref() else {
683
            panic!("expected a permutation domain, got {ground}");
684
        };
685
1
        assert_eq!(attrs.num_moved, conjure_cp_core::ast::Range::Unbounded);
686

            
687
1
        let q_decl = st.lookup(&Name::user("q")).unwrap();
688
1
        let q = q_decl.as_value_letting().unwrap().deref().clone();
689
1
        assert_eq!(
690
1
            q,
691
1
            Expression::AbstractLiteral(
692
1
                Metadata::new(),
693
1
                conjure_cp_core::ast::AbstractLiteral::Permutation(vec![])
694
1
            )
695
        );
696
1
    }
697
}